Refactor auth routes: explicit dependency injection
- Updated all auth route modules to use destructured dependencies - Added JSDoc comments for factory functions - Replaced ctx. references with direct parameter access - Updated auth/index.js to extract and pass explicit dependencies - sso-gate.js maintains session helper exports from session-handlers - All files pass syntax validation Files refactored: - routes/auth/keys.js - routes/auth/session-handlers.js - routes/auth/sso-gate.js - routes/auth/totp.js - routes/auth/index.js (orchestrator)
This commit is contained in:
@@ -4,14 +4,31 @@ const initKeys = require('./keys');
|
|||||||
const initSessionHandlers = require('./session-handlers');
|
const initSessionHandlers = require('./session-handlers');
|
||||||
const initSsoGate = require('./sso-gate');
|
const initSsoGate = require('./sso-gate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth routes aggregator
|
||||||
|
* Assembles all auth sub-routes with their dependencies
|
||||||
|
* @param {Object} ctx - Application context (for backward compatibility)
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { getAppSession, appSessionCache } = initSessionHandlers(ctx);
|
// Extract dependencies from context
|
||||||
|
const deps = {
|
||||||
|
authManager: ctx.authManager,
|
||||||
|
credentialManager: ctx.credentialManager,
|
||||||
|
totpConfig: ctx.totpConfig,
|
||||||
|
session: ctx.session,
|
||||||
|
asyncHandler: ctx.asyncHandler,
|
||||||
|
errorResponse: ctx.errorResponse,
|
||||||
|
log: ctx.log
|
||||||
|
};
|
||||||
|
|
||||||
router.use(initTotp(ctx));
|
const { getAppSession, appSessionCache } = initSessionHandlers(deps);
|
||||||
router.use(initKeys(ctx));
|
|
||||||
router.use(initSsoGate(ctx, getAppSession, appSessionCache));
|
router.use(initTotp(deps));
|
||||||
|
router.use(initKeys(deps));
|
||||||
|
router.use(initSsoGate({ ...deps, getAppSession, appSessionCache }));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors');
|
const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors');
|
||||||
|
/**
|
||||||
|
* Auth API keys routes factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.authManager - Auth manager
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ authManager, asyncHandler, log }) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Helper function to parse expiration strings to milliseconds
|
// Helper function to parse expiration strings to milliseconds
|
||||||
@@ -24,18 +32,18 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List all API keys
|
// List all API keys
|
||||||
router.get('/auth/keys', ctx.asyncHandler(async (req, res) => {
|
router.get('/auth/keys', asyncHandler(async (req, res) => {
|
||||||
// Require session authentication (not API key - can't manage keys with key itself)
|
// Require session authentication (not API key - can't manage keys with key itself)
|
||||||
if (!req.auth || req.auth.type !== 'session') {
|
if (!req.auth || req.auth.type !== 'session') {
|
||||||
throw new ForbiddenError('API key management requires TOTP session authentication');
|
throw new ForbiddenError('API key management requires TOTP session authentication');
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = await ctx.authManager.listAPIKeys();
|
const keys = await authManager.listAPIKeys();
|
||||||
res.json({ success: true, keys });
|
res.json({ success: true, keys });
|
||||||
}, 'auth-keys-list'));
|
}, 'auth-keys-list'));
|
||||||
|
|
||||||
// Generate new API key
|
// Generate new API key
|
||||||
router.post('/auth/keys', ctx.asyncHandler(async (req, res) => {
|
router.post('/auth/keys', asyncHandler(async (req, res) => {
|
||||||
// Require session authentication
|
// Require session authentication
|
||||||
if (!req.auth || req.auth.type !== 'session') {
|
if (!req.auth || req.auth.type !== 'session') {
|
||||||
throw new ForbiddenError('API key generation requires TOTP session authentication');
|
throw new ForbiddenError('API key generation requires TOTP session authentication');
|
||||||
@@ -53,7 +61,7 @@ module.exports = function(ctx) {
|
|||||||
throw new ValidationError(`Invalid scopes. Valid options: ${validScopes.join(', ')}`, 'scopes');
|
throw new ValidationError(`Invalid scopes. Valid options: ${validScopes.join(', ')}`, 'scopes');
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyData = await ctx.authManager.generateAPIKey(
|
const keyData = await authManager.generateAPIKey(
|
||||||
name.trim(),
|
name.trim(),
|
||||||
scopes || ['read', 'write']
|
scopes || ['read', 'write']
|
||||||
);
|
);
|
||||||
@@ -70,7 +78,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'auth-keys-generate'));
|
}, 'auth-keys-generate'));
|
||||||
|
|
||||||
// Revoke API key
|
// Revoke API key
|
||||||
router.delete('/auth/keys/:keyId', ctx.asyncHandler(async (req, res) => {
|
router.delete('/auth/keys/:keyId', asyncHandler(async (req, res) => {
|
||||||
// Require session authentication
|
// Require session authentication
|
||||||
if (!req.auth || req.auth.type !== 'session') {
|
if (!req.auth || req.auth.type !== 'session') {
|
||||||
throw new ForbiddenError('API key revocation requires TOTP session authentication');
|
throw new ForbiddenError('API key revocation requires TOTP session authentication');
|
||||||
@@ -82,7 +90,7 @@ module.exports = function(ctx) {
|
|||||||
throw new ValidationError('Key ID is required', 'keyId');
|
throw new ValidationError('Key ID is required', 'keyId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await ctx.authManager.revokeAPIKey(keyId);
|
const success = await authManager.revokeAPIKey(keyId);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.json({ success: true, message: 'API key revoked successfully' });
|
res.json({ success: true, message: 'API key revoked successfully' });
|
||||||
@@ -92,7 +100,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'auth-keys-revoke'));
|
}, 'auth-keys-revoke'));
|
||||||
|
|
||||||
// Generate JWT from TOTP session
|
// Generate JWT from TOTP session
|
||||||
router.post('/auth/jwt', ctx.asyncHandler(async (req, res) => {
|
router.post('/auth/jwt', asyncHandler(async (req, res) => {
|
||||||
// Require session authentication
|
// Require session authentication
|
||||||
if (!req.auth || req.auth.type !== 'session') {
|
if (!req.auth || req.auth.type !== 'session') {
|
||||||
throw new ForbiddenError('JWT generation requires TOTP session authentication');
|
throw new ForbiddenError('JWT generation requires TOTP session authentication');
|
||||||
@@ -106,7 +114,7 @@ module.exports = function(ctx) {
|
|||||||
throw new ValidationError('Invalid expiresIn format. Use: 60s, 15m, 24h, 7d, 1y', 'expiresIn');
|
throw new ValidationError('Invalid expiresIn format. Use: 60s, 15m, 24h, 7d, 1y', 'expiresIn');
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await ctx.authManager.generateJWT(
|
const token = await authManager.generateJWT(
|
||||||
{
|
{
|
||||||
sub: userId || 'dashcaddy-admin',
|
sub: userId || 'dashcaddy-admin',
|
||||||
scope: ['admin'] // Session-generated JWTs have admin scope
|
scope: ['admin'] // Session-generated JWTs have admin scope
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
||||||
const { createCache, CACHE_CONFIGS } = require('../../cache-config');
|
const { createCache, CACHE_CONFIGS } = require('../../cache-config');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ authManager, credentialManager, asyncHandler, errorResponse, log }) {
|
||||||
// App session cache for auto-login
|
// App session cache for auto-login
|
||||||
|
/**
|
||||||
|
* Auth session handlers routes factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.authManager - Auth manager
|
||||||
|
* @param {Object} deps.credentialManager - Credential manager
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
|
const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
|
||||||
|
|
||||||
async function getAppSession(serviceId, baseUrl, username, password) {
|
async function getAppSession(serviceId, baseUrl, username, password) {
|
||||||
@@ -36,12 +46,12 @@ module.exports = function(ctx) {
|
|||||||
const location = locationMatch ? locationMatch[1].trim() : '';
|
const location = locationMatch ? locationMatch[1].trim() : '';
|
||||||
if (location && !location.includes('login')) {
|
if (location && !location.includes('login')) {
|
||||||
appSessionCache.set(serviceId, { cookies: '__ip_session=1', exp: Date.now() + SESSION_TTL.IP_SESSION });
|
appSessionCache.set(serviceId, { cookies: '__ip_session=1', exp: Date.now() + SESSION_TTL.IP_SESSION });
|
||||||
ctx.log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId });
|
log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId });
|
||||||
return '__ip_session=1';
|
return '__ip_session=1';
|
||||||
}
|
}
|
||||||
ctx.log.warn('auth', 'Router auto-login failed', { serviceId });
|
log.warn('auth', 'Router auto-login failed', { serviceId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) });
|
log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) });
|
||||||
}
|
}
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
return null;
|
return null;
|
||||||
@@ -73,12 +83,12 @@ module.exports = function(ctx) {
|
|||||||
serverId: authData.ServerId, serverName: authData.User?.ServerName || serviceId,
|
serverId: authData.ServerId, serverName: authData.User?.ServerName || serviceId,
|
||||||
};
|
};
|
||||||
appSessionCache.set(serviceId, { cookies: `token=${authData.AccessToken}`, token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
appSessionCache.set(serviceId, { cookies: `token=${authData.AccessToken}`, token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
||||||
ctx.log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId });
|
log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId });
|
||||||
return `token=${authData.AccessToken}`;
|
return `token=${authData.AccessToken}`;
|
||||||
}
|
}
|
||||||
ctx.log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status });
|
log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
||||||
}
|
}
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
return null;
|
return null;
|
||||||
@@ -99,12 +109,12 @@ module.exports = function(ctx) {
|
|||||||
const token = plexData?.user?.authToken;
|
const token = plexData?.user?.authToken;
|
||||||
if (token) {
|
if (token) {
|
||||||
appSessionCache.set(serviceId, { cookies: `plexToken=${token}`, token, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
appSessionCache.set(serviceId, { cookies: `plexToken=${token}`, token, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
||||||
ctx.log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId });
|
log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId });
|
||||||
return `plexToken=${token}`;
|
return `plexToken=${token}`;
|
||||||
}
|
}
|
||||||
ctx.log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status });
|
log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message });
|
log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message });
|
||||||
}
|
}
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
return null;
|
return null;
|
||||||
@@ -129,11 +139,11 @@ module.exports = function(ctx) {
|
|||||||
if (data.token) {
|
if (data.token) {
|
||||||
const cookies = `token=${data.token}`;
|
const cookies = `token=${data.token}`;
|
||||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||||
ctx.log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId });
|
log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId });
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
} catch (e) { /* JSON parse failed */ }
|
} catch (e) { /* JSON parse failed */ }
|
||||||
ctx.log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status });
|
log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status });
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -141,7 +151,7 @@ module.exports = function(ctx) {
|
|||||||
if (serviceId === 'torrent') {
|
if (serviceId === 'torrent') {
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
if (text.trim() !== 'Ok.') {
|
if (text.trim() !== 'Ok.') {
|
||||||
ctx.log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() });
|
log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() });
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -151,7 +161,7 @@ module.exports = function(ctx) {
|
|||||||
if (setCookies.length > 0) {
|
if (setCookies.length > 0) {
|
||||||
const cookies = setCookies.map(c => c.split(';')[0]).join('; ');
|
const cookies = setCookies.map(c => c.split(';')[0]).join('; ');
|
||||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||||
ctx.log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length });
|
log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length });
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,14 +169,14 @@ module.exports = function(ctx) {
|
|||||||
if (rawCookie) {
|
if (rawCookie) {
|
||||||
const cookies = rawCookie.split(/,(?=[^ ])/).map(c => c.split(';')[0].trim()).join('; ');
|
const cookies = rawCookie.split(/,(?=[^ ])/).map(c => c.split(';')[0].trim()).join('; ');
|
||||||
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION });
|
||||||
ctx.log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId });
|
log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId });
|
||||||
return cookies;
|
return cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status });
|
log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status });
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
log.warn('auth', 'Auto-login error', { serviceId, error: e.message });
|
||||||
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN });
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,18 +2,26 @@ const express = require('express');
|
|||||||
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
|
||||||
const { AuthenticationError, NotFoundError } = require('../errors');
|
const { AuthenticationError, NotFoundError } = require('../errors');
|
||||||
|
|
||||||
module.exports = function(ctx, getAppSession, appSessionCache) {
|
/**
|
||||||
|
* Auth SSO gate routes factory
|
||||||
|
* @param {Object} deps - Explicit dependencies (includes session helpers)
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
module.exports = function(deps) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Extract dependencies
|
||||||
|
const { authManager, totpConfig, session, asyncHandler, errorResponse, log, getAppSession, appSessionCache } = deps;
|
||||||
|
|
||||||
// Caddy forward_auth gate: checks TOTP session + injects service credentials
|
// Caddy forward_auth gate: checks TOTP session + injects service credentials
|
||||||
router.get('/auth/gate/:serviceId', ctx.asyncHandler(async (req, res) => {
|
router.get('/auth/gate/:serviceId', asyncHandler(async (req, res) => {
|
||||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
const serviceId = req.params.serviceId;
|
const serviceId = req.params.serviceId;
|
||||||
|
|
||||||
// Check TOTP session first
|
// Check TOTP session first
|
||||||
if (ctx.totpConfig.enabled && ctx.totpConfig.sessionDuration !== 'never') {
|
if (totpConfig.enabled && totpConfig.sessionDuration !== 'never') {
|
||||||
const valid = ctx.session.isValid(req);
|
const valid = session.isValid(req);
|
||||||
if (!valid) return ctx.errorResponse(res, 401, 'Session expired or invalid', { authenticated: false });
|
if (!valid) return errorResponse(res, 401, 'Session expired or invalid', { authenticated: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session valid (or TOTP disabled) - inject credentials if premium SSO is active
|
// Session valid (or TOTP disabled) - inject credentials if premium SSO is active
|
||||||
@@ -73,18 +81,18 @@ module.exports = function(ctx, getAppSession, appSessionCache) {
|
|||||||
const apiKey = arrKey || svcKey;
|
const apiKey = arrKey || svcKey;
|
||||||
if (apiKey) { res.setHeader('X-Api-Key', apiKey); injected = true; }
|
if (apiKey) { res.setHeader('X-Api-Key', apiKey); injected = true; }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Credential error', { serviceId, error: e.message });
|
log.warn('auth', 'Credential error', { serviceId, error: e.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ authenticated: true, credentialsInjected: injected });
|
res.status(200).json({ authenticated: true, credentialsInjected: injected });
|
||||||
}, 'auth-gate'));
|
}, 'auth-gate'));
|
||||||
|
|
||||||
// Return cached app session token for client-side auth (Premium SSO feature)
|
// Return cached app session token for client-side auth (Premium SSO feature)
|
||||||
router.get('/auth/app-token/:serviceId', ctx.licenseManager.requirePremium('sso'), ctx.asyncHandler(async (req, res) => {
|
router.get('/auth/app-token/:serviceId', ctx.licenseManager.requirePremium('sso'), asyncHandler(async (req, res) => {
|
||||||
const { serviceId } = req.params;
|
const { serviceId } = req.params;
|
||||||
|
|
||||||
if (ctx.totpConfig.enabled && ctx.totpConfig.sessionDuration !== 'never') {
|
if (totpConfig.enabled && totpConfig.sessionDuration !== 'never') {
|
||||||
if (!ctx.session.isValid(req)) throw new AuthenticationError('Not authenticated');
|
if (!session.isValid(req)) throw new AuthenticationError('Not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jellyfin/Emby: separate browser-specific token
|
// Jellyfin/Emby: separate browser-specific token
|
||||||
@@ -92,7 +100,7 @@ module.exports = function(ctx, getAppSession, appSessionCache) {
|
|||||||
const browserCacheKey = `${serviceId}_browser`;
|
const browserCacheKey = `${serviceId}_browser`;
|
||||||
const browserCached = appSessionCache.get(browserCacheKey);
|
const browserCached = appSessionCache.get(browserCacheKey);
|
||||||
if (browserCached && browserCached.exp > Date.now()) {
|
if (browserCached && browserCached.exp > Date.now()) {
|
||||||
if (browserCached.failed) return ctx.errorResponse(res, 500, 'Login recently failed');
|
if (browserCached.failed) return errorResponse(res, 500, 'Login recently failed');
|
||||||
if (browserCached.token) {
|
if (browserCached.token) {
|
||||||
const resp = { token: browserCached.token };
|
const resp = { token: browserCached.token };
|
||||||
if (browserCached.tokenData) Object.assign(resp, browserCached.tokenData);
|
if (browserCached.tokenData) Object.assign(resp, browserCached.tokenData);
|
||||||
@@ -118,17 +126,17 @@ module.exports = function(ctx, getAppSession, appSessionCache) {
|
|||||||
appSessionCache.set(browserCacheKey, { token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
appSessionCache.set(browserCacheKey, { token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION });
|
||||||
return res.json({ token: authData.AccessToken, ...tokenData });
|
return res.json({ token: authData.AccessToken, ...tokenData });
|
||||||
}
|
}
|
||||||
return ctx.errorResponse(res, 500, '[DC-501] Authentication failed');
|
return errorResponse(res, 500, '[DC-501] Authentication failed');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'Browser token error', { serviceId, error: e.message });
|
log.warn('auth', 'Browser token error', { serviceId, error: e.message });
|
||||||
return ctx.errorResponse(res, 500, e.message);
|
return errorResponse(res, 500, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cached = appSessionCache.get(serviceId);
|
const cached = appSessionCache.get(serviceId);
|
||||||
if (cached && cached.exp > Date.now()) {
|
if (cached && cached.exp > Date.now()) {
|
||||||
if (cached.failed) return ctx.errorResponse(res, 500, '[DC-501] Login recently failed, retrying in a few minutes');
|
if (cached.failed) return errorResponse(res, 500, '[DC-501] Login recently failed, retrying in a few minutes');
|
||||||
if (cached.token) {
|
if (cached.token) {
|
||||||
const resp = { token: cached.token };
|
const resp = { token: cached.token };
|
||||||
if (cached.tokenData) Object.assign(resp, cached.tokenData);
|
if (cached.tokenData) Object.assign(resp, cached.tokenData);
|
||||||
@@ -172,10 +180,10 @@ module.exports = function(ctx, getAppSession, appSessionCache) {
|
|||||||
return res.json({ cookies: appCookies });
|
return res.json({ cookies: appCookies });
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.errorResponse(res, 500, '[DC-501] Login failed');
|
errorResponse(res, 500, '[DC-501] Login failed');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log.warn('auth', 'App-token error', { error: e.message });
|
log.warn('auth', 'App-token error', { error: e.message });
|
||||||
ctx.errorResponse(res, 500, e.message);
|
errorResponse(res, 500, e.message);
|
||||||
}
|
}
|
||||||
}, 'auth-app-token'));
|
}, 'auth-app-token'));
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,20 @@ const express = require('express');
|
|||||||
const { renewCSRFToken } = require('../../csrf-protection');
|
const { renewCSRFToken } = require('../../csrf-protection');
|
||||||
const { ValidationError, AuthenticationError } = require('../../errors');
|
const { ValidationError, AuthenticationError } = require('../../errors');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ authManager, asyncHandler, errorResponse, log }) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
/**
|
||||||
|
* Auth TOTP routes factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.authManager - Auth manager
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Function} deps.errorResponse - Error response helper
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
|
||||||
// Get current TOTP config (public route)
|
// Get current TOTP config (public route)
|
||||||
router.get('/totp/config', ctx.asyncHandler(async (req, res) => {
|
router.get('/totp/config', asyncHandler(async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
config: {
|
config: {
|
||||||
@@ -18,7 +27,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'totp-config-get'));
|
}, 'totp-config-get'));
|
||||||
|
|
||||||
// Generate new TOTP secret + QR code
|
// Generate new TOTP secret + QR code
|
||||||
router.post('/totp/setup', ctx.asyncHandler(async (req, res) => {
|
router.post('/totp/setup', asyncHandler(async (req, res) => {
|
||||||
const { authenticator } = require('otplib');
|
const { authenticator } = require('otplib');
|
||||||
const QRCode = require('qrcode');
|
const QRCode = require('qrcode');
|
||||||
|
|
||||||
@@ -46,7 +55,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'totp-setup'));
|
}, 'totp-setup'));
|
||||||
|
|
||||||
// Verify first code to confirm setup, then activate TOTP
|
// Verify first code to confirm setup, then activate TOTP
|
||||||
router.post('/totp/verify-setup', ctx.asyncHandler(async (req, res) => {
|
router.post('/totp/verify-setup', asyncHandler(async (req, res) => {
|
||||||
const { authenticator } = require('otplib');
|
const { authenticator } = require('otplib');
|
||||||
const { code } = req.body;
|
const { code } = req.body;
|
||||||
|
|
||||||
@@ -83,7 +92,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'totp-verify-setup'));
|
}, 'totp-verify-setup'));
|
||||||
|
|
||||||
// Login: verify TOTP code and set session cookie
|
// Login: verify TOTP code and set session cookie
|
||||||
router.post('/totp/verify', ctx.asyncHandler(async (req, res) => {
|
router.post('/totp/verify', asyncHandler(async (req, res) => {
|
||||||
const { authenticator } = require('otplib');
|
const { authenticator } = require('otplib');
|
||||||
const { code } = req.body;
|
const { code } = req.body;
|
||||||
|
|
||||||
@@ -105,19 +114,19 @@ module.exports = function(ctx) {
|
|||||||
throw new AuthenticationError('[DC-111] Invalid code');
|
throw new AuthenticationError('[DC-111] Invalid code');
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.log.info('auth', 'TOTP verified, creating session', { ip: ctx.session.getClientIP(req), duration: ctx.totpConfig.sessionDuration });
|
log.info('auth', 'TOTP verified, creating session', { ip: ctx.session.getClientIP(req), duration: ctx.totpConfig.sessionDuration });
|
||||||
ctx.session.create(req, ctx.totpConfig.sessionDuration);
|
ctx.session.create(req, ctx.totpConfig.sessionDuration);
|
||||||
ctx.session.setCookie(res, ctx.totpConfig.sessionDuration);
|
ctx.session.setCookie(res, ctx.totpConfig.sessionDuration);
|
||||||
|
|
||||||
// Rotate CSRF token for the new session
|
// Rotate CSRF token for the new session
|
||||||
const newCsrfToken = renewCSRFToken(res, req.secure || req.protocol === 'https');
|
const newCsrfToken = renewCSRFToken(res, req.secure || req.protocol === 'https');
|
||||||
|
|
||||||
ctx.log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size });
|
log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size });
|
||||||
res.json({ success: true, message: 'Authenticated successfully', sessionDuration: ctx.totpConfig.sessionDuration, csrfToken: newCsrfToken });
|
res.json({ success: true, message: 'Authenticated successfully', sessionDuration: ctx.totpConfig.sessionDuration, csrfToken: newCsrfToken });
|
||||||
}, 'totp-verify'));
|
}, 'totp-verify'));
|
||||||
|
|
||||||
// Check session validity (used by Caddy forward_auth)
|
// Check session validity (used by Caddy forward_auth)
|
||||||
router.get('/totp/check-session', ctx.asyncHandler(async (req, res) => {
|
router.get('/totp/check-session', asyncHandler(async (req, res) => {
|
||||||
// Never cache session checks — stale cached 200s cause auth loops
|
// Never cache session checks — stale cached 200s cause auth loops
|
||||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
@@ -127,7 +136,7 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const valid = ctx.session.isValid(req);
|
const valid = ctx.session.isValid(req);
|
||||||
ctx.log.debug('auth', 'Session check', { ip: ctx.session.getClientIP(req), valid, sessions: ctx.session.ipSessions.size });
|
log.debug('auth', 'Session check', { ip: ctx.session.getClientIP(req), valid, sessions: ctx.session.ipSessions.size });
|
||||||
if (valid) {
|
if (valid) {
|
||||||
return res.status(200).json({ authenticated: true });
|
return res.status(200).json({ authenticated: true });
|
||||||
}
|
}
|
||||||
@@ -136,7 +145,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'totp-check-session'));
|
}, 'totp-check-session'));
|
||||||
|
|
||||||
// Disable TOTP
|
// Disable TOTP
|
||||||
router.post('/totp/disable', ctx.asyncHandler(async (req, res) => {
|
router.post('/totp/disable', asyncHandler(async (req, res) => {
|
||||||
const { code } = req.body;
|
const { code } = req.body;
|
||||||
|
|
||||||
// Always require a valid TOTP code when TOTP is active
|
// Always require a valid TOTP code when TOTP is active
|
||||||
@@ -169,7 +178,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'totp-disable'));
|
}, 'totp-disable'));
|
||||||
|
|
||||||
// Update TOTP settings (session duration)
|
// Update TOTP settings (session duration)
|
||||||
router.post('/totp/config', ctx.asyncHandler(async (req, res) => {
|
router.post('/totp/config', asyncHandler(async (req, res) => {
|
||||||
const { sessionDuration } = req.body;
|
const { sessionDuration } = req.body;
|
||||||
|
|
||||||
if (sessionDuration && !ctx.session.durations.hasOwnProperty(sessionDuration)) {
|
if (sessionDuration && !ctx.session.durations.hasOwnProperty(sessionDuration)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user