auth

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

seed-demo.js (9742B)


      1 import crypto from 'node:crypto';
      2 import { loadConfig } from '../src/lib/config.js';
      3 import { hashPassword } from '../src/lib/password.js';
      4 import { hashClientSecret } from '../src/lib/oidc/clients.js';
      5 import {
      6   addClientRealm,
      7   addOidcClientHost,
      8   addRealmEmailDomain,
      9   closeRealms,
     10   createRealm,
     11   findClientRealm,
     12   findRealmBySlug,
     13   getSystemDatabase,
     14   initRealms,
     15   openRealmDatabase,
     16 } from '../src/lib/realm.js';
     17 import {
     18   addUserEmail,
     19   assignHomeRealmForEmail,
     20   assignRole,
     21   createUser,
     22   ensureMembership,
     23   findUserByEmail,
     24   findUserByUsername,
     25   setPasswordCredential,
     26   setSystemAdmin,
     27 } from '../src/lib/users.js';
     28 
     29 const DEMO_PASSWORD = 'demo';
     30 
     31 async function ensureRealm({ slug, displayName, hosts, branding, allowSessionDurationChoice = true }) {
     32   let realm = await findRealmBySlug(slug);
     33   if (!realm) {
     34     realm = await createRealm({
     35       slug,
     36       displayName,
     37       hosts,
     38       branding,
     39       allowSessionDurationChoice,
     40     });
     41   }
     42   return realm;
     43 }
     44 
     45 async function ensureRealmDomain(realmId, domain) {
     46   const systemDb = getSystemDatabase();
     47   const existing = await systemDb.get(
     48     'SELECT realm_id FROM realm_email_domains WHERE domain = {domain}',
     49     { domain }
     50   );
     51   if (existing) return;
     52   await addRealmEmailDomain(realmId, domain, { verified: true });
     53 }
     54 
     55 async function ensureRealmUser(realmSlug, userId, roles = []) {
     56   const realm = await findRealmBySlug(realmSlug);
     57   if (!realm) throw new Error(`unknown realm: ${realmSlug}`);
     58   const db = await openRealmDatabase(realm);
     59   await ensureMembership(db, userId, 'active');
     60   for (const role of roles) {
     61     await assignRole(db, userId, role);
     62   }
     63 }
     64 
     65 async function ensureUser({
     66   username,
     67   email,
     68   displayName,
     69   password = DEMO_PASSWORD,
     70   isSystemAdmin = false,
     71 }) {
     72   const systemDb = getSystemDatabase();
     73   const existing =
     74     (await findUserByEmail(systemDb, email)) || (await findUserByUsername(systemDb, username));
     75   if (existing) {
     76     await setPasswordCredential(systemDb, existing.id, await hashPassword(password));
     77     if (isSystemAdmin) await setSystemAdmin(systemDb, existing.id, true);
     78     return existing.id;
     79   }
     80 
     81   const id = await createUser(systemDb, { username, displayName, isSystemAdmin });
     82   await addUserEmail(systemDb, id, email, { verified: true, primary: true });
     83   await assignHomeRealmForEmail(systemDb, id, email);
     84   await setPasswordCredential(systemDb, id, await hashPassword(password));
     85   return id;
     86 }
     87 
     88 async function ensureProvider(db, { slug, displayName, issuer, clientId, clientSecret }) {
     89   const existing = await db.get('SELECT id FROM identity_providers WHERE slug = {slug}', { slug });
     90   if (existing) return existing.id;
     91   const now = Date.now();
     92   const id = crypto.randomUUID();
     93   await db.run(
     94     `INSERT INTO identity_providers
     95       (id, slug, display_name, issuer, client_id, client_secret, scopes, enabled, created_at, updated_at)
     96      VALUES
     97       ({id}, {slug}, {displayName}, {issuer}, {clientId}, {clientSecret}, 'openid email profile', 1, {createdAt}, {updatedAt})`,
     98     {
     99       id,
    100       slug,
    101       displayName,
    102       issuer,
    103       clientId,
    104       clientSecret: clientSecret || null,
    105       createdAt: now,
    106       updatedAt: now,
    107     }
    108   );
    109   return id;
    110 }
    111 
    112 async function ensureProviderDomain(db, domain, providerId) {
    113   const existing = await db.get('SELECT domain FROM email_domain_providers WHERE domain = {domain}', {
    114     domain,
    115   });
    116   if (existing) return;
    117   await db.run(
    118     'INSERT INTO email_domain_providers (domain, provider_id, created_at) VALUES ({domain}, {providerId}, {createdAt})',
    119     { domain, providerId, createdAt: Date.now() }
    120   );
    121 }
    122 
    123 async function ensureClient(
    124   realm,
    125   db,
    126   {
    127     clientId,
    128     name,
    129     accessMode,
    130     roles = [],
    131     redirectUris = [],
    132     postLogoutRedirectUris = [],
    133     tokenEndpointAuthMethod = 'none',
    134     clientSecret = null,
    135     branding = {},
    136     requirePkce = true,
    137   }
    138 ) {
    139   const mapping = await findClientRealm(clientId);
    140   if (mapping && mapping.realm_id !== realm.id) {
    141     throw new Error(`client id is already assigned: ${clientId}`);
    142   }
    143 
    144   let client = await db.get('SELECT id FROM oidc_clients WHERE client_id = {clientId}', { clientId });
    145   if (!client) {
    146     const now = Date.now();
    147     client = { id: crypto.randomUUID() };
    148     await db.run(
    149       `INSERT INTO oidc_clients
    150         (id, client_id, name, redirect_uris, scopes, access_mode, enabled, branding,
    151          client_secret_hash, token_endpoint_auth_method, require_pkce,
    152          post_logout_redirect_uris, created_at, updated_at)
    153        VALUES
    154         ({id}, {clientId}, {name}, {redirectUris}, 'openid profile email', {accessMode}, 1, {branding},
    155          {clientSecretHash}, {tokenEndpointAuthMethod}, {requirePkce},
    156          {postLogoutRedirectUris}, {createdAt}, {updatedAt})`,
    157       {
    158         id: client.id,
    159         clientId,
    160         name,
    161         redirectUris: JSON.stringify(redirectUris),
    162         accessMode,
    163         branding: JSON.stringify(branding),
    164         clientSecretHash: clientSecret ? hashClientSecret(clientSecret) : null,
    165         tokenEndpointAuthMethod,
    166         requirePkce: requirePkce ? 1 : 0,
    167         postLogoutRedirectUris: JSON.stringify(postLogoutRedirectUris),
    168         createdAt: now,
    169         updatedAt: now,
    170       }
    171     );
    172   }
    173 
    174   await addClientRealm(clientId, realm.id);
    175 
    176   for (const slug of roles) {
    177     const role = await db.get('SELECT id FROM roles WHERE slug = {slug}', { slug });
    178     if (!role) continue;
    179     const existing = await db.get(
    180       'SELECT client_id FROM oidc_client_roles WHERE client_id = {clientId} AND role_id = {roleId}',
    181       { clientId: client.id, roleId: role.id }
    182     );
    183     if (existing) continue;
    184     await db.run(
    185       'INSERT INTO oidc_client_roles (client_id, role_id) VALUES ({clientId}, {roleId})',
    186       { clientId: client.id, roleId: role.id }
    187     );
    188   }
    189 }
    190 
    191 const cfg = loadConfig();
    192 const publicUrl = new URL(cfg.server.public_url);
    193 const portSuffix = publicUrl.port ? `:${publicUrl.port}` : '';
    194 const clientOrigin = (host) => `${publicUrl.protocol}//${host}${portSuffix}`;
    195 
    196 await initRealms(cfg);
    197 
    198 const acme = await ensureRealm({
    199   slug: 'acme',
    200   displayName: 'Acme',
    201   hosts: ['acme.localhost'],
    202   branding: { primary_color: '#25A' },
    203 });
    204 const globex = await ensureRealm({
    205   slug: 'globex',
    206   displayName: 'Globex',
    207   hosts: ['globex.localhost'],
    208   branding: { primary_color: '#25A' },
    209 });
    210 
    211 await ensureRealmDomain(acme.id, 'acme.test');
    212 await ensureRealmDomain(acme.id, 'sso.acme.test');
    213 await ensureRealmDomain(globex.id, 'globex.test');
    214 
    215 const alice = await ensureUser({
    216   username: 'alice',
    217   email: 'alice@acme.test',
    218   displayName: 'Alice',
    219 });
    220 const acmeAdmin = await ensureUser({
    221   username: 'acme-it',
    222   email: 'it@acme.test',
    223   displayName: 'Acme IT',
    224 });
    225 const guest = await ensureUser({
    226   username: 'guest',
    227   email: 'guest@personal.test',
    228   displayName: 'Guest User',
    229 });
    230 const contractor = await ensureUser({
    231   username: 'contractor',
    232   email: 'contractor@personal.test',
    233   displayName: 'Contractor',
    234 });
    235 const systemAdmin = await ensureUser({
    236   username: 'admin',
    237   email: 'admin@system.test',
    238   displayName: 'System Admin',
    239   isSystemAdmin: true,
    240 });
    241 
    242 await ensureRealmUser('acme', alice);
    243 await ensureRealmUser('globex', alice);
    244 await ensureRealmUser('acme', acmeAdmin, ['realm-admin']);
    245 await ensureRealmUser('acme', guest);
    246 await ensureRealmUser('globex', contractor, ['realm-admin']);
    247 
    248 const acmeDb = await openRealmDatabase(acme);
    249 const providerId = await ensureProvider(acmeDb, {
    250   slug: 'example-sso',
    251   displayName: 'Example SSO',
    252   issuer: 'https://idp.example.com',
    253   clientId: 'demo',
    254   clientSecret: 'demo-secret',
    255 });
    256 await ensureProviderDomain(acmeDb, 'sso.acme.test', providerId);
    257 
    258 await ensureClient(acme, acmeDb, {
    259   clientId: 'internal-app',
    260   name: 'Internal App',
    261   accessMode: 'internal',
    262   redirectUris: [`${clientOrigin('app.acme.localhost')}/callback`],
    263   postLogoutRedirectUris: [`${clientOrigin('app.acme.localhost')}/`],
    264 });
    265 await ensureClient(acme, acmeDb, {
    266   clientId: 'acme-members-app',
    267   name: 'Acme Members App',
    268   accessMode: 'members',
    269   redirectUris: [`${clientOrigin('members.localhost')}/callback`],
    270 });
    271 await ensureClient(acme, acmeDb, {
    272   clientId: 'acme-public-app',
    273   name: 'Acme Public App',
    274   accessMode: 'public',
    275   redirectUris: [`${clientOrigin('public.localhost')}/callback`],
    276 });
    277 await ensureClient(acme, acmeDb, {
    278   clientId: 'confidential-app',
    279   name: 'Confidential App',
    280   accessMode: 'members',
    281   redirectUris: [`${clientOrigin('confidential.localhost')}/callback`],
    282   tokenEndpointAuthMethod: 'client_secret_basic',
    283   clientSecret: 'demo-secret',
    284 });
    285 
    286 const globexDb = await openRealmDatabase(globex);
    287 await ensureClient(globex, globexDb, {
    288   clientId: 'globex-members-app',
    289   name: 'Globex Members App',
    290   accessMode: 'members',
    291   redirectUris: [`${clientOrigin('members.localhost')}/callback`],
    292 });
    293 await ensureClient(globex, globexDb, {
    294   clientId: 'globex-public-app',
    295   name: 'Globex Public App',
    296   accessMode: 'public',
    297   redirectUris: [`${clientOrigin('public.localhost')}/callback`],
    298 });
    299 
    300 await addOidcClientHost('internal-app', 'app.acme.localhost');
    301 
    302 console.log('seeded realms acme and globex');
    303 console.log('password for all demo users: demo');
    304 console.log('home users: alice@acme.test, it@acme.test, admin@system.test (system admin)');
    305 console.log('guest users: guest@personal.test (acme), contractor@personal.test (globex realm-admin)');
    306 console.log('upstream mapping: sso.acme.test -> Example SSO');
    307 console.log(
    308   'clients: internal-app, acme-members-app, acme-public-app, confidential-app, ' +
    309     'globex-members-app, globex-public-app'
    310 );
    311 console.log('client host: app.acme.localhost -> acme/internal-app');
    312 console.log('virtual clients: admin, account');
    313 
    314 await closeRealms();