From cb353032668b4aa2e4a74632373d1b9a96e8acca Mon Sep 17 00:00:00 2001 From: DavidLost Date: Mon, 22 Sep 2025 23:02:16 +0200 Subject: [PATCH] Added original configuration fields back for compatibility with providers not having a well-known endpoint --- server/helpers/jwt.js | 55 ++++---- .../authentication/oidc/authentication.js | 127 ++++++++++-------- .../authentication/oidc/definition.yml | 47 ++++--- 3 files changed, 122 insertions(+), 107 deletions(-) diff --git a/server/helpers/jwt.js b/server/helpers/jwt.js index 7b59a0e6..5a19aa11 100644 --- a/server/helpers/jwt.js +++ b/server/helpers/jwt.js @@ -1,5 +1,5 @@ -const jwt = require('jsonwebtoken'); -const jwksClient = require('jwks-rsa'); +const jwt = require('jsonwebtoken') +const jwksClient = require('jwks-rsa') /** * Function to get the signing key for a specific token. @@ -7,16 +7,15 @@ const jwksClient = require('jwks-rsa'); * @returns {Promise} - Resolves with the signing key. */ function getSigningKey(header, jwksUri) { - return new Promise((resolve, reject) => { - const client = jwksClient({ jwksUri }); - client.getSigningKey(header.kid, (err, key) => { - if (err) { - return reject('Error getting signing key:' + err); - } - const signingKey = key.getPublicKey(); - resolve(signingKey); - }); - }); + return new Promise((resolve, reject) => { + const client = jwksClient({ jwksUri }) + client.getSigningKey(header.kid, (err, key) => { + if (err) { + return reject(new Error('Error getting signing key: ' + err)) + } + resolve(key.getPublicKey()) + }) + }) } /** @@ -26,23 +25,23 @@ function getSigningKey(header, jwksUri) { * @returns {Promise} - Resolves with the decoded token if verification is successful. */ async function verifyJwt(token, conf) { - try { - const decodedHeader = jwt.decode(token, { complete: true }); - if (!decodedHeader || !decodedHeader.header) { - throw new Error('JWT verification failed: Invalid token header'); - } - const signingKey = await getSigningKey(decodedHeader.header, conf.jwksUri); - const decoded = jwt.verify(token, signingKey, { - algorithms: conf.algorithms || ['RS256'], - issuer: conf.issuer, - audience: conf.clientId - }); - return decoded; - } catch (err) { - throw new Error('JWT verification failed: ' + err.message); + try { + const decodedHeader = jwt.decode(token, { complete: true }) + if (!decodedHeader || !decodedHeader.header) { + throw new Error('JWT verification failed: Invalid token header') } + const signingKey = await getSigningKey(decodedHeader.header, conf.jwksUri) + const decoded = jwt.verify(token, signingKey, { + algorithms: conf.algorithms || ['RS256'], + issuer: conf.issuer, + audience: conf.clientId + }) + return decoded + } catch (err) { + throw new Error('JWT verification failed: ' + err.message) + } } module.exports = { - verifyJwt -}; \ No newline at end of file + verifyJwt +} diff --git a/server/modules/authentication/oidc/authentication.js b/server/modules/authentication/oidc/authentication.js index 7da5c13d..3b667709 100644 --- a/server/modules/authentication/oidc/authentication.js +++ b/server/modules/authentication/oidc/authentication.js @@ -1,6 +1,5 @@ const _ = require('lodash') const { verifyJwt } = require('../../../helpers/jwt') - /* global WIKI */ // ------------------------------------ @@ -10,71 +9,81 @@ const { verifyJwt } = require('../../../helpers/jwt') const OpenIDConnectStrategy = require('passport-openidconnect').Strategy module.exports = { - async init (passport, conf) { + async init(passport, conf) { try { - const response = await fetch(conf.wellKnownURL) - if (!response.ok) throw new Error(`Failed to fetch well-known config: ${response.statusText}`) - const wellKnown = await response.json() - - passport.use(conf.key, - new OpenIDConnectStrategy({ - issuer: wellKnown.issuer, - authorizationURL: wellKnown.authorization_endpoint, - tokenURL: wellKnown.token_endpoint, - userInfoURL: wellKnown.userinfo_endpoint, - clientID: conf.clientId, - clientSecret: conf.clientSecret, - callbackURL: conf.callbackURL, - scope: conf.scope, - passReqToCallback: true, - skipUserProfile: conf.skipUserProfile, - acrValues: conf.acrValues - }, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => { - let idTokenClaims = {} - if (conf.mergeIdTokenClaims && idToken) { - idTokenClaims = await verifyJwt(idToken, { - issuer: wellKnown.issuer, - clientId: conf.clientId, - jwksUri: wellKnown.jwks_uri, - algorithms: wellKnown.id_token_signing_alg_values_supported - }) - } - // Merge claims from ID token and profile, with idProfile taking precedence - const profile = { ...idTokenClaims, ...idProfile } - try { - const user = await WIKI.models.users.processProfile({ - providerKey: req.params.strategy, - profile: { - ...profile, - id: _.get(profile, conf.userIdClaim), - displayName: _.get(profile, conf.displayNameClaim, '???'), - email: _.get(profile, conf.emailClaim), + let oidcConfig = { + issuer: conf.issuer, + authorizationURL: conf.authorizationURL, + tokenURL: conf.tokenURL, + userInfoURL: conf.userInfoURL, + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + scope: conf.scope, + passReqToCallback: true, + skipUserProfile: conf.skipUserProfile, + acrValues: conf.acrValues + } + if (conf.wellKnownURL) { + try { + const response = await fetch(conf.wellKnownURL) + if (!response.ok) throw new Error(response.statusText) + const wellKnown = await response.json() + if (!oidcConfig.issuer) oidcConfig.issuer = wellKnown.issuer + if (!oidcConfig.authorizationURL) oidcConfig.authorizationURL = wellKnown.authorization_endpoint + if (!oidcConfig.tokenURL) oidcConfig.tokenURL = wellKnown.token_endpoint + if (!oidcConfig.userInfoURL) oidcConfig.userInfoURL = wellKnown.userinfo_endpoint + oidcConfig.jwksUri = wellKnown.jwks_uri + oidcConfig.idTokenSigningAlgValuesSupported = wellKnown.id_token_signing_alg_values_supported + } catch (error) { + WIKI.logger.error('Error fetching OIDC well-known configuration:', error) + } + } + passport.use(conf.key, new OpenIDConnectStrategy(oidcConfig, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => { + let idTokenClaims = {} + if (conf.mergeIdTokenClaims && idToken) { + idTokenClaims = await verifyJwt(idToken, { + issuer: oidcConfig.issuer, + clientId: oidcConfig.clientID, + jwksUri: oidcConfig.jwksUri, + algorithms: oidcConfig.idTokenSigningAlgValuesSupported + }) + } + // Merge claims from ID token and profile, with idProfile taking precedence + const profile = { ...idTokenClaims, ...idProfile } + try { + const user = await WIKI.models.users.processProfile({ + providerKey: req.params.strategy, + profile: { + ...profile, + id: _.get(profile, conf.userIdClaim), + displayName: _.get(profile, conf.displayNameClaim, 'Unknown User'), + email: _.get(profile, conf.emailClaim) + } + }) + if (conf.mapGroups) { + const groups = _.get(profile, conf.groupsClaim) + if (groups && _.isArray(groups)) { + const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) + const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) + for (const groupId of _.difference(expectedGroups, currentGroups)) { + await user.$relatedQuery('groups').relate(groupId) } - }) - if (conf.mapGroups) { - const groups = _.get(profile, conf.groupsClaim) - if (groups && _.isArray(groups)) { - const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) - const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) - for (const groupId of _.difference(expectedGroups, currentGroups)) { - await user.$relatedQuery('groups').relate(groupId) - } - for (const groupId of _.difference(currentGroups, expectedGroups)) { - await user.$relatedQuery('groups').unrelate().where('groupId', groupId) - } + for (const groupId of _.difference(currentGroups, expectedGroups)) { + await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } - cb(null, user) - } catch (err) { - cb(err, null) } - }) - ) - } catch (error) { - console.error('Error initializing OpenID Connect strategy:', error) + cb(null, user) + } catch (err) { + cb(err, null) + } + })) + } catch (err) { + WIKI.logger.error(`Error initializing OpenID Connect strategy: ${err}`) } }, - logout (conf) { + logout(conf) { return conf.logoutURL || '/' } } diff --git a/server/modules/authentication/oidc/definition.yml b/server/modules/authentication/oidc/definition.yml index 33e4f757..29f53af5 100644 --- a/server/modules/authentication/oidc/definition.yml +++ b/server/modules/authentication/oidc/definition.yml @@ -27,65 +27,72 @@ props: title: Well-Known Configuration URL hint: The Well-Known configuration Endpoint URL (e.g. https://provider/.well-known/openid-configuration) order: 3 + authorizationURL: + type: String + title: Authorization Endpoint URL + hint: Application Authorization Endpoint URL (overrides value from well-known URL if set) + order: 4 + tokenURL: + type: String + title: Token Endpoint URL + hint: Application Token Endpoint URL (overrides value from well-known URL if set) + order: 5 + userInfoURL: + type: String + title: User Info Endpoint URL + hint: User Info Endpoint URL (overrides value from well-known URL if set) + order: 6 skipUserProfile: type: Boolean default: false title: Skip User Profile hint: Skips call to the OIDC UserInfo endpoint - order: 4 - userIdClaim: - userIdClaim: + order: 7 + issuer: type: String - title: ID Claim - hint: Field containing the user ID - default: id - maxWidth: 500 - order: 5 + title: Issuer + hint: Issuer URL (overrides value from well-known URL if set) + order: 8 emailClaim: type: String title: Email Claim hint: Field containing the email address default: email maxWidth: 500 - order: 6 + order: 9 displayNameClaim: type: String title: Display Name Claim hint: Field containing the user display name default: displayName maxWidth: 500 - order: 7 + order: 10 mergeIdTokenClaims: type: Boolean title: Merge ID Token Claims hint: If enabled, verifies the ID token and merges its claims into the user profile default: false - order: 8 + order: 11 mapGroups: type: Boolean title: Map Groups hint: Map groups matching names from the groups claim value default: false - order: 9 + order: 12 groupsClaim: type: String title: Groups Claim hint: Field containing the group names default: groups maxWidth: 500 - order: 10 + order: 13 logoutURL: type: String title: Logout URL hint: (optional) Logout URL on the OAuth2 provider where the user will be redirected to complete the logout process. - order: 11 - scope: - type: String - title: Scope - hint: (optional) Application Client permission scopes. - order: 12 + order: 14 acrValues: type: String title: ACR Values hint: (optional) Authentication Context Class Reference - order: 13 \ No newline at end of file + order: 15 \ No newline at end of file