mirror of
https://github.com/wekan/wekan.git
synced 2025-12-27 12:48:49 +01:00
Add support for external email verification against OIDC login script. This will check local file for presence of email and log the user in if it is or deny them if it isn't.
260 lines
No EOL
9.1 KiB
JavaScript
260 lines
No EOL
9.1 KiB
JavaScript
import { addGroupsWithAttributes, addEmail, changeFullname, changeUsername } from './loginHandler';
|
|
const fs = Npm.require('fs'); // For file handling
|
|
|
|
Oidc = {};
|
|
httpCa = false;
|
|
|
|
// Load CA certificate if specified in the environment variable
|
|
if (process.env.OAUTH2_CA_CERT !== undefined) {
|
|
try {
|
|
if (fs.existsSync(process.env.OAUTH2_CA_CERT)) {
|
|
httpCa = fs.readFileSync(process.env.OAUTH2_CA_CERT);
|
|
}
|
|
} catch (e) {
|
|
console.log('WARNING: failed loading: ' + process.env.OAUTH2_CA_CERT);
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
var profile = {};
|
|
var serviceData = {};
|
|
var userinfo = {};
|
|
|
|
// Function to read the allowed emails from a local file specified in the environment variable
|
|
var getAllowedEmailsFromFile = function() {
|
|
var allowedEmails = [];
|
|
const filePath = process.env.OAUTH2_ALLOWEDEMAILS_FILEPATH; // Get the file path from environment variable
|
|
|
|
if (!filePath) {
|
|
throw new Error("OAUTH2_ALLOWEDEMAILS_FILEPATH environment variable is not set.");
|
|
}
|
|
|
|
try {
|
|
// Read the allowed emails file
|
|
const data = fs.readFileSync(filePath, 'utf-8');
|
|
allowedEmails = data.split('\n').map(email => email.trim());
|
|
} catch (error) {
|
|
console.error("Error reading allowed emails file:", error);
|
|
}
|
|
return allowedEmails;
|
|
};
|
|
|
|
// OAuth service registration
|
|
OAuth.registerService('oidc', 2, null, function (query) {
|
|
var debug = process.env.DEBUG === 'true';
|
|
|
|
var token = getToken(query);
|
|
if (debug) console.log('XXX: register token:', token);
|
|
|
|
var accessToken = token.access_token || token.id_token;
|
|
var expiresAt = (+new Date) + (1000 * parseInt(token.expires_in, 10));
|
|
|
|
var claimsInAccessToken = (process.env.OAUTH2_ADFS_ENABLED === 'true' ||
|
|
process.env.OAUTH2_ADFS_ENABLED === true ||
|
|
process.env.OAUTH2_B2C_ENABLED === 'true' ||
|
|
process.env.OAUTH2_B2C_ENABLED === true) || false;
|
|
|
|
if (claimsInAccessToken) {
|
|
userinfo = getTokenContent(accessToken);
|
|
} else {
|
|
userinfo = getUserInfo(accessToken);
|
|
}
|
|
|
|
if (userinfo.ocs) userinfo = userinfo.ocs.data;
|
|
if (userinfo.metadata) userinfo = userinfo.metadata;
|
|
if (debug) console.log('XXX: userinfo:', userinfo);
|
|
|
|
serviceData.id = userinfo[process.env.OAUTH2_ID_MAP];
|
|
serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP];
|
|
serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP];
|
|
serviceData.accessToken = accessToken;
|
|
serviceData.expiresAt = expiresAt;
|
|
|
|
// Oracle OIM and B2C checks remain the same
|
|
if (process.env.ORACLE_OIM_ENABLED === 'true' || process.env.ORACLE_OIM_ENABLED === true) {
|
|
if (userinfo[process.env.OAUTH2_EMAIL_MAP]) {
|
|
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP];
|
|
} else {
|
|
serviceData.email = userinfo[process.env.OAUTH2_USERNAME_MAP];
|
|
}
|
|
}
|
|
|
|
if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) {
|
|
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP];
|
|
}
|
|
|
|
if (process.env.OAUTH2_B2C_ENABLED === 'true' || process.env.OAUTH2_B2C_ENABLED === true) {
|
|
serviceData.email = userinfo["emails"][0];
|
|
}
|
|
|
|
if (accessToken) {
|
|
var tokenContent = getTokenContent(accessToken);
|
|
var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
|
|
_.extend(serviceData, fields);
|
|
}
|
|
|
|
if (token.refresh_token)
|
|
serviceData.refreshToken = token.refresh_token;
|
|
if (debug) console.log('XXX: serviceData:', serviceData);
|
|
|
|
profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP];
|
|
profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP];
|
|
|
|
if (process.env.OAUTH2_B2C_ENABLED === 'true' || process.env.OAUTH2_B2C_ENABLED === true) {
|
|
profile.email = userinfo["emails"][0];
|
|
}
|
|
|
|
if (debug) console.log('XXX: profile:', profile);
|
|
|
|
// New code: Check if the user's email is in the allowed emails list (only if oauth2-checkemails is true)
|
|
if (process.env.OAUTH2_CHECKEMAILS === 'true') {
|
|
const allowedEmails = getAllowedEmailsFromFile();
|
|
if (!allowedEmails.includes(profile.email)) {
|
|
throw new Error("Email not allowed: " + profile.email);
|
|
}
|
|
}
|
|
|
|
// Temporarily store data from oidc in user.services.oidc.groups to update groups
|
|
serviceData.groups = (userinfo["groups"] && userinfo["wekanGroups"]) ? userinfo["wekanGroups"] : userinfo["groups"];
|
|
|
|
if (Array.isArray(serviceData.groups) && serviceData.groups.length && typeof serviceData.groups[0] === "string") {
|
|
user = Meteor.users.findOne({'_id': serviceData.id});
|
|
|
|
serviceData.groups.forEach(function (groupName, i) {
|
|
if (user?.isAdmin && i == 0) {
|
|
serviceData.groups[i] = {"isAdmin": true};
|
|
serviceData.groups[i]["displayName"] = groupName;
|
|
} else {
|
|
serviceData.groups[i] = {"displayName": groupName};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fix OIDC login loop for integer user ID. Thanks to danielkaiser.
|
|
Meteor.call('groupRoutineOnLogin', serviceData, "" + serviceData.id);
|
|
Meteor.call('boardRoutineOnLogin', serviceData, "" + serviceData.id);
|
|
|
|
return {
|
|
serviceData: serviceData,
|
|
options: { profile: profile }
|
|
};
|
|
});
|
|
|
|
// Function to retrieve token based on environment
|
|
var getToken = function (query) {
|
|
var debug = process.env.DEBUG === 'true';
|
|
var config = getConfiguration();
|
|
var serverTokenEndpoint = config.tokenEndpoint.includes('https://') ?
|
|
config.tokenEndpoint : config.serverUrl + config.tokenEndpoint;
|
|
var response;
|
|
|
|
try {
|
|
var postOptions = {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
"User-Agent": "Meteor"
|
|
},
|
|
params: {
|
|
code: query.code,
|
|
client_id: config.clientId,
|
|
client_secret: OAuth.openSecret(config.secret),
|
|
redirect_uri: OAuth._redirectUri('oidc', config),
|
|
grant_type: 'authorization_code',
|
|
state: query.state
|
|
}
|
|
};
|
|
if (httpCa) {
|
|
postOptions['npmRequestOptions'] = { ca: httpCa };
|
|
}
|
|
response = HTTP.post(serverTokenEndpoint, postOptions);
|
|
} catch (err) {
|
|
throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
|
|
{ response: err.response });
|
|
}
|
|
if (response.data.error) {
|
|
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
|
|
} else {
|
|
return response.data;
|
|
}
|
|
};
|
|
|
|
// Function to fetch user information from the OIDC service
|
|
var getUserInfo = function (accessToken) {
|
|
var debug = process.env.DEBUG === 'true';
|
|
var config = getConfiguration();
|
|
var serverUserinfoEndpoint = config.userinfoEndpoint.includes("https://") ?
|
|
config.userinfoEndpoint : config.serverUrl + config.userinfoEndpoint;
|
|
|
|
var response;
|
|
try {
|
|
var getOptions = {
|
|
headers: {
|
|
"User-Agent": "Meteor",
|
|
"Authorization": "Bearer " + accessToken
|
|
}
|
|
};
|
|
if (httpCa) {
|
|
getOptions['npmRequestOptions'] = { ca: httpCa };
|
|
}
|
|
response = HTTP.get(serverUserinfoEndpoint, getOptions);
|
|
} catch (err) {
|
|
throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
|
|
{response: err.response});
|
|
}
|
|
return response.data;
|
|
};
|
|
|
|
// Function to get the configuration of the OIDC service
|
|
var getConfiguration = function () {
|
|
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
|
|
if (!config) {
|
|
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
|
|
}
|
|
return config;
|
|
};
|
|
|
|
// Function to decode the token content (JWT)
|
|
var getTokenContent = function (token) {
|
|
var content = null;
|
|
if (token) {
|
|
try {
|
|
var parts = token.split('.');
|
|
var header = JSON.parse(Buffer.from(parts[0], 'base64').toString());
|
|
content = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
} catch (err) {
|
|
content = { exp: 0 };
|
|
}
|
|
}
|
|
return content;
|
|
}
|
|
|
|
// Meteor methods to update groups and boards on login
|
|
Meteor.methods({
|
|
'groupRoutineOnLogin': function(info, userId) {
|
|
check(info, Object);
|
|
check(userId, String);
|
|
var propagateOidcData = process.env.PROPAGATE_OIDC_DATA || false;
|
|
if (propagateOidcData) {
|
|
users = Meteor.users;
|
|
user = users.findOne({'services.oidc.id': userId});
|
|
|
|
if (user) {
|
|
if (info.groups) {
|
|
addGroupsWithAttributes(user, info.groups);
|
|
}
|
|
|
|
if(info.email) addEmail(user, info.email);
|
|
if(info.fullname) changeFullname(user, info.fullname);
|
|
if(info.username) changeUsername(user, info.username);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Meteor.methods({
|
|
'boardRoutineOnLogin': function(info, userId) {
|
|
check(info, Object);
|
|
check(userId, String);
|
|
// Add board updates here if needed
|
|
}
|
|
}); |