state.js (1986B)
1 // src/lib/auth/state.js - the OIDC login state 2 // 3 // A short signed value that travels to the provider as the state parameter 4 // and comes back on the callback. It carries when the login started and, 5 // when there is somewhere to return to, that path. It is verified on the 6 // way back, so nothing has to be stored between the two requests. 7 // 8 // body = base64url(JSON.stringify({ iat, url })) 9 // sign = base64url(hmacSha256(secret, body)) 10 // state = body + "." + sign 11 12 import crypto from 'node:crypto'; 13 14 const MAX_AGE = 300; 15 16 function sign(secret, body) { 17 return crypto.createHmac('sha256', secret).update(body).digest('base64url'); 18 } 19 20 export function createState(secret, url, { now = Date.now() } = {}) { 21 const claims = { iat: Math.floor(now / 1000) }; 22 if (url) claims.url = url; 23 const body = Buffer.from(JSON.stringify(claims)).toString('base64url'); 24 return `${body}.${sign(secret, body)}`; 25 } 26 27 // Returns { iat, url }, or null when the value is malformed, forged or 28 // older than five minutes. A missing url means the root. 29 export function verifyState(secret, state, { now = Date.now() } = {}) { 30 if (typeof state !== 'string') return null; 31 32 const [body, given, ...rest] = state.split('.'); 33 if (!body || !given || rest.length > 0) return null; 34 35 const expected = sign(secret, body); 36 const a = Buffer.from(given); 37 const b = Buffer.from(expected); 38 if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null; 39 40 let claims; 41 try { 42 claims = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')); 43 } catch { 44 return null; 45 } 46 47 const seconds = Math.floor(now / 1000); 48 if (!Number.isInteger(claims?.iat) || claims.iat > seconds || claims.iat < seconds - MAX_AGE) return null; 49 50 const url = claims.url === undefined 51 ? '/' 52 : typeof claims.url === 'string' && claims.url.startsWith('/') && !claims.url.startsWith('//') 53 ? claims.url 54 : null; 55 if (url === null) return null; 56 57 return { iat: claims.iat, url }; 58 }