Added original configuration fields back for compatibility with providers not having a well-known endpoint

pull/7445/head
DavidLost 8 months ago
parent b0877ed15f
commit cb35303266

@ -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<string>} - 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<Object>} - 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
};
verifyJwt
}

@ -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 || '/'
}
}

@ -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
order: 15
Loading…
Cancel
Save