conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 }