wekan/packages/wekan-oidc/oidc_server.js

260 lines
9.1 KiB
JavaScript
Raw Normal View History

import { addGroupsWithAttributes, addEmail, changeFullname, changeUsername } from './loginHandler';
const fs = Npm.require('fs'); // For file handling
Oidc = {};
2020-11-01 20:48:50 +01:00
httpCa = false;
// Load CA certificate if specified in the environment variable
if (process.env.OAUTH2_CA_CERT !== undefined) {
2020-11-01 20:48:50 +01:00
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);
2020-11-01 20:48:50 +01:00
}
}
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
}
2020-11-01 20:48:50 +01:00
};
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});
2020-11-01 20:48:50 +01:00
}
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
}
});