auth

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

clients.js (3489B)


      1 import crypto from 'node:crypto';
      2 
      3 const VIRTUAL_CLIENTS = {
      4   admin: {
      5     id: 'virtual:admin',
      6     client_id: 'admin',
      7     name: 'Admin',
      8     access_mode: 'admin',
      9     enabled: 1,
     10     require_pkce: 1,
     11     token_endpoint_auth_method: 'none',
     12     redirect_uris: [],
     13     post_logout_redirect_uris: [],
     14     branding: {},
     15     required_roles: [],
     16     virtual: true,
     17   },
     18   account: {
     19     id: 'virtual:account',
     20     client_id: 'account',
     21     name: 'Account',
     22     access_mode: 'public',
     23     enabled: 1,
     24     require_pkce: 1,
     25     token_endpoint_auth_method: 'none',
     26     redirect_uris: [],
     27     post_logout_redirect_uris: [],
     28     branding: {},
     29     required_roles: [],
     30     virtual: true,
     31   },
     32 };
     33 
     34 function parseJson(raw, fallback) {
     35   try {
     36     return JSON.parse(raw || 'null') ?? fallback;
     37   } catch {
     38     return fallback;
     39   }
     40 }
     41 
     42 export function hashClientSecret(secret) {
     43   return crypto.createHash('sha256').update(secret).digest('hex');
     44 }
     45 
     46 export function isVirtualClient(clientId) {
     47   return Object.hasOwn(VIRTUAL_CLIENTS, clientId);
     48 }
     49 
     50 export async function getClient(db, clientId) {
     51   const virtual = VIRTUAL_CLIENTS[clientId];
     52   if (virtual) return { ...virtual };
     53 
     54   const row = await db.get(
     55     `SELECT id, client_id, name, redirect_uris, scopes, access_mode, enabled,
     56             client_secret_hash, token_endpoint_auth_method, require_pkce,
     57             post_logout_redirect_uris, branding
     58        FROM oidc_clients
     59       WHERE client_id = {clientId}`,
     60     { clientId }
     61   );
     62   if (!row) return null;
     63 
     64   const roles = await db.all(
     65     `SELECT r.slug
     66        FROM oidc_client_roles cr
     67        JOIN roles r ON r.id = cr.role_id
     68       WHERE cr.client_id = {id}`,
     69     { id: row.id }
     70   );
     71 
     72   return {
     73     ...row,
     74     enabled: Boolean(row.enabled),
     75     require_pkce: Boolean(row.require_pkce),
     76     redirect_uris: parseJson(row.redirect_uris, []),
     77     post_logout_redirect_uris: parseJson(row.post_logout_redirect_uris, []),
     78     branding: parseJson(row.branding, {}),
     79     required_roles: roles.map((role) => role.slug),
     80     virtual: false,
     81   };
     82 }
     83 
     84 export function redirectUriAllowed(client, redirectUri, virtualCallbacks = []) {
     85   if (client.virtual) return virtualCallbacks.includes(redirectUri);
     86   return client.redirect_uris.includes(redirectUri);
     87 }
     88 
     89 export function postLogoutRedirectAllowed(client, redirectUri, virtualCallbacks = []) {
     90   if (!redirectUri) return true;
     91   if (client.virtual) return virtualCallbacks.includes(redirectUri);
     92   return client.post_logout_redirect_uris.includes(redirectUri);
     93 }
     94 
     95 export function authenticateClient(req, client, body = {}) {
     96   const method = client.token_endpoint_auth_method || 'none';
     97 
     98   if (method === 'none') {
     99     return !body.client_id || body.client_id === client.client_id;
    100   }
    101 
    102   if (method !== 'client_secret_basic' || !client.client_secret_hash) {
    103     return false;
    104   }
    105 
    106   const header = String(req.headers.authorization || '');
    107   if (!header.startsWith('Basic ')) return false;
    108 
    109   let decoded;
    110   try {
    111     decoded = Buffer.from(header.slice(6), 'base64').toString('utf8');
    112   } catch {
    113     return false;
    114   }
    115 
    116   const colon = decoded.indexOf(':');
    117   if (colon === -1) return false;
    118   const clientId = decoded.slice(0, colon);
    119   const secret = decoded.slice(colon + 1);
    120   if (clientId !== client.client_id) return false;
    121 
    122   const expected = Buffer.from(client.client_secret_hash, 'hex');
    123   const actual = Buffer.from(hashClientSecret(secret), 'hex');
    124   return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
    125 }