token.js (2124B)
1 // src/lib/auth/token.js - access tokens and authorization codes 2 // 3 // Compact HS256 tokens in the JWT shape, signed with node:crypto rather than 4 // a library. The jose dependency exists for the external provider, where the 5 // verification rules genuinely are complicated. 6 // 7 // Both kinds are signed with the same secret and are otherwise the same 8 // shape, so _tu separates them and is checked on every path. Without it an 9 // authorization code would pass wherever an access token is expected. 10 11 import crypto from 'node:crypto'; 12 13 const HEADER = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); 14 15 function sign(secret, data) { 16 return crypto.createHmac('sha256', secret).update(data).digest('base64url'); 17 } 18 19 export function signToken(secret, { use, iss, aud, sub, ttl, now = Date.now(), ...rest }) { 20 const issued = Math.floor(now / 1000); 21 const payload = Buffer.from(JSON.stringify({ 22 ...rest, 23 iss, 24 aud, 25 sub, 26 _tu: use, 27 iat: issued, 28 exp: issued + ttl, 29 })).toString('base64url'); 30 31 const body = `${HEADER}.${payload}`; 32 return `${body}.${sign(secret, body)}`; 33 } 34 35 // Returns the claims, or null. Never throws, so a malformed token from a 36 // browser is an ordinary 401 rather than a 500. 37 export function verifyToken(secret, token, { use, iss, aud, now = Date.now() }) { 38 if (typeof token !== 'string') return null; 39 40 const parts = token.split('.'); 41 if (parts.length !== 3) return null; 42 const [header, payload, signature] = parts; 43 44 const expected = sign(secret, `${header}.${payload}`); 45 const given = Buffer.from(signature); 46 const want = Buffer.from(expected); 47 if (given.length !== want.length || !crypto.timingSafeEqual(given, want)) return null; 48 49 let claims; 50 try { 51 claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); 52 } catch { 53 return null; 54 } 55 56 if (typeof claims !== 'object' || claims === null) return null; 57 if (claims._tu !== use) return null; 58 if (claims.iss !== iss || claims.aud !== aud) return null; 59 if (typeof claims.exp !== 'number' || claims.exp * 1000 <= now) return null; 60 61 return claims; 62 }