From 1627e75b29525ed4c160ebd4cba0bf692f3e3922 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 10 Jul 2026 18:05:57 -0500 Subject: [PATCH] chore(deps)!: openid-client 5 -> 6 (4j) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package-lock.json | 65 +++++++++++----------------------------- package.json | 2 +- services/oidcService.cts | 64 ++++++++++++++++++++------------------- 3 files changed, 53 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c6c2fe..a08c403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "lucide-react": "^1.24.0", "node-cron": "^4.2.1", "nodemailer": "^9.0.3", - "openid-client": "^5.7.1", + "openid-client": "^6.8.4", "otplib": "^13.4.1", "qrcode": "^1.5.4", "react": "^19.2.7", @@ -8843,9 +8843,9 @@ } }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -9808,6 +9808,15 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9817,15 +9826,6 @@ "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": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -9883,15 +9883,6 @@ "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": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9914,38 +9905,18 @@ } }, "node_modules/openid-client": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", - "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.4.tgz", + "integrity": "sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==", "license": "MIT", "dependencies": { - "jose": "^4.15.9", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" + "jose": "^6.2.2", + "oauth4webapi": "^3.8.5" }, "funding": { "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": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index a28a550..4729d00 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "lucide-react": "^1.24.0", "node-cron": "^4.2.1", "nodemailer": "^9.0.3", - "openid-client": "^5.7.1", + "openid-client": "^6.8.4", "otplib": "^13.4.1", "qrcode": "^1.5.4", "react": "^19.2.7", diff --git a/services/oidcService.cts b/services/oidcService.cts index 44dee03..8d7ff24 100644 --- a/services/oidcService.cts +++ b/services/oidcService.cts @@ -51,7 +51,7 @@ const crypto = require('crypto'); 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 { 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 -// 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 _cachedClientKey = null; let _cacheTs = 0; 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 * 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) { const now = Date.now(); @@ -489,14 +489,16 @@ async function getOidcClient(config) { return _cachedClient; } - const issuer = await Issuer.discover(config.issuerUrl); - _cachedClient = new issuer.Client({ - client_id: config.clientId, - client_secret: config.clientSecret, - token_endpoint_auth_method: config.tokenEndpointAuthMethod, - redirect_uris: [config.redirectUri], - response_types: ['code'], - }); + const clientAuth = + config.tokenEndpointAuthMethod === 'client_secret_post' + ? oidc.ClientSecretPost(config.clientSecret) + : oidc.ClientSecretBasic(config.clientSecret); + _cachedClient = await oidc.discovery( + new URL(config.issuerUrl), + config.clientId, + { redirect_uris: [config.redirectUri], response_types: ['code'] }, + clientAuth, + ); _cachedClientKey = clientKey; _cacheTs = now; return _cachedClient; @@ -513,7 +515,7 @@ async function testOidcConfiguration(config = getOidcConfig()) { try { const client = await getOidcClient(config); - const metadata = client.issuer?.metadata || {}; + const metadata = client.serverMetadata() || {}; const missingMetadata = []; if (!metadata.authorization_endpoint) missingMetadata.push('authorization_endpoint'); if (!metadata.token_endpoint) missingMetadata.push('token_endpoint'); @@ -618,15 +620,15 @@ async function buildAuthorizationUrl(config, state) { const client = await getOidcClient(config); const codeChallenge = generateCodeChallenge(state.codeVerifier); - // client.authorizationUrl() uses the discovered authorization_endpoint - return client.authorizationUrl({ + // oidc.buildAuthorizationUrl() uses the discovered authorization_endpoint + return oidc.buildAuthorizationUrl(client, { scope: config.scopes.join(' '), redirect_uri: config.redirectUri, state: state.id, nonce: state.nonce, code_challenge: codeChallenge, code_challenge_method: 'S256', - }); + }).href; } // ── Token exchange + full ID token verification ─────────────────────────────── @@ -650,21 +652,23 @@ async function buildAuthorizationUrl(config, state) { async function exchangeAndVerifyTokens(config, code, stateId, savedState) { const client = await getOidcClient(config); - // client.callback() handles token exchange + full validation in one step. - // It throws OPError (provider error) or RPError (validation failure) on any problem. - const tokenSet = await client.callback( - config.redirectUri, - { code, state: stateId }, // parameters from the callback URL - { - code_verifier: savedState.code_verifier, - nonce: savedState.nonce, - state: stateId, // ensures state param matches expected value - }, - ); + // authorizationCodeGrant() handles token exchange + full validation in one + // step (signature via discovered JWKS, iss/aud/exp/nbf, PKCE, state, nonce). + // It throws on any problem. The "current URL" is the callback URL as the + // provider redirected to it. + const currentUrl = new URL(config.redirectUri); + currentUrl.searchParams.set('code', code); + currentUrl.searchParams.set('state', stateId); - 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; }