chore(deps)!: openid-client 5 -> 6 (4j)
v6 replaced the Issuer/Client classes with a functional API. The four touchpoints rewritten: discovery() + ClientSecretBasic/Post for the cached configuration, serverMetadata() for the admin config test, buildAuthorizationUrl() (returns URL -> .href), and authorizationCodeGrant() with pkceCodeVerifier/expectedNonce/expectedState for the callback exchange — same validation surface (JWKS signature, iss/aud/exp/nbf, PKCE, state, nonce), now enforced by the library's single grant call. scripts/test-oidc-smoke.js: 44/44. Server suite 252/252. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
522eaaf101
commit
1627e75b29
|
|
@ -38,7 +38,7 @@
|
||||||
"lucide-react": "^1.24.0",
|
"lucide-react": "^1.24.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "^6.8.4",
|
||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
|
|
@ -8843,9 +8843,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "4.15.9",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
|
@ -9808,6 +9808,15 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth4webapi": {
|
||||||
|
"version": "3.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz",
|
||||||
|
"integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
@ -9817,15 +9826,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-hash": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
@ -9883,15 +9883,6 @@
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oidc-token-hash": {
|
|
||||||
"version": "5.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
|
||||||
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^10.13.0 || >=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
|
@ -9914,38 +9905,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openid-client": {
|
"node_modules/openid-client": {
|
||||||
"version": "5.7.1",
|
"version": "6.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.4.tgz",
|
||||||
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
"integrity": "sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jose": "^4.15.9",
|
"jose": "^6.2.2",
|
||||||
"lru-cache": "^6.0.0",
|
"oauth4webapi": "^3.8.5"
|
||||||
"object-hash": "^2.2.0",
|
|
||||||
"oidc-token-hash": "^5.0.3"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/openid-client/node_modules/lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openid-client/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
"lucide-react": "^1.24.0",
|
"lucide-react": "^1.24.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "^6.8.4",
|
||||||
"otplib": "^13.4.1",
|
"otplib": "^13.4.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { log } = require('../utils/logger.cts');
|
const { log } = require('../utils/logger.cts');
|
||||||
const { Issuer } = require('openid-client');
|
const oidc = require('openid-client'); // v6 functional API (discovery/buildAuthorizationUrl/authorizationCodeGrant)
|
||||||
|
|
||||||
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
const { getDb, getSetting, setSetting } = require('../db/database.cts');
|
||||||
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
const { encryptSecret, decryptSecret } = require('./encryptionService.cts');
|
||||||
|
|
@ -462,20 +462,20 @@ function getPublicOidcInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── openid-client Issuer / Client cache ───────────────────────────────────────
|
// ── openid-client Configuration cache ─────────────────────────────────────────
|
||||||
|
|
||||||
// Cache keyed by issuer/client/redirect so Admin config changes pick up a
|
// Cache keyed by issuer/client/redirect so Admin config changes pick up a
|
||||||
// fresh client. TTL prevents stale JWKS when the provider rotates keys.
|
// fresh configuration. TTL prevents stale JWKS when the provider rotates keys.
|
||||||
let _cachedClient = null;
|
let _cachedClient = null;
|
||||||
let _cachedClientKey = null;
|
let _cachedClientKey = null;
|
||||||
let _cacheTs = 0;
|
let _cacheTs = 0;
|
||||||
const CLIENT_CACHE_TTL = 60 * 60 * 1000; // 1 hour — JWKS key rotation window
|
const CLIENT_CACHE_TTL = 60 * 60 * 1000; // 1 hour — JWKS key rotation window
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a cached openid-client Client for the given config.
|
* Returns a cached openid-client v6 Configuration for the given config.
|
||||||
* Performs OIDC discovery (fetching jwks_uri and endpoints) on first call
|
* Performs OIDC discovery (fetching jwks_uri and endpoints) on first call
|
||||||
* or when the cache has expired. Signature verification uses the discovered
|
* or when the cache has expired. Signature verification uses the discovered
|
||||||
* JWKS URI automatically via openid-client@5.
|
* JWKS automatically via openid-client.
|
||||||
*/
|
*/
|
||||||
async function getOidcClient(config) {
|
async function getOidcClient(config) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -489,14 +489,16 @@ async function getOidcClient(config) {
|
||||||
return _cachedClient;
|
return _cachedClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuer = await Issuer.discover(config.issuerUrl);
|
const clientAuth =
|
||||||
_cachedClient = new issuer.Client({
|
config.tokenEndpointAuthMethod === 'client_secret_post'
|
||||||
client_id: config.clientId,
|
? oidc.ClientSecretPost(config.clientSecret)
|
||||||
client_secret: config.clientSecret,
|
: oidc.ClientSecretBasic(config.clientSecret);
|
||||||
token_endpoint_auth_method: config.tokenEndpointAuthMethod,
|
_cachedClient = await oidc.discovery(
|
||||||
redirect_uris: [config.redirectUri],
|
new URL(config.issuerUrl),
|
||||||
response_types: ['code'],
|
config.clientId,
|
||||||
});
|
{ redirect_uris: [config.redirectUri], response_types: ['code'] },
|
||||||
|
clientAuth,
|
||||||
|
);
|
||||||
_cachedClientKey = clientKey;
|
_cachedClientKey = clientKey;
|
||||||
_cacheTs = now;
|
_cacheTs = now;
|
||||||
return _cachedClient;
|
return _cachedClient;
|
||||||
|
|
@ -513,7 +515,7 @@ async function testOidcConfiguration(config = getOidcConfig()) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await getOidcClient(config);
|
const client = await getOidcClient(config);
|
||||||
const metadata = client.issuer?.metadata || {};
|
const metadata = client.serverMetadata() || {};
|
||||||
const missingMetadata = [];
|
const missingMetadata = [];
|
||||||
if (!metadata.authorization_endpoint) missingMetadata.push('authorization_endpoint');
|
if (!metadata.authorization_endpoint) missingMetadata.push('authorization_endpoint');
|
||||||
if (!metadata.token_endpoint) missingMetadata.push('token_endpoint');
|
if (!metadata.token_endpoint) missingMetadata.push('token_endpoint');
|
||||||
|
|
@ -618,15 +620,15 @@ async function buildAuthorizationUrl(config, state) {
|
||||||
const client = await getOidcClient(config);
|
const client = await getOidcClient(config);
|
||||||
const codeChallenge = generateCodeChallenge(state.codeVerifier);
|
const codeChallenge = generateCodeChallenge(state.codeVerifier);
|
||||||
|
|
||||||
// client.authorizationUrl() uses the discovered authorization_endpoint
|
// oidc.buildAuthorizationUrl() uses the discovered authorization_endpoint
|
||||||
return client.authorizationUrl({
|
return oidc.buildAuthorizationUrl(client, {
|
||||||
scope: config.scopes.join(' '),
|
scope: config.scopes.join(' '),
|
||||||
redirect_uri: config.redirectUri,
|
redirect_uri: config.redirectUri,
|
||||||
state: state.id,
|
state: state.id,
|
||||||
nonce: state.nonce,
|
nonce: state.nonce,
|
||||||
code_challenge: codeChallenge,
|
code_challenge: codeChallenge,
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
});
|
}).href;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Token exchange + full ID token verification ───────────────────────────────
|
// ── Token exchange + full ID token verification ───────────────────────────────
|
||||||
|
|
@ -650,21 +652,23 @@ async function buildAuthorizationUrl(config, state) {
|
||||||
async function exchangeAndVerifyTokens(config, code, stateId, savedState) {
|
async function exchangeAndVerifyTokens(config, code, stateId, savedState) {
|
||||||
const client = await getOidcClient(config);
|
const client = await getOidcClient(config);
|
||||||
|
|
||||||
// client.callback() handles token exchange + full validation in one step.
|
// authorizationCodeGrant() handles token exchange + full validation in one
|
||||||
// It throws OPError (provider error) or RPError (validation failure) on any problem.
|
// step (signature via discovered JWKS, iss/aud/exp/nbf, PKCE, state, nonce).
|
||||||
const tokenSet = await client.callback(
|
// It throws on any problem. The "current URL" is the callback URL as the
|
||||||
config.redirectUri,
|
// provider redirected to it.
|
||||||
{ code, state: stateId }, // parameters from the callback URL
|
const currentUrl = new URL(config.redirectUri);
|
||||||
{
|
currentUrl.searchParams.set('code', code);
|
||||||
code_verifier: savedState.code_verifier,
|
currentUrl.searchParams.set('state', stateId);
|
||||||
nonce: savedState.nonce,
|
|
||||||
state: stateId, // ensures state param matches expected value
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const claims = tokenSet.claims();
|
const tokens = await oidc.authorizationCodeGrant(client, currentUrl, {
|
||||||
|
pkceCodeVerifier: savedState.code_verifier,
|
||||||
|
expectedNonce: savedState.nonce,
|
||||||
|
expectedState: stateId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!claims.sub) throw new Error('ID token missing required sub claim');
|
const claims = tokens.claims();
|
||||||
|
|
||||||
|
if (!claims?.sub) throw new Error('ID token missing required sub claim');
|
||||||
|
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue