auth

Main authentication module for my projects
git clone git://git.finwo.net/app/auth
Log | Files | Refs

requests.js (6089B)


      1 import crypto from 'node:crypto';
      2 import { parseCookie, stringifySetCookie } from 'cookie';
      3 
      4 const AUTH_REQUEST_TTL = 10 * 60;
      5 const CODE_TTL = 60;
      6 
      7 export function authRequestCookieName(realm) {
      8   return `auth_request_${realm.slug}`;
      9 }
     10 
     11 function secureRequest(req) {
     12   if (req.socket?.encrypted) return true;
     13   const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
     14   return proto === 'https';
     15 }
     16 
     17 function appendSetCookie(res, value) {
     18   const existing = res.getHeader('Set-Cookie');
     19   if (!existing) {
     20     res.setHeader('Set-Cookie', value);
     21     return;
     22   }
     23   const values = Array.isArray(existing) ? existing : [existing];
     24   res.setHeader('Set-Cookie', [...values, value]);
     25 }
     26 
     27 export function setAuthRequestCookie(res, req, realm, id, ttl = AUTH_REQUEST_TTL) {
     28   appendSetCookie(
     29     res,
     30     stringifySetCookie({
     31       name: authRequestCookieName(realm),
     32       value: id,
     33       httpOnly: true,
     34       sameSite: 'lax',
     35       path: '/',
     36       maxAge: ttl,
     37       secure: secureRequest(req),
     38     })
     39   );
     40 }
     41 
     42 export function clearAuthRequestCookie(res, req, realm) {
     43   appendSetCookie(
     44     res,
     45     stringifySetCookie({
     46       name: authRequestCookieName(realm),
     47       value: '',
     48       httpOnly: true,
     49       sameSite: 'lax',
     50       path: '/',
     51       maxAge: 0,
     52       secure: secureRequest(req),
     53     })
     54   );
     55 }
     56 
     57 export function readAuthRequestId(req, realm) {
     58   const cookies = parseCookie(req.headers.cookie || '');
     59   return cookies[authRequestCookieName(realm)] || null;
     60 }
     61 
     62 export async function purgeExpired(db, now = Date.now()) {
     63   await db.run('DELETE FROM auth_requests WHERE expires_at <= {now}', { now });
     64   await db.run('DELETE FROM authorization_codes WHERE expires_at <= {now}', { now });
     65 }
     66 
     67 export async function createAuthRequest(db, data, now = Date.now()) {
     68   const id = crypto.randomBytes(24).toString('base64url');
     69   await db.run(
     70     `INSERT INTO auth_requests
     71       (id, client_id, redirect_uri, scope, state, nonce, code_challenge,
     72        code_challenge_method, created_at, expires_at)
     73      VALUES
     74       ({id}, {clientId}, {redirectUri}, {scope}, {state}, {nonce}, {codeChallenge},
     75        {codeChallengeMethod}, {createdAt}, {expiresAt})`,
     76     {
     77       id,
     78       clientId: data.clientId,
     79       redirectUri: data.redirectUri,
     80       scope: data.scope,
     81       state: data.state || null,
     82       nonce: data.nonce || null,
     83       codeChallenge: data.codeChallenge || null,
     84       codeChallengeMethod: data.codeChallengeMethod || null,
     85       createdAt: now,
     86       expiresAt: now + AUTH_REQUEST_TTL * 1000,
     87     }
     88   );
     89   return id;
     90 }
     91 
     92 export async function getAuthRequest(db, id, now = Date.now()) {
     93   if (!id) return null;
     94   const request = await db.get(
     95     `SELECT id, client_id, redirect_uri, scope, state, nonce, code_challenge,
     96             code_challenge_method, session_id, user_id, auth_time,
     97             created_at, expires_at, completed_at
     98        FROM auth_requests
     99       WHERE id = {id}`,
    100     { id }
    101   );
    102   if (!request) return null;
    103   if (request.expires_at <= now) {
    104     await db.run('DELETE FROM auth_requests WHERE id = {id}', { id });
    105     return null;
    106   }
    107   return request;
    108 }
    109 
    110 export async function selectAuthRequestAccount(
    111   db,
    112   id,
    113   { userId, sessionId, authTime }
    114 ) {
    115   if (!id) return;
    116   await db.run(
    117     `UPDATE auth_requests
    118         SET user_id = {userId}, session_id = {sessionId}, auth_time = {authTime}
    119       WHERE id = {id} AND completed_at IS NULL`,
    120     { id, userId, sessionId, authTime }
    121   );
    122 }
    123 
    124 export async function clearAuthRequestSelection(db, id) {
    125   if (!id) return;
    126   await db.run(
    127     `UPDATE auth_requests
    128         SET user_id = NULL, session_id = NULL, auth_time = NULL
    129       WHERE id = {id} AND completed_at IS NULL`,
    130     { id }
    131   );
    132 }
    133 
    134 export async function completeAuthRequest(db, id, { userId, sessionId, authTime }, now = Date.now()) {
    135   await db.run(
    136     `UPDATE auth_requests
    137         SET user_id = {userId}, session_id = {sessionId}, auth_time = {authTime}, completed_at = {now}
    138       WHERE id = {id}`,
    139     { id, userId, sessionId, authTime, now }
    140   );
    141 }
    142 
    143 export async function deleteAuthRequest(db, id) {
    144   if (!id) return;
    145   await db.run('DELETE FROM auth_requests WHERE id = {id}', { id });
    146 }
    147 
    148 export async function createAuthorizationCode(db, request, now = Date.now()) {
    149   const code = crypto.randomBytes(32).toString('base64url');
    150   const id = crypto.createHash('sha256').update(code).digest('hex');
    151 
    152   await db.run(
    153     `INSERT INTO authorization_codes
    154       (id, auth_request_id, client_id, user_id, redirect_uri, scope, nonce,
    155        code_challenge, code_challenge_method, session_id, auth_time,
    156        created_at, expires_at)
    157      VALUES
    158       ({id}, {authRequestId}, {clientId}, {userId}, {redirectUri}, {scope}, {nonce},
    159        {codeChallenge}, {codeChallengeMethod}, {sessionId}, {authTime},
    160        {createdAt}, {expiresAt})`,
    161     {
    162       id,
    163       authRequestId: request.id,
    164       clientId: request.client_id,
    165       userId: request.user_id,
    166       redirectUri: request.redirect_uri,
    167       scope: request.scope,
    168       nonce: request.nonce,
    169       codeChallenge: request.code_challenge,
    170       codeChallengeMethod: request.code_challenge_method,
    171       sessionId: request.session_id,
    172       authTime: request.auth_time,
    173       createdAt: now,
    174       expiresAt: now + CODE_TTL * 1000,
    175     }
    176   );
    177 
    178   return code;
    179 }
    180 
    181 export async function consumeAuthorizationCode(db, code, clientId, now = Date.now()) {
    182   if (!code) return null;
    183   const id = crypto.createHash('sha256').update(String(code)).digest('hex');
    184 
    185   return db.transaction(async (tx) => {
    186     const row = await tx.get(
    187       `SELECT id, auth_request_id, client_id, user_id, redirect_uri, scope, nonce,
    188               code_challenge, code_challenge_method, session_id, auth_time,
    189               expires_at, consumed_at
    190          FROM authorization_codes
    191         WHERE id = {id}`,
    192       { id }
    193     );
    194     if (!row || row.consumed_at || row.expires_at <= now || row.client_id !== clientId) {
    195       return null;
    196     }
    197     await tx.run('UPDATE authorization_codes SET consumed_at = {now} WHERE id = {id}', { now, id });
    198     return row;
    199   });
    200 }