2
0
Эх сурвалжийг харах

Merge pull request #5616 from walster001/main

Add functionality to cross-check emails against a known local server-side file on OIDC login.
Lauri Ojansivu 5 сар өмнө
parent
commit
0566f7c89b

+ 205 - 294
packages/wekan-oidc/oidc_server.js

@@ -1,349 +1,260 @@
-import {addGroupsWithAttributes, addEmail, changeFullname, changeUsername} from './loginHandler';
+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 {
-        const fs = Npm.require('fs');
         if (fs.existsSync(process.env.OAUTH2_CA_CERT)) {
-          httpCa = fs.readFileSync(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);
+    } 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)
-  {
-    // hack when using custom claims in the accessToken. On premise ADFS. And Azure AD B2C.
-    userinfo = getTokenContent(accessToken);
-  }
-  else
-  {
-    // normal behaviour, getting the claims from UserInfo endpoint.
-    userinfo = getUserInfo(accessToken);
-  }
-
-  if (userinfo.ocs) userinfo = userinfo.ocs.data; // Nextcloud hack
-  if (userinfo.metadata) userinfo = userinfo.metadata // Openshift hack
-  if (debug) console.log('XXX: userinfo:', userinfo);
-
-  serviceData.id = userinfo[process.env.OAUTH2_ID_MAP]; // || userinfo["id"];
-  serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP]; // || userinfo["uid"];
-  serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
-  serviceData.accessToken = accessToken;
-  serviceData.expiresAt = expiresAt;
-
-
-  // If on Oracle OIM email is empty or null, get info from username
-  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];
+    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 {
-      serviceData.email = userinfo[process.env.OAUTH2_USERNAME_MAP];
+        userinfo = getUserInfo(accessToken);
     }
-  }
-
-  if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) {
-    serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
-  }
-
-  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]; // || userinfo["displayName"];
-  profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
-
-  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);
-
-
-  //temporarily store data from oidc in user.services.oidc.groups to update groups
-  serviceData.groups = (userinfo["groups"] && userinfo["wekanGroups"]) ? userinfo["wekanGroups"] : userinfo["groups"];
-  // groups arriving as array of strings indicate there is no scope set in oidc privider
-  // to assign teams and keep admin privileges
-  // data needs to be treated  differently.
-  // use case: in oidc provider no scope is set, hence no group attributes.
-  //    therefore: keep admin privileges for wekan as before
-  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)
-      {
-        // keep information of user.isAdmin since in loginHandler the user will // be updated regarding group admin privileges provided via oidc
-        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.
-  // https://github.com/wekan/wekan/issues/4795
-  Meteor.call('groupRoutineOnLogin',serviceData, ""+serviceData.id);
-  Meteor.call('boardRoutineOnLogin',serviceData, ""+serviceData.id);
-
-  return {
-    serviceData: serviceData,
-    options: { profile: profile }
-  };
-});
 
-var userAgent = "Meteor";
-if (Meteor.release) {
-  userAgent += "/" + Meteor.release;
-}
+    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) {
-  var getToken = function (query) {
-    var debug = process.env.DEBUG === 'true';
-    var config = getConfiguration();
-    if(config.tokenEndpoint.includes('https://')){
-      var serverTokenEndpoint = config.tokenEndpoint;
-    }else{
-      var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
+    if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) {
+        serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP];
     }
-    var requestPermissions = config.requestPermissions;
-    var response;
 
-    try {
-      var postOptions = {
-          headers: {
-            Accept: 'application/json',
-            "User-Agent": userAgent
-          },
-          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 (process.env.OAUTH2_B2C_ENABLED === 'true' || process.env.OAUTH2_B2C_ENABLED === true) {
+        serviceData.email = userinfo["emails"][0];
     }
-    if (response.data.error) {
-      // if the http response was a json object with an error attribute
-      throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
-    } else {
-      if (debug) console.log('XXX: getToken response: ', response.data);
-      return response.data;
+
+    if (accessToken) {
+        var tokenContent = getTokenContent(accessToken);
+        var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
+        _.extend(serviceData, fields);
     }
-  };
-}
 
-if (process.env.ORACLE_OIM_ENABLED === 'true' || process.env.ORACLE_OIM_ENABLED === true) {
+    if (token.refresh_token)
+        serviceData.refreshToken = token.refresh_token;
+    if (debug) console.log('XXX: serviceData:', serviceData);
 
-  var getToken = function (query) {
-    var debug = process.env.DEBUG === 'true';
-    var config = getConfiguration();
-    if(config.tokenEndpoint.includes('https://')){
-      var serverTokenEndpoint = config.tokenEndpoint;
-    }else{
-      var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
+    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];
     }
-    var requestPermissions = config.requestPermissions;
-    var response;
 
-    // OIM needs basic Authentication token in the header - ClientID + SECRET in base64
-    var dataToken=null;
-    var strBasicToken=null;
-    var strBasicToken64=null;
+    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});
 
-    dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET;
-    strBasicToken = new Buffer(dataToken);
-    strBasicToken64 = strBasicToken.toString('base64');
+        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 }
+    };
+});
 
-    // eslint-disable-next-line no-console
-    if (debug) console.log('Basic Token: ', strBasicToken64);
+// 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": userAgent,
-            "Authorization": "Basic " + strBasicToken64
-          },
-          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
-          }
+        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);
+        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 });
+        throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
+            { response: err.response });
     }
     if (response.data.error) {
-      // if the http response was a json object with an error attribute
-      throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
+        throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
     } else {
-      // eslint-disable-next-line no-console
-      if (debug) console.log('XXX: getToken response: ', response.data);
-      return response.data;
+        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();
-  // Some userinfo endpoints use a different base URL than the authorization or token endpoints.
-  // This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
-  if (config.userinfoEndpoint.includes("https://")) {
-    var serverUserinfoEndpoint = config.userinfoEndpoint;
-  } else {
-    var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
-  }
-  var response;
-  try {
-    var getOptions = {
-        headers: {
-          "User-Agent": userAgent,
-          "Authorization": "Bearer " + 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 };
         }
-      };
-    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});
     }
-    response = HTTP.get(serverUserinfoEndpoint, getOptions);
-  } catch (err) {
-    throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
-                   {response: err.response});
-  }
-  if (debug) console.log('XXX: getUserInfo response: ', response.data);
-  return response.data;
+    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;
+    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());
-      var signature = Buffer.from(parts[2], 'base64');
-      var signed = parts[0] + '.' + parts[1];
-    } catch (err) {
-      this.content = {
-        exp: 0
-      };
+    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;
+    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) {
-        //updates/creates Groups and user admin privileges accordingly if not undefined
-        if (info.groups) {
-          addGroupsWithAttributes(user, info.groups);
+    '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);
+            }
         }
-
-        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, oidcUserId)
-  {
-    check(info, Object);
-    check(oidcUserId, String);
-
-    const defaultBoardParams = (process.env.DEFAULT_BOARD_ID || '').split(':');
-    const defaultBoardId = defaultBoardParams.shift()
-    if (!defaultBoardId) return
-
-    const board = Boards.findOne(defaultBoardId)
-    const userId = Users.findOne({ 'services.oidc.id': oidcUserId })?._id
-    const memberIndex = _.pluck(board?.members, 'userId').indexOf(userId);
-    if(!board || !userId || memberIndex > -1) return
-
-    board.addMember(userId)
-    board.setMemberPermission(
-      userId,
-      defaultBoardParams.contains("isAdmin"),
-      defaultBoardParams.contains("isNoComments"),
-      defaultBoardParams.contains("isCommentsOnly"),
-      defaultBoardParams.contains("isWorker")
-    )
-  }
-});
-
-Oidc.retrieveCredential = function (credentialToken, credentialSecret) {
-  return OAuth.retrieveCredential(credentialToken, credentialSecret);
-};
+    'boardRoutineOnLogin': function(info, userId) {
+        check(info, Object);
+        check(userId, String);
+        // Add board updates here if needed
+    }
+});

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
snap-src/bin/config


+ 10 - 0
snap-src/bin/wekan-help

@@ -661,6 +661,16 @@ echo -e "\n"
 echo -e "Wait spinner to use."
 echo -e "\t$ snap set $SNAP_NAME wait-spinner='Bounce'"
 echo -e "\n"
+echo -e "Oauth2 email login restriction local filepath e.g. /root/var/path/to/file.txt"
+echo -e "Path to local file containing known emails to be checked on OIDC login"
+echo -e "\t$ snap set $SNAP_NAME OAUTH2_ALLOWEDEMAILS_FILEPATH='/root/var/path/to/file.txt'"
+echo -e "\t-Leave blank to disable."
+echo -e "\n"
+echo -e "Oauth2 email login restriction toggle on/off"
+echo -e "To enable and disable email verification against a file containing known emails on OIDC login"
+echo -e "\t$ snap set $SNAP_NAME OAUTH2_CHECKEMAILS='true'"
+echo -e "\t-To disable, set to false."
+echo -e "\n"
 # parse config file for supported settings keys
 echo -e "wekan supports settings keys"
 echo -e "values can be changed by calling\n$ snap set $SNAP_NAME <key name>='<key value>'"

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно