1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Ability to configure OIDC claims source (#888)

Closes #884
This commit is contained in:
iMarKoLiGa 2024-09-20 16:19:54 +02:00 committed by GitHub
parent e410e21363
commit 1217969e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 46 additions and 29 deletions

View file

@ -37,6 +37,7 @@ services:
# - OIDC_RESPONSE_MODE=fragment # - OIDC_RESPONSE_MODE=fragment
# - OIDC_USE_DEFAULT_RESPONSE_MODE=true # - OIDC_USE_DEFAULT_RESPONSE_MODE=true
# - OIDC_ADMIN_ROLES=admin # - OIDC_ADMIN_ROLES=admin
# - OIDC_CLAIMS_SOURCE=userinfo
# - OIDC_EMAIL_ATTRIBUTE=email # - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name # - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username # - OIDC_USERNAME_ATTRIBUTE=preferred_username

View file

@ -44,6 +44,7 @@ services:
# - OIDC_RESPONSE_MODE=fragment # - OIDC_RESPONSE_MODE=fragment
# - OIDC_USE_DEFAULT_RESPONSE_MODE=true # - OIDC_USE_DEFAULT_RESPONSE_MODE=true
# - OIDC_ADMIN_ROLES=admin # - OIDC_ADMIN_ROLES=admin
# - OIDC_CLAIMS_SOURCE=userinfo
# - OIDC_EMAIL_ATTRIBUTE=email # - OIDC_EMAIL_ATTRIBUTE=email
# - OIDC_NAME_ATTRIBUTE=name # - OIDC_NAME_ATTRIBUTE=name
# - OIDC_USERNAME_ATTRIBUTE=preferred_username # - OIDC_USERNAME_ATTRIBUTE=preferred_username

View file

@ -35,6 +35,7 @@ SECRET_KEY=notsecretkey
# OIDC_RESPONSE_MODE=fragment # OIDC_RESPONSE_MODE=fragment
# OIDC_USE_DEFAULT_RESPONSE_MODE=true # OIDC_USE_DEFAULT_RESPONSE_MODE=true
# OIDC_ADMIN_ROLES=admin # OIDC_ADMIN_ROLES=admin
# OIDC_CLAIMS_SOURCE=userinfo
# OIDC_EMAIL_ATTRIBUTE=email # OIDC_EMAIL_ATTRIBUTE=email
# OIDC_NAME_ATTRIBUTE=name # OIDC_NAME_ATTRIBUTE=name
# OIDC_USERNAME_ATTRIBUTE=preferred_username # OIDC_USERNAME_ATTRIBUTE=preferred_username

View file

@ -6,8 +6,8 @@ const Errors = {
INVALID_CODE_OR_NONCE: { INVALID_CODE_OR_NONCE: {
invalidCodeOrNonce: 'Invalid code or nonce', invalidCodeOrNonce: 'Invalid code or nonce',
}, },
INVALID_USERINFO_SIGNATURE: { INVALID_USERINFO_CONFIGURATION: {
invalidUserinfoSignature: 'Invalid signature on userinfo due to client misconfiguration', invalidUserinfoConfiguration: 'Invalid userinfo configuration',
}, },
EMAIL_ALREADY_IN_USE: { EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use', emailAlreadyInUse: 'Email already in use',
@ -40,7 +40,7 @@ module.exports = {
invalidCodeOrNonce: { invalidCodeOrNonce: {
responseType: 'unauthorized', responseType: 'unauthorized',
}, },
invalidUserinfoSignature: { invalidUserinfoConfiguration: {
responseType: 'unauthorized', responseType: 'unauthorized',
}, },
emailAlreadyInUse: { emailAlreadyInUse: {
@ -63,7 +63,7 @@ module.exports = {
sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`); sails.log.warn(`Invalid code or nonce! (IP: ${remoteAddress})`);
return Errors.INVALID_CODE_OR_NONCE; return Errors.INVALID_CODE_OR_NONCE;
}) })
.intercept('invalidUserinfoSignature', () => Errors.INVALID_USERINFO_SIGNATURE) .intercept('invalidUserinfoConfiguration', () => Errors.INVALID_USERINFO_CONFIGURATION)
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE) .intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE) .intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
.intercept('missingValues', () => Errors.MISSING_VALUES); .intercept('missingValues', () => Errors.MISSING_VALUES);

View file

@ -12,7 +12,7 @@ module.exports = {
exits: { exits: {
invalidCodeOrNonce: {}, invalidCodeOrNonce: {},
invalidUserinfoSignature: {}, invalidUserinfoConfiguration: {},
missingValues: {}, missingValues: {},
emailAlreadyInUse: {}, emailAlreadyInUse: {},
usernameAlreadyInUse: {}, usernameAlreadyInUse: {},
@ -21,9 +21,9 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const client = sails.hooks.oidc.getClient(); const client = sails.hooks.oidc.getClient();
let userInfo; let tokenSet;
try { try {
const tokenSet = await client.callback( tokenSet = await client.callback(
sails.config.custom.oidcRedirectUri, sails.config.custom.oidcRedirectUri,
{ {
iss: sails.config.custom.oidcIssuer, iss: sails.config.custom.oidcIssuer,
@ -33,23 +33,36 @@ module.exports = {
nonce: inputs.nonce, nonce: inputs.nonce,
}, },
); );
userInfo = await client.userinfo(tokenSet); } catch (error) {
} catch (e) { sails.log.warn(`Error while exchanging OIDC code: ${error}`);
if (
e instanceof SyntaxError &&
e.message.includes('Unexpected token e in JSON at position 0')
) {
sails.log.warn('Error while exchanging OIDC code: userinfo response is signed');
throw 'invalidUserinfoSignature';
}
sails.log.warn(`Error while exchanging OIDC code: ${e}`);
throw 'invalidCodeOrNonce'; throw 'invalidCodeOrNonce';
} }
let claims;
if (sails.config.custom.oidcClaimsSource === 'id_token') {
claims = tokenSet.claims();
} else {
try {
claims = await client.userinfo(tokenSet);
} catch (error) {
let errorText;
if (
error instanceof SyntaxError &&
error.message.includes('Unexpected token e in JSON at position 0')
) {
errorText = 'response is signed';
} else {
errorText = error.toString();
}
sails.log.warn(`Error while fetching OIDC userinfo: ${errorText}`);
throw 'invalidUserinfoConfiguration';
}
}
if ( if (
!userInfo[sails.config.custom.oidcEmailAttribute] || !claims[sails.config.custom.oidcEmailAttribute] ||
!userInfo[sails.config.custom.oidcNameAttribute] !claims[sails.config.custom.oidcNameAttribute]
) { ) {
throw 'missingValues'; throw 'missingValues';
} }
@ -58,23 +71,23 @@ module.exports = {
if (sails.config.custom.oidcAdminRoles.includes('*')) { if (sails.config.custom.oidcAdminRoles.includes('*')) {
isAdmin = true; isAdmin = true;
} else { } else {
const roles = userInfo[sails.config.custom.oidcRolesAttribute]; const roles = claims[sails.config.custom.oidcRolesAttribute];
if (Array.isArray(roles)) { if (Array.isArray(roles)) {
// Use a Set here to avoid quadratic time complexity // Use a Set here to avoid quadratic time complexity
const userRoles = new Set(userInfo[sails.config.custom.oidcRolesAttribute]); const userRoles = new Set(claims[sails.config.custom.oidcRolesAttribute]);
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1; isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
} }
} }
const values = { const values = {
isAdmin, isAdmin,
email: userInfo[sails.config.custom.oidcEmailAttribute], email: claims[sails.config.custom.oidcEmailAttribute],
isSso: true, isSso: true,
name: userInfo[sails.config.custom.oidcNameAttribute], name: claims[sails.config.custom.oidcNameAttribute],
subscribeToOwnCards: false, subscribeToOwnCards: false,
}; };
if (!sails.config.custom.oidcIgnoreUsername) { if (!sails.config.custom.oidcIgnoreUsername) {
values.username = userInfo[sails.config.custom.oidcUsernameAttribute]; values.username = claims[sails.config.custom.oidcUsernameAttribute];
} }
let user; let user;
@ -84,7 +97,7 @@ module.exports = {
// concurrently with logging in via OIDC. // concurrently with logging in via OIDC.
let identityProviderUser = await IdentityProviderUser.findOne({ let identityProviderUser = await IdentityProviderUser.findOne({
issuer: sails.config.custom.oidcIssuer, issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub, sub: claims.sub,
}); });
if (identityProviderUser) { if (identityProviderUser) {
@ -108,7 +121,7 @@ module.exports = {
identityProviderUser = await IdentityProviderUser.create({ identityProviderUser = await IdentityProviderUser.create({
userId: user.id, userId: user.id,
issuer: sails.config.custom.oidcIssuer, issuer: sails.config.custom.oidcIssuer,
sub: userInfo.sub, sub: claims.sub,
}); });
} }

View file

@ -130,8 +130,8 @@ async function sendWebhook(webhook, event, data, user) {
`Webhook ${webhook.url} failed with status ${response.status} and message: ${message}`, `Webhook ${webhook.url} failed with status ${response.status} and message: ${message}`,
); );
} }
} catch (e) { } catch (error) {
sails.log.error(`Webhook ${webhook.url} failed with error message: ${e.message}`); sails.log.error(`Webhook ${webhook.url} failed with error: ${error}`);
} }
} }

View file

@ -52,6 +52,7 @@ module.exports.custom = {
oidcResponseMode: process.env.OIDC_RESPONSE_MODE || 'fragment', oidcResponseMode: process.env.OIDC_RESPONSE_MODE || 'fragment',
oidcUseDefaultResponseMode: process.env.OIDC_USE_DEFAULT_RESPONSE_MODE === 'true', oidcUseDefaultResponseMode: process.env.OIDC_USE_DEFAULT_RESPONSE_MODE === 'true',
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [], oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
oidcClaimsSource: process.env.OIDC_CLAIMS_SOURCE || 'userinfo',
oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email', oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email',
oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name', oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name',
oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username', oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username',