1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 13:19:44 +02:00
planka/server/api/helpers/users/get-or-create-one-with-oidc.js

188 lines
5.1 KiB
JavaScript
Raw Normal View History

/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports = {
inputs: {
code: {
type: 'string',
required: true,
},
nonce: {
type: 'string',
required: true,
},
},
exits: {
invalidOidcConfiguration: {},
invalidCodeOrNonce: {},
invalidUserinfoConfiguration: {},
missingValues: {},
emailAlreadyInUse: {},
usernameAlreadyInUse: {},
activeLimitReached: {},
},
async fn(inputs) {
const client = await sails.hooks.oidc.getClient();
if (!client) {
throw 'invalidOidcConfiguration';
}
let tokenSet;
try {
tokenSet = await client.callback(
sails.config.custom.oidcRedirectUri,
{
iss: sails.config.custom.oidcIssuer,
code: inputs.code,
},
{
nonce: inputs.nonce,
},
);
} catch (error) {
sails.log.warn(`Error while exchanging OIDC code: ${error}`);
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';
}
}
const email = claims[sails.config.custom.oidcEmailAttribute];
const name = claims[sails.config.custom.oidcNameAttribute];
if (!email || !name) {
throw 'missingValues';
}
let role = User.Roles.BOARD_USER;
if (!sails.config.custom.oidcIgnoreRoles) {
const claimsRoles = claims[sails.config.custom.oidcRolesAttribute];
if (Array.isArray(claimsRoles)) {
// Use a Set here to avoid quadratic time complexity
const claimsRolesSet = new Set(claimsRoles);
const foundRole = [User.Roles.ADMIN, User.Roles.PROJECT_OWNER, User.Roles.BOARD_USER].find(
(roleItem) => {
const configRoles = sails.config.custom[`oidc${_.upperFirst(roleItem)}Roles`];
if (configRoles.includes('*')) {
return true;
}
return configRoles.some((configRole) => claimsRolesSet.has(configRole));
},
);
if (foundRole) {
role = foundRole;
}
}
}
const values = {
email,
role,
name,
isSsoUser: true,
};
if (!sails.config.custom.oidcIgnoreUsername) {
values.username = claims[sails.config.custom.oidcUsernameAttribute];
}
// This whole block technically needs to be executed in a transaction
// with SERIALIZABLE isolation level (but Waterline does not support
// that), so this will result in errors if for example users are deleted
// concurrently with logging in via OIDC.
let identityProviderUser = await IdentityProviderUser.qm.getOneByIssuerAndSub(
sails.config.custom.oidcIssuer,
claims.sub,
);
let user;
let isCreated = false;
if (identityProviderUser) {
user = await User.qm.getOneById(identityProviderUser.userId);
} else {
// If no IDP/User mapping exists, search for the user by email.
user = await User.qm.getOneByEmail(values.email);
// Otherwise, create a new user.
if (!user) {
user = await sails.helpers.users.createOne
.with({
values,
actorUser: User.OIDC,
})
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse')
.intercept('activeLimitReached', 'activeLimitReached');
isCreated = true;
}
identityProviderUser = await IdentityProviderUser.qm.createOne({
userId: user.id,
issuer: sails.config.custom.oidcIssuer,
sub: claims.sub,
});
}
if (!isCreated) {
values.isDeactivated = false;
const updateFieldKeys = ['email', 'name', 'isSsoUser', 'isDeactivated'];
if (!sails.config.custom.oidcIgnoreUsername) {
updateFieldKeys.push('username');
}
if (!sails.config.custom.oidcIgnoreRoles) {
updateFieldKeys.push('role');
}
const updateValues = {};
// eslint-disable-next-line no-restricted-syntax
for (const k of updateFieldKeys) {
if (values[k] !== user[k]) updateValues[k] = values[k];
}
if (Object.keys(updateValues).length > 0) {
user = await sails.helpers.users.updateOne
.with({
record: user,
values: updateValues,
actorUser: User.OIDC,
})
.intercept('emailAlreadyInUse', 'emailAlreadyInUse')
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse')
.intercept('activeLimitReached', 'activeLimitReached');
}
}
return user;
},
};