Skip to content

Commit 20408f8

Browse files
committed
Improved dual AUTH handling
1 parent 344cf22 commit 20408f8

File tree

7 files changed

+284
-43
lines changed

7 files changed

+284
-43
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@ ALLOWED_PATHS: "login,about,api/health,api/public"
205205

206206
Now `/login` and `/api/health` work without a hash, but `/dashboard` still requires it.
207207

208+
**Important - Reserved Paths:**
209+
- `/nhl-auth/` is reserved for internal authentication endpoints and cannot be used in ALLOWED_PATHS
210+
- `/login` is reserved for the login page
211+
- All other paths are available for use in ALLOWED_PATHS
212+
- The `/auth` path is now available for your application (previously reserved)
213+
208214
## Dynamic Path Allowlisting (Advanced Feature)
209215

210216
### What Problem Does It Solve?

auth-service/app.js

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ const PORT = 9999;
1010
// Configuration from environment
1111
const USERNAME = process.env.USER || '';
1212
const PASSWORD = process.env.PASSWORD || '';
13+
const AUTH_HASH = process.env.AUTH_HASH || '';
1314
const SESSION_DURATION_HOURS = parseInt(process.env.SESSION_DURATION_HOURS || '720', 10);
1415
const SESSION_DURATION_MS = SESSION_DURATION_HOURS * 60 * 60 * 1000;
1516

17+
// Generate password hash for session validation
18+
// When password changes (container restart), password-based sessions become invalid
19+
const PASSWORD_HASH = crypto.createHash('sha256').update(PASSWORD + USERNAME).digest('hex');
20+
1621
// In-memory session store
17-
// Format: { sessionId: { expires: timestamp } }
22+
// Format: { sessionId: { expires: timestamp, passwordHash?: string, authHash?: string } }
23+
// Sessions can be authenticated via password OR auth hash
1824
const sessions = {};
1925

2026
// Generate secure random session ID
@@ -123,7 +129,7 @@ app.get('/login', (req, res) => {
123129
<body>
124130
<div class="container">
125131
<h1>🔒 Login Required</h1>
126-
<form method="POST" action="/auth/login">
132+
<form method="POST" action="/nhl-auth/login">
127133
<div class="form-group">
128134
<label>Username</label>
129135
<input type="text" name="username" required autofocus>
@@ -157,7 +163,7 @@ app.get('/login', (req, res) => {
157163
});
158164

159165
// Handle login submission
160-
app.post('/auth/login', async (req, res) => {
166+
app.post('/nhl-auth/login', async (req, res) => {
161167
const { username, password, redirect } = req.body;
162168
const startTime = Date.now();
163169

@@ -168,7 +174,8 @@ app.post('/auth/login', async (req, res) => {
168174
// Create session
169175
const sessionId = generateSessionId();
170176
sessions[sessionId] = {
171-
expires: Date.now() + SESSION_DURATION_MS
177+
expires: Date.now() + SESSION_DURATION_MS,
178+
passwordHash: PASSWORD_HASH
172179
};
173180

174181
console.log(`[Auth Service] Login successful for user: ${username}`);
@@ -197,14 +204,34 @@ app.post('/auth/login', async (req, res) => {
197204
});
198205

199206
// Auth check endpoint (called by nginx auth_request)
200-
app.get('/auth/check', (req, res) => {
207+
app.get('/nhl-auth/check', (req, res) => {
201208
// Check for existing session first
202209
let sessionId = req.cookies.nginxhashlock_session;
203210

204-
if (sessionId && sessions[sessionId] && sessions[sessionId].expires > Date.now()) {
205-
// Valid session exists
206-
console.log(`[Auth Service] Auth check passed via session (${sessionId.substring(0, 8)}...)`);
207-
return res.status(200).send('OK');
211+
if (sessionId && sessions[sessionId]) {
212+
const session = sessions[sessionId];
213+
214+
// Check if session is expired
215+
if (session.expires < Date.now()) {
216+
console.log(`[Auth Service] Auth check failed: Session expired (${sessionId.substring(0, 8)}...)`);
217+
delete sessions[sessionId];
218+
// Continue to check other auth methods
219+
}
220+
// Check if credentials are still valid
221+
else {
222+
// Session is valid if EITHER password hash OR auth hash matches
223+
const passwordValid = session.passwordHash && session.passwordHash === PASSWORD_HASH;
224+
const authHashValid = session.authHash && session.authHash === AUTH_HASH;
225+
226+
if (passwordValid || authHashValid) {
227+
console.log(`[Auth Service] Auth check passed via session (${sessionId.substring(0, 8)}...)`);
228+
return res.status(200).send('OK');
229+
} else {
230+
console.log(`[Auth Service] Auth check failed: Credentials changed, invalidating session (${sessionId.substring(0, 8)}...)`);
231+
delete sessions[sessionId];
232+
// Continue to check other auth methods
233+
}
234+
}
208235
}
209236

210237
// No valid session - check if hash parameter is valid
@@ -219,7 +246,8 @@ app.get('/auth/check', (req, res) => {
219246
if (!sessionId || !sessions[sessionId]) {
220247
sessionId = generateSessionId();
221248
sessions[sessionId] = {
222-
expires: Date.now() + SESSION_DURATION_MS
249+
expires: Date.now() + SESSION_DURATION_MS,
250+
authHash: AUTH_HASH
223251
};
224252

225253
console.log(`[Auth Service] Session created for hash auth: ${sessionId.substring(0, 8)}... (expires in ${SESSION_DURATION_HOURS}h)`);
@@ -258,36 +286,57 @@ app.get('/auth/check', (req, res) => {
258286
return res.status(401).send('Unauthorized');
259287
}
260288

289+
// Check if password has changed
290+
if (session.passwordHash && session.passwordHash !== PASSWORD_HASH) {
291+
console.log(`[Auth Service] Auth check failed: Password changed, invalidating session (${sessionId.substring(0, 8)}...)`);
292+
delete sessions[sessionId];
293+
return res.status(401).send('Unauthorized');
294+
}
295+
261296
// Session is valid
262297
console.log(`[Auth Service] Auth check passed via session (${sessionId.substring(0, 8)}...)`);
263298
res.status(200).send('OK');
264299
});
265300

266301
// Establish session endpoint (for hash authentication to set cookies properly)
267-
app.get('/auth/establish-session', (req, res) => {
302+
app.get('/nhl-auth/establish-session', (req, res) => {
268303
// Check if hash parameter is valid
269304
if (process.env.AUTH_HASH) {
270305
const hash = req.query.hash;
271-
const returnTo = req.query.return_to || '/';
306+
let returnTo = req.query.return_to || '/';
307+
308+
// Strip hash parameter from return URL to prevent redirect loop
309+
returnTo = returnTo.replace(/[?&]hash=[^&]+(&|$)/, (match, p1) => p1 === '&' ? '&' : '?').replace(/[?&]$/, '').replace(/\?$/, '') || '/';
272310

273311
if (hash && hash === process.env.AUTH_HASH) {
274312
// Check if session already exists
275313
let sessionId = req.cookies.nginxhashlock_session;
276314

277-
if (sessionId && sessions[sessionId] && sessions[sessionId].expires > Date.now()) {
278-
// Valid session already exists
279-
console.log(`[Auth Service] Session already valid: ${sessionId.substring(0, 8)}...`);
280-
// Redirect back if requested
281-
if (req.query.return_to) {
282-
return res.redirect(returnTo);
315+
if (sessionId && sessions[sessionId]) {
316+
const session = sessions[sessionId];
317+
// Check if session is valid (not expired and credentials are still valid)
318+
const passwordValid = session.passwordHash && session.passwordHash === PASSWORD_HASH;
319+
const authHashValid = session.authHash && session.authHash === AUTH_HASH;
320+
321+
if (session.expires > Date.now() && (passwordValid || authHashValid)) {
322+
// Valid session already exists
323+
console.log(`[Auth Service] Session already valid: ${sessionId.substring(0, 8)}...`);
324+
// Redirect back if requested
325+
if (req.query.return_to) {
326+
return res.redirect(returnTo);
327+
}
328+
return res.status(200).json({ status: 'ok', message: 'Session already valid' });
329+
} else {
330+
// Session expired or password changed, delete it
331+
delete sessions[sessionId];
283332
}
284-
return res.status(200).json({ status: 'ok', message: 'Session already valid' });
285333
}
286334

287335
// Create new session
288336
sessionId = generateSessionId();
289337
sessions[sessionId] = {
290-
expires: Date.now() + SESSION_DURATION_MS
338+
expires: Date.now() + SESSION_DURATION_MS,
339+
authHash: AUTH_HASH
291340
};
292341

293342
console.log(`[Auth Service] Session established via hash: ${sessionId.substring(0, 8)}... (expires in ${SESSION_DURATION_HOURS}h)`);

auto-add-hash.sh

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,34 @@ const fs = require('fs');
1818
const DYNAMIC_PATHS_FILE = process.env.DYNAMIC_PATHS_FILE || '/tmp/dynamic_paths/allowed.txt';
1919
const DEFAULT_TTL = 3600; // 1 hour in seconds
2020
const LISTEN_PORT = 9997;
21-
const AUTH_SERVICE_URL = 'http://127.0.0.1:9999/auth/check';
21+
const AUTH_SERVICE_URL = 'http://127.0.0.1:9999/nhl-auth/check';
2222
23+
// Helper function to check if IP is from Docker internal network
24+
function isDockerInternalIP(ip) {
25+
// Remove IPv6 prefix if present
26+
ip = ip.replace(/^::ffff:/, '');
27+
28+
// Localhost
29+
if (ip === '127.0.0.1' || ip === '::1') return true;
30+
31+
const parts = ip.split('.');
32+
if (parts.length !== 4) return false;
33+
34+
const first = parseInt(parts[0]);
35+
const second = parseInt(parts[1]);
36+
37+
// Docker network ranges
38+
// 10.0.0.0/8
39+
if (first === 10) return true;
40+
41+
// 172.16.0.0/12
42+
if (first === 172 && second >= 16 && second <= 31) return true;
43+
44+
// 192.168.0.0/16
45+
if (first === 192 && second === 168) return true;
46+
47+
return false;
48+
}
2349
2450
const server = http.createServer((req, res) => {
2551
const originalUri = req.headers['x-original-uri'] || '';
@@ -33,6 +59,36 @@ const server = http.createServer((req, res) => {
3359
return;
3460
}
3561
62+
// Handle request errors to prevent crashes
63+
req.on('error', (err) => {
64+
console.error('[AUTO-ADD] Request error:', err);
65+
try {
66+
res.writeHead(500);
67+
res.end();
68+
} catch (e) {
69+
console.error('[AUTO-ADD] Failed to send error response:', e);
70+
}
71+
});
72+
73+
res.on('error', (err) => {
74+
console.error('[AUTO-ADD] Response error:', err);
75+
});
76+
77+
// Check if request is from Docker internal network (container-to-container)
78+
const remoteAddr = req.headers['x-real-ip'] || req.connection.remoteAddress || '';
79+
const isInternalRequest = isDockerInternalIP(remoteAddr);
80+
81+
if (isInternalRequest) {
82+
// Internal requests from Stremio server itself - always allow and add to allowlist
83+
console.log(`[AUTO-ADD] Internal request from ${remoteAddr} for hash ${hash} - bypassing auth`);
84+
if (!isHashAllowed(hash)) {
85+
addHashToAllowlist(hash);
86+
}
87+
res.writeHead(204);
88+
res.end();
89+
return;
90+
}
91+
3692
// FIRST check if hash is already in allowlist (for subsequent requests)
3793
if (isHashAllowed(hash)) {
3894
res.writeHead(204);
@@ -90,8 +146,9 @@ const server = http.createServer((req, res) => {
90146
}
91147
});
92148
93-
authReq.on('error', () => {
149+
authReq.on('error', (err) => {
94150
// Auth service error - fall back to dynamic allowlist check
151+
console.error('[AUTO-ADD] Auth service connection error:', err.message);
95152
if (isHashAllowed(hash)) {
96153
res.writeHead(204);
97154
res.end();
@@ -101,6 +158,12 @@ const server = http.createServer((req, res) => {
101158
}
102159
});
103160
161+
// Add timeout to prevent hanging connections
162+
authReq.setTimeout(5000, () => {
163+
console.error('[AUTO-ADD] Auth service request timeout');
164+
authReq.destroy();
165+
});
166+
104167
authReq.end();
105168
});
106169
@@ -167,10 +230,37 @@ function addHashToAllowlist(hash) {
167230
}
168231
}
169232
233+
// Handle server-level errors
234+
server.on('error', (err) => {
235+
console.error('[AUTO-ADD] Server error:', err);
236+
});
237+
238+
server.on('clientError', (err, socket) => {
239+
console.error('[AUTO-ADD] Client error:', err.message);
240+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
241+
});
242+
170243
server.listen(LISTEN_PORT, '127.0.0.1', () => {
171244
console.log(`[AUTO-ADD] Listening on port ${LISTEN_PORT}`);
172245
});
246+
247+
// Prevent crashes from uncaught exceptions
248+
process.on('uncaughtException', (err) => {
249+
console.error('[AUTO-ADD] Uncaught exception:', err);
250+
console.error('[AUTO-ADD] Service continuing...');
251+
});
252+
253+
process.on('unhandledRejection', (reason, promise) => {
254+
console.error('[AUTO-ADD] Unhandled rejection at:', promise, 'reason:', reason);
255+
console.error('[AUTO-ADD] Service continuing...');
256+
});
173257
JSEOF
174258

175-
# Run the Node.js server
176-
exec node /tmp/auto-add-hash-server.js
259+
# Run the Node.js server with auto-restart
260+
while true; do
261+
echo "[AUTO-ADD] Starting service..."
262+
node /tmp/auto-add-hash-server.js
263+
EXIT_CODE=$?
264+
echo "[AUTO-ADD] Service exited with code $EXIT_CODE, restarting in 2 seconds..."
265+
sleep 2
266+
done

0 commit comments

Comments
 (0)