// Relying-party (OAuth/OIDC client) registry. Backed by _idp_clients and exposed // to oidc-provider via the Client model in the storage adapter (so new clients // are picked up without rebuilding the provider). Confidential clients get a // random secret, sealed at rest; public clients (token_auth_method='none') use // PKCE and have no secret. const nodeCrypto = require("crypto"); const db = require("@saltcorn/data/db"); const idpCrypto = require("./crypto"); const { TABLE_CLIENTS } = require("./constants"); const SECRET_BYTES = 32; const AUTH_NONE = "none"; const listClients = async () => { return await db.select(TABLE_CLIENTS, {}, { orderBy: "client_id" }); }; const getClient = async (clientId) => { return await db.selectMaybeOne(TABLE_CLIENTS, { client_id: clientId }); }; const deleteClient = async (clientId) => { await db.deleteWhere(TABLE_CLIENTS, { client_id: clientId }); }; // Creates a client. Returns { client_id, secret } where secret is the plaintext // to display ONCE (null for public clients). Throws if the client_id exists. const createClient = async (opts) => { const authMethod = opts.authMethod || AUTH_NONE; let cipher = null; let iv = null; let tag = null; let secret = null; if (authMethod !== AUTH_NONE) { secret = nodeCrypto.randomBytes(SECRET_BYTES).toString("base64url"); const sealed = idpCrypto.sealText(secret); cipher = sealed.ciphertext; iv = sealed.iv; tag = sealed.tag; } await db.insert(TABLE_CLIENTS, { client_id: opts.clientId, label: opts.label || null, token_auth_method: authMethod, redirect_uris: JSON.stringify(opts.redirectUris || []), grant_types: JSON.stringify(["authorization_code"]), response_types: JSON.stringify(["code"]), scope: opts.scope || null, secret_ciphertext: cipher, secret_iv: iv, secret_tag: tag, created_at: new Date().toISOString() }, { noid: true }); return { client_id: opts.clientId, secret: secret }; }; // Render a stored client row into the metadata object oidc-provider expects. // The (unsealed) secret is only included for confidential clients. const toOidcMetadata = (row) => { const meta = { client_id: row.client_id, redirect_uris: JSON.parse(row.redirect_uris), grant_types: JSON.parse(row.grant_types), response_types: JSON.parse(row.response_types), token_endpoint_auth_method: row.token_auth_method }; if (row.scope) { meta.scope = row.scope; } if (row.token_auth_method !== AUTH_NONE && row.secret_ciphertext) { meta.client_secret = idpCrypto.openText({ ciphertext: row.secret_ciphertext, iv: row.secret_iv, tag: row.secret_tag }).toString("utf8"); } return meta; }; module.exports = { listClients, getClient, deleteClient, createClient, toOidcMetadata };