@strongnguyen/oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
255 lines (223 loc) • 7.1 kB
JavaScript
const { strict: assert, AssertionError } = require('assert');
const { createHash } = require('crypto');
const hash = require('object-hash');
const { DEV_KEYSTORE } = require('../consts');
const base64url = require('./base64url');
const attention = require('./attention');
const instance = require('./weak_cache');
const KeyStore = require('./keystore');
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 base64url.encodeBuffer(createHash('sha256').update(JSON.stringify(components)).digest());
};
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 'secp256k1':
available = ['ES256K'];
break;
case 'P-384':
available = ['ES384'];
break;
case 'P-521':
available = ['ES512'];
break;
default:
}
break;
case 'OKP':
switch (jwk.crv) {
case 'Ed25519':
case 'Ed448':
available = ['EdDSA'];
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', 'RSA1_5'];
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':
case 'X448':
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 registerKey(key, i, keystore) {
assert(KEY_TYPES.has(key.kty), `only RSA, EC, or OKP keys should be part of jwks configuration (index ${i})`);
Object.entries(key).forEach(([property, value]) => {
if (['crv', 'd', 'dp', 'dq', 'e', 'kid', 'kty', 'n', 'p', 'q', 'qi', 'x', 'y', 'use'].includes(property)) {
assert(typeof value === 'string' && value, `jwks.keys[${i}].${property} configuration must be string`);
}
if (['key_ops', 'x5c'].includes(property)) {
assert(Array.isArray(value) && value.length && value.every((x) => typeof x === 'string' && x), `jwks.keys[${i}].${property} configuration must be an array of strings`);
}
switch (key.kty) {
case 'OKP':
assert(key.crv && key.x && key.d, `jwks.keys[${i}] configuration is missing required properties`);
break;
case 'EC':
assert(key.crv && key.x && key.y && key.d, `jwks.keys[${i}] configuration is missing required properties`);
break;
case 'RSA':
assert(key.e && key.n && key.d && key.p && key.q && key.dp && key.dq && key.qi, `jwks.keys[${i}] configuration is missing required properties`);
break;
default:
}
});
const conf = instance(this).configuration();
let encryptionAlgs;
if (conf.features.encryption.enabled) {
encryptionAlgs = jwkEncryptionAlgorithms(key);
[
// 'idTokenEncryptionAlgValues',
'requestObjectEncryptionAlgValues',
// 'userinfoEncryptionAlgValues',
].forEach((prop) => {
conf[prop] = [...new Set([...conf[prop], ...encryptionAlgs])]
.filter((v) => conf.enabledJWA[prop].includes(v));
});
}
const signingAlgs = jwkSignatureAlgorithms(key);
[
'idTokenSigningAlgValues',
// 'requestObjectSigningAlgValues' uses client's keystore
// 'tokenEndpointAuthSigningAlgValues' uses client's keystore
'userinfoSigningAlgValues',
'introspectionSigningAlgValues',
'authorizationSigningAlgValues',
].forEach((prop) => {
conf[prop] = [...new Set([...conf[prop], ...signingAlgs])]
.filter((v) => conf.enabledJWA[prop].includes(v));
});
const combined = signingAlgs.concat(encryptionAlgs).filter(Boolean);
/* eslint-disable no-param-reassign */
if (combined.length === 1) {
[key.alg] = combined;
}
if ((encryptionAlgs && encryptionAlgs.length) && !signingAlgs.length) {
key.use = 'enc';
} else if (signingAlgs.length && (!encryptionAlgs || !encryptionAlgs.length)) {
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 */
}
module.exports = function initializeKeystore(jwks) {
if (hash(jwks) === hash(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 configuration "jwks" property');
/* eslint-enable */
}
// eslint-disable-next-line no-undef
const keystore = new KeyStore();
let warned;
const keyIds = new Set();
try {
jwks.keys.map(({ ...jwk }) => jwk).forEach((key, i) => {
// eslint-disable-next-line no-unused-expressions, no-param-reassign
key.kid || (key.kid = calculateKid(key));
if (!warned && keyIds.has(key.kid)) {
warned = true;
/* eslint-disable no-multi-str */
attention.warn('different keys within the keystore SHOULD use distinct `kid` values, with \
your current keystore you should expect interoperability issues with your clients');
/* eslint-enable */
}
registerKey.call(this, key, i, keystore);
keyIds.add(key.kid);
});
} catch (err) {
throw new Error(err instanceof AssertionError ? err.message : 'keystore must be a JSON Web Key Set formatted object');
}
instance(this).keystore = keystore;
instance(this).jwksResponse = {
keys: [...keystore].map((jwk) => ({
kty: jwk.kty,
use: jwk.use,
key_ops: jwk.key_ops ? [...jwk.key_ops] : undefined,
kid: jwk.kid,
alg: jwk.alg,
crv: jwk.crv,
e: jwk.e,
n: jwk.n,
x: jwk.x,
x5c: jwk.x5c ? [...jwk.x5c] : undefined,
y: jwk.y,
})),
};
};