sc-idp/lib/clients.js
2026-06-01 16:40:54 -05:00

95 lines
3.1 KiB
JavaScript

// 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
};