Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"vue"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": [
"server/locales"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions server/helpers/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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(new Error('Error getting signing key: ' + err))
}
resolve(key.getPublicKey())
})
})
}

/**
* 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
}
64 changes: 45 additions & 19 deletions server/modules/authentication/oidc/authentication.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const _ = require('lodash')
const { verifyJwt } = require('../../../helpers/jwt')

/* global WIKI */

Expand All @@ -9,33 +10,60 @@ const _ = require('lodash')
const OpenIDConnectStrategy = require('passport-openidconnect').Strategy

module.exports = {
init (passport, conf) {
passport.use(conf.key,
new OpenIDConnectStrategy({
async init(passport, conf) {
try {
let oidcConfig = {
issuer: conf.issuer,
authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL,
userInfoURL: conf.userInfoURL,
clientID: conf.clientId,
clientSecret: conf.clientSecret,
issuer: conf.issuer,
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
scope: 'profile email ' + conf.scope,
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)

}
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,
email: _.get(profile, '_json.' + conf.emailClaim),
displayName: _.get(profile, '_json.' + conf.displayNameClaim, '')
id: _.get(profile, conf.userIdClaim),
displayName: _.get(profile, conf.displayNameClaim, 'Unknown User'),
email: _.get(profile, conf.emailClaim)
}
})
if (conf.mapGroups) {
const groups = _.get(profile, '_json.' + conf.groupsClaim)
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)
Expand All @@ -51,14 +79,12 @@ module.exports = {
} catch (err) {
cb(err, null)
}
})
)
},
logout (conf) {
if (!conf.logoutURL) {
return '/'
} else {
return conf.logoutURL
}))
} catch (err) {
WIKI.logger.error(`Error initializing OpenID Connect strategy: ${err}`)
}
},
logout(conf) {
return conf.logoutURL || '/'
}
}
90 changes: 55 additions & 35 deletions server/modules/authentication/oidc/definition.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,62 @@ color: blue-grey darken-2
website: http://openid.net/connect/
isAvailable: true
useForm: false
scopes:
- openid
- profile
- email
props:
clientId:
wellKnownURL:
type: String
title: Client ID
hint: Application Client ID
title: Well-Known Configuration URL
hint: The Well-Known configuration Endpoint URL (e.g. https://provider/.well-known/openid-configuration)
order: 1
clientSecret:
type: String
title: Client Secret
hint: Application Client Secret
order: 2
authorizationURL:
type: String
title: Authorization Endpoint URL
hint: Application Authorization Endpoint URL
order: 3
hint: Application Authorization Endpoint URL (overrides value from well-known URL if set)
order: 2
tokenURL:
type: String
title: Token Endpoint URL
hint: Application Token Endpoint URL
order: 4
hint: Application Token Endpoint URL (overrides value from well-known URL if set)
order: 3
userInfoURL:
type: String
title: User Info Endpoint URL
hint: User Info Endpoint URL
hint: User Info Endpoint URL (overrides value from well-known URL if set)
order: 4
issuer:
type: String
title: Issuer URL
hint: Issuer URL (overrides value from well-known URL if set)
order: 5
skipUserProfile:
type: Boolean
default: false
title: Skip User Profile
hint: Skips call to the OIDC UserInfo endpoint
clientId:
type: String
title: Client ID
hint: Application Client ID
order: 6
issuer:
clientSecret:
type: String
title: Issuer
hint: Issuer URL
title: Client Secret
hint: Application Client Secret
order: 7
userIdClaim:
type: String
title: User Id Claim
hint: Field containing the unique user identifier
default: sub
maxWidth: 500
order: 8
emailClaim:
type: String
title: Email Claim
hint: Field containing the email address
default: email
maxWidth: 500
order: 8
order: 9
displayNameClaim:
type: String
title: Display Name Claim
hint: Field containing the user display name
default: displayName
default: name
maxWidth: 500
order: 9
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 10
groupsClaim:
type: String
Expand All @@ -75,13 +71,37 @@ props:
default: groups
maxWidth: 500
order: 11
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: 12
mapGroups:
type: Boolean
title: Map Groups
hint: Map groups matching names from the groups claim value
default: false
order: 13
skipUserProfile:
type: Boolean
default: false
title: Skip User Profile
hint: Skips call to the OIDC UserInfo endpoint
order: 14
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: 12
order: 15
scope:
type: String
title: Additional Scopes
hint: (optional) Additional space-separated OIDC scopes (e.g. 'offline_access groups') - openid, profile and email are always included
maxWidth: 500
order: 16
acrValues:
type: String
title: ACR Values
hint: (optional) Authentication Context Class Reference
order: 13
order: 17