index.js (3104B)
1 // src/lib/auth/index.js - request authentication 2 // 3 // One provider at a time: the external one when auth.oidc.discovery_url is 4 // configured, the built-in one otherwise. Access tokens arrive as bearer 5 // credentials and nothing is stored between requests. 6 7 import { signToken, verifyToken } from './token.js'; 8 import { createOidc } from './oidc.js'; 9 10 export { signToken, verifyToken } from './token.js'; 11 export { hashPassword, verifyPassword, generatePassword, validatePassword } from './password.js'; 12 13 // The built-in provider has no registration step, so the app is named here. 14 export const LOCAL_CLIENT_ID = 'conductor'; 15 16 export function createAuth({ cfg, users, logger = console }) { 17 const oidc = cfg.auth.mode === 'oidc' ? createOidc(cfg) : null; 18 const localLogin = !oidc; 19 const issuer = cfg.server.public_url.replace(/\/+$/, ''); 20 const clientId = oidc ? cfg.auth.oidc.client_id : LOCAL_CLIENT_ID; 21 22 // A worker token is 64 hex characters and never parses as a JWT, so the 23 // two meanings of this header cannot be confused. 24 function bearerFrom(req) { 25 const header = req.headers.authorization || ''; 26 return header.startsWith('Bearer ') ? header.slice(7).trim() : null; 27 } 28 29 async function verifyAccessToken(token) { 30 if (oidc) { 31 const profile = await oidc.verify(token); 32 33 // The provider vouched for them, but they still need a local row to 34 // own anything, since projects and worker tokens reference users(id). 35 const account = await users.upsertExternal({ 36 externalId: `${profile.issuer}|${profile.subject}`, 37 username: profile.username, 38 role: profile.role, 39 }); 40 if (account.disabled === 1) return null; 41 return { id: account.id, username: account.username, role: account.role, source: 'oidc' }; 42 } 43 44 const claims = verifyToken(cfg.auth.session_secret, token, { 45 use: 'access', 46 iss: issuer, 47 aud: clientId, 48 }); 49 if (!claims) return null; 50 51 // Re-read the account, so disabling a user takes effect immediately 52 // rather than when their token happens to expire. 53 const user = await users.get(claims.sub); 54 if (!user || user.disabled === 1) return null; 55 56 return { id: user.id, username: user.username, role: user.role, source: 'local' }; 57 } 58 59 return { 60 mode: cfg.auth.mode, 61 localLogin, 62 oidc, 63 issuer, 64 clientId, 65 66 accessToken(user, { now = Date.now() } = {}) { 67 return signToken(cfg.auth.session_secret, { 68 use: 'access', 69 iss: issuer, 70 aud: clientId, 71 sub: user.id, 72 name: user.username, 73 role: user.role, 74 ttl: cfg.auth.access_ttl, 75 now, 76 }); 77 }, 78 79 verifyAccessToken, 80 81 // Resolves a request to a user, or null. Never throws: an unparseable 82 // credential is simply not authenticated. 83 async identify(req) { 84 const token = bearerFrom(req); 85 if (!token) return null; 86 try { 87 return await verifyAccessToken(token); 88 } catch (e) { 89 logger.debug?.(`access token rejected: ${e.message}`); 90 return null; 91 } 92 }, 93 }; 94 }