UNPKG

oidc-provider

Version:

OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect

288 lines (250 loc) 7.77 kB
import { strict as assert } from 'node:assert'; import * as crypto from 'node:crypto'; import { DEV_KEYSTORE } from '../consts/index.js'; import * as attention from './attention.js'; import instance from './weak_cache.js'; import KeyStore, { ExternalSigningKey } from './keystore.js'; const calculateKid = (jwk) => { let components; switch (jwk.kty) { case 'RSA': components = { e: jwk.e, kty: 'RSA', n: jwk.n, }; break; case 'EC': components = { crv: jwk.crv, kty: 'EC', x: jwk.x, y: jwk.y, }; break; case 'OKP': components = { crv: jwk.crv, kty: 'OKP', x: jwk.x, }; break; default: return undefined; } return crypto.hash('sha256', JSON.stringify(components), 'base64url'); }; const KEY_TYPES = new Set(['RSA', 'EC', 'OKP']); const jwkSignatureAlgorithms = (jwk) => { if (jwk.use !== 'sig' && jwk.use !== undefined) { return []; } let available; switch (jwk.kty) { case 'RSA': available = ['PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512']; break; case 'EC': switch (jwk.crv) { case 'P-256': available = ['ES256']; break; case 'P-384': available = ['ES384']; break; case 'P-521': available = ['ES512']; break; default: } break; case 'OKP': switch (jwk.crv) { case 'Ed25519': available = ['EdDSA', 'Ed25519']; break; default: } break; default: } if (jwk.alg) { if (available && available.includes(jwk.alg)) { return [jwk.alg]; } return []; } return available || []; }; const jwkEncryptionAlgorithms = (jwk) => { if (jwk.use !== 'enc' && jwk.use !== undefined) { return []; } let available; switch (jwk.kty) { case 'RSA': available = ['RSA-OAEP', 'RSA-OAEP-256', 'RSA-OAEP-384', 'RSA-OAEP-512']; break; case 'EC': switch (jwk.crv) { case 'P-256': case 'P-384': case 'P-521': available = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']; break; default: } break; case 'OKP': switch (jwk.crv) { case 'X25519': available = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']; break; default: } break; default: } if (jwk.alg) { if (available && available.includes(jwk.alg)) { return [jwk.alg]; } return []; } return available || []; }; function checkString(value, property, i) { assert(typeof value === 'string' && value, `jwks.keys[${i}].${property} configuration must be a non-empty string`); } function isExternal(key) { return key instanceof ExternalSigningKey; } function registerKey(input, i, keystore, kids) { const { configuration, features } = instance(this); let key; if (isExternal(input)) { assert(features.externalSigningSupport.enabled, 'features.externalSigningSupport must be enabled for ExternalSigningKey support'); key = input; } else { key = structuredClone(input); } assert(KEY_TYPES.has(key.kty), `only RSA, EC, or OKP keys should be part of jwks configuration (index ${i})`); key.kid ??= calculateKid(key); checkString(key.kid, 'kid', i); assert(!kids.has(key.kid), 'jwks.keys configuration must not contain duplicate "kid" values'); kids.add(key.kid); switch (key.kty) { case 'OKP': checkString(key.crv, 'crv', i); checkString(key.x, 'x', i); if (!(key instanceof ExternalSigningKey)) { checkString(key.d, 'd', i); } break; case 'EC': checkString(key.crv, 'crv', i); checkString(key.x, 'x', i); checkString(key.y, 'y', i); if (!(key instanceof ExternalSigningKey)) { checkString(key.d, 'd', i); } break; case 'RSA': checkString(key.e, 'e', i); checkString(key.n, 'n', i); if (!(key instanceof ExternalSigningKey)) { checkString(key.d, 'd', i); checkString(key.p, 'p', i); checkString(key.q, 'q', i); checkString(key.dp, 'dp', i); checkString(key.dq, 'dq', i); checkString(key.qi, 'qi', i); } break; default: throw new Error('unreachable'); } if (key instanceof ExternalSigningKey) { assert(key.use === 'sig', `jwks.keys[${i}] configuration "use" must be "sig"`); } if (key.key_ops !== undefined) { assert(Array.isArray(key.key_ops) && key.key_ops.length && key.key_ops.every((x) => typeof x === 'string' && x), `jwks.keys[${i}].key_ops configuration must be an array of strings`); } if (key.x5c !== undefined) { assert(Array.isArray(key.x5c) && key.x5c.length && key.x5c.every((x) => typeof x === 'string' && x), `jwks.keys[${i}].x5c configuration must be an array of strings`); } let encryptionAlgs; if (features.encryption.enabled) { encryptionAlgs = jwkEncryptionAlgorithms(key); [ // 'idTokenEncryptionAlgValues', 'requestObjectEncryptionAlgValues', // 'userinfoEncryptionAlgValues', ].forEach((prop) => { configuration[prop] = [...new Set([...configuration[prop], ...encryptionAlgs])] .filter((v) => configuration.enabledJWA[prop].includes(v)); }); } const signingAlgs = jwkSignatureAlgorithms(key); [ 'idTokenSigningAlgValues', // 'requestObjectSigningAlgValues' uses client's keystore // 'tokenEndpointAuthSigningAlgValues' uses client's keystore 'userinfoSigningAlgValues', 'introspectionSigningAlgValues', 'authorizationSigningAlgValues', ].forEach((prop) => { configuration[prop] = [...new Set([...configuration[prop], ...signingAlgs])] .filter((v) => configuration.enabledJWA[prop].includes(v)); }); const combined = signingAlgs.concat(encryptionAlgs).filter(Boolean); /* eslint-disable no-param-reassign */ if (combined.length === 1 && key.alg !== combined[0]) { [key.alg] = combined; } if (isExternal(key) && combined.length > 1) { checkString(key.alg, 'alg', i); } if (encryptionAlgs?.length && !signingAlgs.length && key.use !== 'enc') { key.use = 'enc'; } else if (signingAlgs.length && !encryptionAlgs?.length && key.use !== 'sig') { key.use = 'sig'; } if (!Array.isArray(key.x5c) || !key.x5c.length) { delete key.x5c; } assert(combined.length, `jwks.keys[${i}] is of no use given the other configuration, remove it`); keystore.add(key); /* eslint-enable */ } export default function initialize(jwks) { if (jwks === undefined) { // eslint-disable-next-line no-param-reassign jwks = structuredClone(DEV_KEYSTORE); /* eslint-disable no-multi-str */ attention.warn('a quick start development-only signing keys are used, you are expected to \ provide your own in the configuration "jwks" property'); /* eslint-enable */ } const keystore = new KeyStore(); const kids = new Set(); try { if (!Array.isArray(jwks.keys)) { throw new Error(); } // eslint-disable-next-line no-plusplus for (let i = 0; i < jwks.keys.length; i++) { registerKey.call(this, jwks.keys[i], i, keystore, kids); } } catch (err) { throw new Error(err.message || 'keystore must be a JSON Web Key Set formatted object', { cause: err }); } instance(this).keystore = keystore; const keys = [...keystore].map((key) => ({ kty: key.kty, use: key.use, key_ops: key.key_ops ? [...key.key_ops] : undefined, kid: key.kid, alg: key.alg, crv: key.crv, e: key.e, n: key.n, x: key.x, x5c: key.x5c ? [...key.x5c] : undefined, y: key.y, })); instance(this).jwks = { keys }; }