mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 21:29:43 +02:00
178 lines
4.8 KiB
JavaScript
178 lines
4.8 KiB
JavaScript
|
const jwt = require('jsonwebtoken');
|
||
|
const jwksClient = require('jwks-rsa');
|
||
|
const openidClient = require('openid-client');
|
||
|
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||
|
|
||
|
const Errors = {
|
||
|
INVALID_TOKEN: {
|
||
|
invalidToken: 'Access Token is invalid',
|
||
|
},
|
||
|
MISSING_VALUES: {
|
||
|
missingValues:
|
||
|
'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
const jwks = jwksClient({
|
||
|
jwksUri: sails.config.custom.oidcJwksUri,
|
||
|
requestHeaders: {}, // Optional
|
||
|
timeout: 30000, // Defaults to 30s
|
||
|
});
|
||
|
|
||
|
const getJwtVerificationOptions = () => {
|
||
|
const options = {};
|
||
|
if (sails.config.custom.oidcIssuer) {
|
||
|
options.issuer = sails.config.custom.oidcIssuer;
|
||
|
}
|
||
|
if (sails.config.custom.oidcAudience) {
|
||
|
options.audience = sails.config.custom.oidcAudience;
|
||
|
}
|
||
|
return options;
|
||
|
};
|
||
|
|
||
|
const validateAndDecodeToken = async (accessToken, options) => {
|
||
|
const keys = await jwks.getSigningKeys();
|
||
|
let validToken = {};
|
||
|
|
||
|
const isTokenValid = keys.some((signingKey) => {
|
||
|
try {
|
||
|
const key = signingKey.getPublicKey();
|
||
|
validToken = jwt.verify(accessToken, key, options);
|
||
|
return 'true';
|
||
|
} catch (error) {
|
||
|
sails.log.error(error);
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
|
||
|
if (!isTokenValid) {
|
||
|
const tokenForLogging = jwt.decode(accessToken);
|
||
|
const remoteAddress = getRemoteAddress(this.req);
|
||
|
|
||
|
sails.log.warn(
|
||
|
`invalid token: sub: "${tokenForLogging.sub}" issuer: "${tokenForLogging.iss}" audience: "${tokenForLogging.aud}" exp: ${tokenForLogging.exp} (IP: ${remoteAddress})`,
|
||
|
);
|
||
|
throw Errors.INVALID_TOKEN;
|
||
|
}
|
||
|
return validToken;
|
||
|
};
|
||
|
|
||
|
const getUserInfo = async (accessToken, options) => {
|
||
|
if (sails.config.custom.oidcSkipUserInfo) {
|
||
|
return {};
|
||
|
}
|
||
|
const issuer = await openidClient.Issuer.discover(options.issuer);
|
||
|
const oidcClient = new issuer.Client({
|
||
|
client_id: 'irrelevant',
|
||
|
});
|
||
|
const userInfo = await oidcClient.userinfo(accessToken);
|
||
|
return userInfo;
|
||
|
};
|
||
|
const mergeUserData = (validToken, userInfo) => {
|
||
|
const oidcUser = { ...validToken, ...userInfo };
|
||
|
return oidcUser;
|
||
|
};
|
||
|
const getOrCreateUser = async (newUser) => {
|
||
|
const user = await User.findOne({
|
||
|
where: {
|
||
|
username: newUser.username,
|
||
|
},
|
||
|
});
|
||
|
if (user) {
|
||
|
return user;
|
||
|
}
|
||
|
return User.create(newUser).fetch();
|
||
|
};
|
||
|
module.exports = {
|
||
|
inputs: {
|
||
|
token: {
|
||
|
type: 'string',
|
||
|
required: true,
|
||
|
},
|
||
|
},
|
||
|
|
||
|
exits: {
|
||
|
invalidToken: {
|
||
|
responseType: 'unauthorized',
|
||
|
},
|
||
|
missingValues: {
|
||
|
responseType: 'unauthorized',
|
||
|
},
|
||
|
},
|
||
|
|
||
|
async fn(inputs) {
|
||
|
const options = getJwtVerificationOptions();
|
||
|
const validToken = await validateAndDecodeToken(inputs.token, options);
|
||
|
const userInfo = await getUserInfo(inputs.token, options);
|
||
|
const oidcUser = mergeUserData(validToken, userInfo);
|
||
|
|
||
|
const now = new Date();
|
||
|
let isAdmin = false;
|
||
|
if (sails.config.custom.oidcAdminRoles.includes('*')) isAdmin = true;
|
||
|
else if (Array.isArray(oidcUser[sails.config.custom.oidcRolesAttribute])) {
|
||
|
const userRoles = new Set(oidcUser[sails.config.custom.oidcRolesAttribute]);
|
||
|
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
|
||
|
}
|
||
|
|
||
|
const newUser = {
|
||
|
email: oidcUser.email,
|
||
|
isAdmin,
|
||
|
name: oidcUser.name,
|
||
|
username: oidcUser.preferred_username,
|
||
|
subscribeToOwnCards: false,
|
||
|
createdAt: now,
|
||
|
updatedAt: now,
|
||
|
locked: true,
|
||
|
};
|
||
|
|
||
|
if (!newUser.email || !newUser.username || !newUser.name) {
|
||
|
sails.log.error(Errors.MISSING_VALUES.missingValues);
|
||
|
throw Errors.MISSING_VALUES;
|
||
|
}
|
||
|
|
||
|
const identityProviderUser = await IdentityProviderUser.findOne({
|
||
|
where: {
|
||
|
issuer: oidcUser.iss,
|
||
|
sub: oidcUser.sub,
|
||
|
},
|
||
|
}).populate('userId');
|
||
|
|
||
|
let user = identityProviderUser ? identityProviderUser.userId : {};
|
||
|
if (!identityProviderUser) {
|
||
|
user = await getOrCreateUser(newUser);
|
||
|
await IdentityProviderUser.create({
|
||
|
issuer: oidcUser.iss,
|
||
|
sub: oidcUser.sub,
|
||
|
userId: user.id,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const controlledFields = ['email', 'password', 'isAdmin', 'name', 'username'];
|
||
|
const updateFields = {};
|
||
|
controlledFields.forEach((field) => {
|
||
|
if (user[field] !== newUser[field]) {
|
||
|
updateFields[field] = newUser[field];
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (Object.keys(updateFields).length > 0) {
|
||
|
updateFields.updatedAt = now;
|
||
|
await User.updateOne({ id: user.id }).set(updateFields);
|
||
|
}
|
||
|
|
||
|
const plankaToken = sails.helpers.utils.createToken(user.id);
|
||
|
|
||
|
const remoteAddress = getRemoteAddress(this.req);
|
||
|
await Session.create({
|
||
|
accessToken: plankaToken,
|
||
|
remoteAddress,
|
||
|
userId: user.id,
|
||
|
userAgent: this.req.headers['user-agent'],
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
item: plankaToken,
|
||
|
};
|
||
|
},
|
||
|
};
|