2024-12-12 00:34:22 +10:30
|
|
|
import { addGroupsWithAttributes, addEmail, changeFullname, changeUsername } from './loginHandler';
|
|
|
|
const fs = Npm.require('fs'); // For file handling
|
2022-02-23 15:09:03 +01:00
|
|
|
|
2019-04-20 15:18:33 +03:00
|
|
|
Oidc = {};
|
2020-11-01 20:48:50 +01:00
|
|
|
httpCa = false;
|
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// Load CA certificate if specified in the environment variable
|
2021-01-06 15:43:46 +02:00
|
|
|
if (process.env.OAUTH2_CA_CERT !== undefined) {
|
2020-11-01 20:48:50 +01:00
|
|
|
try {
|
2021-01-06 15:47:21 +02:00
|
|
|
if (fs.existsSync(process.env.OAUTH2_CA_CERT)) {
|
2024-12-12 00:34:22 +10:30
|
|
|
httpCa = fs.readFileSync(process.env.OAUTH2_CA_CERT);
|
2021-01-06 15:43:46 +02:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
} catch (e) {
|
|
|
|
console.log('WARNING: failed loading: ' + process.env.OAUTH2_CA_CERT);
|
|
|
|
console.log(e);
|
2020-11-01 20:48:50 +01:00
|
|
|
}
|
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
|
2022-03-10 15:56:35 +01:00
|
|
|
var profile = {};
|
|
|
|
var serviceData = {};
|
|
|
|
var userinfo = {};
|
2019-04-20 15:18:33 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// 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
|
2019-04-20 15:18:33 +03:00
|
|
|
OAuth.registerService('oidc', 2, null, function (query) {
|
2024-12-12 00:34:22 +10:30
|
|
|
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);
|
2020-10-02 23:15:39 +03:00
|
|
|
} else {
|
2024-12-12 00:34:22 +10:30
|
|
|
userinfo = getUserInfo(accessToken);
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
2019-04-20 15:18:33 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
2019-04-20 15:18:33 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) {
|
|
|
|
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP];
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
2019-04-20 15:18:33 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
if (process.env.OAUTH2_B2C_ENABLED === 'true' || process.env.OAUTH2_B2C_ENABLED === true) {
|
|
|
|
serviceData.email = userinfo["emails"][0];
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
|
|
|
|
if (accessToken) {
|
|
|
|
var tokenContent = getTokenContent(accessToken);
|
|
|
|
var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
|
|
|
|
_.extend(serviceData, fields);
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
if (token.refresh_token)
|
|
|
|
serviceData.refreshToken = token.refresh_token;
|
|
|
|
if (debug) console.log('XXX: serviceData:', serviceData);
|
2020-10-02 23:15:39 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
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];
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
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});
|
2020-10-02 23:15:39 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
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 }
|
|
|
|
};
|
|
|
|
});
|
2020-10-02 23:15:39 +03:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// 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;
|
2020-10-02 23:15:39 +03:00
|
|
|
|
|
|
|
try {
|
2024-12-12 00:34:22 +10:30
|
|
|
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
|
|
|
};
|
2024-12-12 00:34:22 +10:30
|
|
|
if (httpCa) {
|
|
|
|
postOptions['npmRequestOptions'] = { ca: httpCa };
|
|
|
|
}
|
|
|
|
response = HTTP.post(serverTokenEndpoint, postOptions);
|
2020-10-02 23:15:39 +03:00
|
|
|
} catch (err) {
|
2024-12-12 00:34:22 +10:30
|
|
|
throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
|
|
|
|
{ response: err.response });
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
|
|
|
if (response.data.error) {
|
2024-12-12 00:34:22 +10:30
|
|
|
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
|
2020-10-02 23:15:39 +03:00
|
|
|
} else {
|
2024-12-12 00:34:22 +10:30
|
|
|
return response.data;
|
2020-10-02 23:15:39 +03:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
};
|
2022-03-10 15:56:35 +01:00
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// Function to fetch user information from the OIDC service
|
2019-04-20 15:18:33 +03:00
|
|
|
var getUserInfo = function (accessToken) {
|
2024-12-12 00:34:22 +10:30
|
|
|
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 };
|
2019-04-20 15:18:33 +03:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
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
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
return response.data;
|
2019-04-20 15:18:33 +03:00
|
|
|
};
|
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// Function to get the configuration of the OIDC service
|
2019-04-20 15:18:33 +03:00
|
|
|
var getConfiguration = function () {
|
2024-12-12 00:34:22 +10:30
|
|
|
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
|
|
|
|
if (!config) {
|
|
|
|
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
|
|
|
|
}
|
|
|
|
return config;
|
2019-04-20 15:18:33 +03:00
|
|
|
};
|
|
|
|
|
2024-12-12 00:34:22 +10:30
|
|
|
// Function to decode the token content (JWT)
|
2019-04-20 15:18:33 +03:00
|
|
|
var getTokenContent = function (token) {
|
2024-12-12 00:34:22 +10:30
|
|
|
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 };
|
|
|
|
}
|
2019-04-20 15:18:33 +03:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
return content;
|
2019-04-20 15:18:33 +03:00
|
|
|
}
|
2024-12-12 00:34:22 +10:30
|
|
|
|
|
|
|
// Meteor methods to update groups and boards on login
|
2022-03-10 15:56:35 +01:00
|
|
|
Meteor.methods({
|
2024-12-12 00:34:22 +10:30
|
|
|
'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);
|
|
|
|
}
|
2023-07-16 23:13:41 +02:00
|
|
|
}
|
2022-03-10 15:56:35 +01:00
|
|
|
}
|
|
|
|
});
|
2019-04-20 15:18:33 +03:00
|
|
|
|
2023-08-22 14:06:49 +02:00
|
|
|
Meteor.methods({
|
2024-12-12 00:34:22 +10:30
|
|
|
'boardRoutineOnLogin': function(info, userId) {
|
|
|
|
check(info, Object);
|
|
|
|
check(userId, String);
|
|
|
|
// Add board updates here if needed
|
|
|
|
}
|
|
|
|
});
|