Improved OIDC authentication: Only the well-known-id is required for configuration and added the option to merge claims from the id token

pull/7445/head
DavidLost 1 year ago
parent c7c20579fd
commit 277ce79ae9

@ -101,6 +101,7 @@
"js-yaml": "3.14.0",
"jsdom": "16.4.0",
"jsonwebtoken": "9.0.0",
"jwks-rsa": "3.1.0",
"katex": "0.12.0",
"klaw": "3.0.0",
"knex": "0.21.7",

@ -0,0 +1,48 @@
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
/**
* Function to get the signing key for a specific token.
* @param {Object} header - JWT header containing the `kid`.
* @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);
});
});
}
/**
* Verifies a JWT token using a public key from JWKS.
* @param {string} token - The JWT token to verify.
* @param {Object} conf - Configuration object containing `issuer` and `clientId`.
* @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);
}
}
module.exports = {
verifyJwt
};

@ -1,4 +1,5 @@
const _ = require('lodash')
const { verifyJwt } = require('../../../helpers/jwt')
/* global WIKI */
@ -9,56 +10,71 @@ const _ = require('lodash')
const OpenIDConnectStrategy = require('passport-openidconnect').Strategy
module.exports = {
init (passport, conf) {
passport.use(conf.key,
new OpenIDConnectStrategy({
authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL,
clientID: conf.clientId,
clientSecret: conf.clientSecret,
issuer: conf.issuer,
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
passReqToCallback: true,
skipUserProfile: conf.skipUserProfile,
acrValues: conf.acrValues
}, async (req, iss, uiProfile, idProfile, context, idToken, accessToken, refreshToken, params, cb) => {
const profile = Object.assign({}, idProfile, uiProfile)
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()
try {
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile: {
...profile,
email: _.get(profile, '_json.' + conf.emailClaim),
displayName: _.get(profile, '_json.' + conf.displayNameClaim, '')
}
})
if (conf.mapGroups) {
const groups = _.get(profile, '_json.' + 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)
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),
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', 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)
}
}
}
cb(null, user)
} catch (err) {
cb(err, null)
}
cb(null, user)
} catch (err) {
cb(err, null)
}
})
)
})
)
} catch (error) {
console.error('Error initializing OpenID Connect strategy:', error)
}
},
logout (conf) {
if (!conf.logoutURL) {
return '/'
} else {
return conf.logoutURL
}
return conf.logoutURL || '/'
}
}

@ -22,66 +22,70 @@ props:
title: Client Secret
hint: Application Client Secret
order: 2
authorizationURL:
wellKnownURL:
type: String
title: Authorization Endpoint URL
hint: Application Authorization Endpoint URL
title: Well-Known Configuration URL
hint: The Well-Known configuration Endpoint URL (e.g. https://provider/.well-known/openid-configuration)
order: 3
tokenURL:
type: String
title: Token Endpoint URL
hint: Application Token Endpoint URL
order: 4
userInfoURL:
type: String
title: User Info Endpoint URL
hint: User Info Endpoint URL
order: 5
skipUserProfile:
type: Boolean
default: false
title: Skip User Profile
hint: Skips call to the OIDC UserInfo endpoint
order: 6
issuer:
order: 4
userIdClaim:
userIdClaim:
type: String
title: Issuer
hint: Issuer URL
order: 7
title: ID Claim
hint: Field containing the user ID
default: id
maxWidth: 500
order: 5
emailClaim:
type: String
title: Email Claim
hint: Field containing the email address
default: email
maxWidth: 500
order: 8
order: 6
displayNameClaim:
type: String
title: Display Name Claim
hint: Field containing the user display name
default: displayName
maxWidth: 500
order: 9
order: 7
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
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 10
order: 9
groupsClaim:
type: String
title: Groups Claim
hint: Field containing the group names
default: groups
maxWidth: 500
order: 11
order: 10
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
acrValues:
type: String
title: ACR Values
hint: (optional) Authentication Context Class Reference
order: 13
order: 13
Loading…
Cancel
Save