Browse Source

Reverted all previous Oauth2 filepath code, thinking is there some better way.

Thanks to xet7 !

Related https://github.com/wekan/wekan/pull/5619,
related https://github.com/wekan/wekan/pull/5616
Lauri Ojansivu 5 months ago
parent
commit
c936d83b38
3 changed files with 294 additions and 215 deletions
  1. 294 205
      packages/wekan-oidc/oidc_server.js
  2. 0 0
      snap-src/bin/config
  3. 0 10
      snap-src/bin/wekan-help

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

@@ -1,260 +1,349 @@
-import { addGroupsWithAttributes, addEmail, changeFullname, changeUsername } from './loginHandler';
-const fs = Npm.require('fs');  // For file handling
+import {addGroupsWithAttributes, addEmail, changeFullname, changeUsername} from './loginHandler';
 
 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) {
-        userinfo = getTokenContent(accessToken);
+  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];
     } 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];
+      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]; // || 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 }
+  };
+});
 
-    if (process.env.OAUTH2_B2C_ENABLED === 'true' || process.env.OAUTH2_B2C_ENABLED === true) {
-        serviceData.email = userinfo["emails"][0];
-    }
+var userAgent = "Meteor";
+if (Meteor.release) {
+  userAgent += "/" + Meteor.release;
+}
 
-    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) {
+  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;
     }
+    var requestPermissions = config.requestPermissions;
+    var response;
 
-    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];
+    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 (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);
-        }
+    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;
     }
+  };
+}
 
-    // 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});
+if (process.env.ORACLE_OIM_ENABLED === 'true' || process.env.ORACLE_OIM_ENABLED === true) {
 
-        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};
-            }
-        });
+  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;
     }
+    var requestPermissions = config.requestPermissions;
+    var response;
 
-    // Fix OIDC login loop for integer user ID. Thanks to danielkaiser.
-    Meteor.call('groupRoutineOnLogin', serviceData, "" + serviceData.id);
-    Meteor.call('boardRoutineOnLogin', serviceData, "" + serviceData.id);
+    // OIM needs basic Authentication token in the header - ClientID + SECRET in base64
+    var dataToken=null;
+    var strBasicToken=null;
+    var strBasicToken64=null;
 
-    return {
-        serviceData: serviceData,
-        options: { profile: profile }
-    };
-});
+    dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET;
+    strBasicToken = new Buffer(dataToken);
+    strBasicToken64 = strBasicToken.toString('base64');
 
-// 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;
+    // eslint-disable-next-line no-console
+    if (debug) console.log('Basic Token: ', strBasicToken64);
 
     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
-            }
+      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
+          }
         };
-        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) {
-        throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + 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 {
-        return response.data;
+      // eslint-disable-next-line no-console
+      if (debug) console.log('XXX: getToken response: ', 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();
-    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 };
+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
         }
-        response = HTTP.get(serverUserinfoEndpoint, getOptions);
-    } catch (err) {
-        throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
-            {response: err.response});
+      };
+    if (httpCa) {
+      getOptions['npmRequestOptions'] = { ca: httpCa };
     }
-    return response.data;
+    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;
 };
 
-// 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());
-        } catch (err) {
-            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());
+      var signature = Buffer.from(parts[2], 'base64');
+      var signed = parts[0] + '.' + parts[1];
+    } catch (err) {
+      this.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) {
-                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);
-            }
+  '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);
         }
+
+        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
-    }
-});
+  '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);
+};

File diff suppressed because it is too large
+ 0 - 0
snap-src/bin/config


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

@@ -661,16 +661,6 @@ 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>'"

Some files were not shown because too many files changed in this diff