diff --git a/packages/wekan-oidc/loginHandler.js b/packages/wekan-oidc/loginHandler.js index 3063d0bc9..916c4681c 100644 --- a/packages/wekan-oidc/loginHandler.js +++ b/packages/wekan-oidc/loginHandler.js @@ -1,11 +1,11 @@ // creates Object if not present in collection // initArr = [displayName, shortName, website, isActive] // objString = ["Org","Team"] for method mapping -function createObject(initArr, objString) +async function createObject(initArr, objString) { functionName = objString === "Org" ? 'setCreateOrgFromOidc' : 'setCreateTeamFromOidc'; creationString = 'setCreate'+ objString + 'FromOidc'; - return Meteor.call(functionName, + return await Meteor.callAsync(functionName, initArr[0],//displayName initArr[1],//desc initArr[2],//shortName @@ -13,10 +13,10 @@ function createObject(initArr, objString) initArr[4]//xxxisActive ); } -function updateObject(initArr, objString) +async function updateObject(initArr, objString) { functionName = objString === "Org" ? 'setOrgAllFieldsFromOidc' : 'setTeamAllFieldsFromOidc'; - return Meteor.call(functionName, + return await Meteor.callAsync(functionName, initArr[0],//team || org Object initArr[1],//displayName initArr[2],//desc @@ -57,7 +57,7 @@ module.exports = { // isAdmin: [true, false] -> admin group becomes admin in wekan // isOrganization: [true, false] -> creates org and adds to user // displayName: "string" -addGroupsWithAttributes: function (user, groups){ +addGroupsWithAttributes: async function (user, groups){ teamArray=[]; orgArray=[]; isAdmin = []; @@ -76,20 +76,20 @@ addGroupsWithAttributes: function (user, groups){ isAdmin.push(group.isAdmin || false); if (isOrg) { - org = Org.findOne({"orgDisplayName": group.displayName}); + org = await Org.findOneAsync({"orgDisplayName": group.displayName}); if(org) { if(contains(orgs, org, "org")) { initAttributes.unshift(org); - updateObject(initAttributes, "Org"); + await updateObject(initAttributes, "Org"); continue; } } else if(forceCreate) { - createObject(initAttributes, "Org"); - org = Org.findOne({'orgDisplayName': group.displayName}); + await createObject(initAttributes, "Org"); + org = await Org.findOneAsync({'orgDisplayName': group.displayName}); } else { @@ -102,20 +102,20 @@ addGroupsWithAttributes: function (user, groups){ else { //start team routine - team = Team.findOne({"teamDisplayName": group.displayName}); + team = await Team.findOneAsync({"teamDisplayName": group.displayName}); if (team) { if(contains(teams, team, "team")) { initAttributes.unshift(team); - updateObject(initAttributes, "Team"); + await updateObject(initAttributes, "Team"); continue; } } else if(forceCreate) { - createObject(initAttributes, "Team"); - team = Team.findOne({'teamDisplayName': group.displayName}); + await createObject(initAttributes, "Team"); + team = await Team.findOneAsync({'teamDisplayName': group.displayName}); } else { @@ -129,28 +129,28 @@ addGroupsWithAttributes: function (user, groups){ // hence user will get admin privileges in wekan // E.g. Admin rights will be withdrawn if no group in oidc provider has isAdmin set to true - users.update({ _id: user._id }, { $set: {isAdmin: isAdmin.some(i => (i === true))}}); + await users.updateAsync({ _id: user._id }, { $set: {isAdmin: isAdmin.some(i => (i === true))}}); teams = {'teams': {'$each': teamArray}}; orgs = {'orgs': {'$each': orgArray}}; - users.update({ _id: user._id }, { $push: teams}); - users.update({ _id: user._id }, { $push: orgs}); + await users.updateAsync({ _id: user._id }, { $push: teams}); + await users.updateAsync({ _id: user._id }, { $push: orgs}); // remove temporary oidc data from user collection - users.update({ _id: user._id }, { $unset: {"services.oidc.groups": []}}); + await users.updateAsync({ _id: user._id }, { $unset: {"services.oidc.groups": []}}); return; }, -changeUsername: function(user, name) +changeUsername: async function(user, name) { username = {'username': name}; - if (user.username != username) users.update({ _id: user._id }, { $set: username}); + if (user.username != username) await users.updateAsync({ _id: user._id }, { $set: username}); }, -changeFullname: function(user, name) +changeFullname: async function(user, name) { username = {'profile.fullname': name}; - if (user.username != username) users.update({ _id: user._id }, { $set: username}); + if (user.username != username) await users.updateAsync({ _id: user._id }, { $set: username}); }, -addEmail: function(user, email) +addEmail: async function(user, email) { user_email = user.emails || []; var contained = false; @@ -173,7 +173,7 @@ addEmail: function(user, email) { user_email.unshift({'address': email, 'verified': true}); user_email = {'emails': user_email}; - users.update({ _id: user._id }, { $set: user_email}); + await users.updateAsync({ _id: user._id }, { $set: user_email}); } } } diff --git a/packages/wekan-oidc/oidc_server.js b/packages/wekan-oidc/oidc_server.js index a8cb0f2dd..86ecb265d 100644 --- a/packages/wekan-oidc/oidc_server.js +++ b/packages/wekan-oidc/oidc_server.js @@ -1,11 +1,15 @@ import {addGroupsWithAttributes, addEmail, changeFullname, changeUsername} from './loginHandler'; +import { fetch, Headers } from 'meteor/fetch'; +import { URLSearchParams } from 'meteor/url'; +import { Buffer } from 'node:buffer'; +import https from 'https'; +import fs from 'fs'; Oidc = {}; httpCa = false; 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); } @@ -18,10 +22,10 @@ var profile = {}; var serviceData = {}; var userinfo = {}; -OAuth.registerService('oidc', 2, null, function (query) { +OAuth.registerService('oidc', 2, null, async function (query) { var debug = process.env.DEBUG === 'true'; - var token = getToken(query); + var token = await getToken(query); if (debug) console.log('XXX: register token:', token); var accessToken = token.access_token || token.id_token; @@ -40,7 +44,7 @@ OAuth.registerService('oidc', 2, null, function (query) { else { // normal behaviour, getting the claims from UserInfo endpoint. - userinfo = getUserInfo(accessToken); + userinfo = await getUserInfo(accessToken); } if (userinfo.ocs) userinfo = userinfo.ocs.data; // Nextcloud hack @@ -73,7 +77,8 @@ OAuth.registerService('oidc', 2, null, function (query) { if (accessToken) { var tokenContent = getTokenContent(accessToken); - var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields); + var config = await getConfiguration(); + var fields = _.pick(tokenContent, config.idTokenWhitelistFields); _.extend(serviceData, fields); } @@ -100,7 +105,7 @@ OAuth.registerService('oidc', 2, null, function (query) { // 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}); + user = await Meteor.users.findOneAsync({'_id': serviceData.id}); serviceData.groups.forEach(function(groupName, i) { @@ -119,8 +124,8 @@ OAuth.registerService('oidc', 2, null, function (query) { // 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); + await Meteor.callAsync('groupRoutineOnLogin',serviceData, ""+serviceData.id); + await Meteor.callAsync('boardRoutineOnLogin',serviceData, ""+serviceData.id); return { serviceData: serviceData, @@ -134,143 +139,166 @@ if (Meteor.release) { } if (process.env.ORACLE_OIM_ENABLED !== 'true' && process.env.ORACLE_OIM_ENABLED !== true) { - var getToken = function (query) { + var getToken = async function (query) { var debug = process.env.DEBUG === 'true'; - var config = getConfiguration(); + var config = await getConfiguration(); + var serverTokenEndpoint; if(config.tokenEndpoint.includes('https://')){ - var serverTokenEndpoint = config.tokenEndpoint; + serverTokenEndpoint = config.tokenEndpoint; }else{ - var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint; + serverTokenEndpoint = config.serverUrl + config.tokenEndpoint; } - 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 - } - }; + var body = new URLSearchParams({ + 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 fetchOptions = { + method: 'POST', + headers: new Headers({ + 'Accept': 'application/json', + 'User-Agent': userAgent, + 'Content-Type': 'application/x-www-form-urlencoded' + }), + body: body.toString() + }; + if (httpCa) { - postOptions['npmRequestOptions'] = { ca: httpCa }; + fetchOptions.agent = new https.Agent({ ca: httpCa }); } - response = HTTP.post(serverTokenEndpoint, postOptions); + + var response = await fetch(serverTokenEndpoint, fetchOptions); + var data = await response.json(); + + if (!response.ok) { + throw new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + response.statusText); + } + if (data.error) { + // if the http response was a json object with an error attribute + throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + data.error); + } + if (debug) console.log('XXX: getToken response: ', data); + return data; } catch (err) { 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); - } else { - if (debug) console.log('XXX: getToken response: ', response.data); - return response.data; - } }; } if (process.env.ORACLE_OIM_ENABLED === 'true' || process.env.ORACLE_OIM_ENABLED === true) { - var getToken = function (query) { + var getToken = async function (query) { var debug = process.env.DEBUG === 'true'; - var config = getConfiguration(); + var config = await getConfiguration(); + var serverTokenEndpoint; if(config.tokenEndpoint.includes('https://')){ - var serverTokenEndpoint = config.tokenEndpoint; + serverTokenEndpoint = config.tokenEndpoint; }else{ - var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint; + serverTokenEndpoint = config.serverUrl + config.tokenEndpoint; } - 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; - - dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET; - strBasicToken = new Buffer(dataToken); - strBasicToken64 = strBasicToken.toString('base64'); + var dataToken = process.env.OAUTH2_CLIENT_ID + ':' + process.env.OAUTH2_SECRET; + var strBasicToken64 = Buffer.from(dataToken).toString('base64'); // eslint-disable-next-line no-console if (debug) console.log('Basic Token: ', strBasicToken64); 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 body = new URLSearchParams({ + 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 fetchOptions = { + method: 'POST', + headers: new Headers({ + 'Accept': 'application/json', + 'User-Agent': userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + strBasicToken64 + }), + body: body.toString() + }; + if (httpCa) { - postOptions['npmRequestOptions'] = { ca: httpCa }; + fetchOptions.agent = new https.Agent({ ca: httpCa }); } - response = HTTP.post(serverTokenEndpoint, postOptions); + + var response = await fetch(serverTokenEndpoint, fetchOptions); + var data = await response.json(); + + if (!response.ok) { + throw new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + response.statusText); + } + if (data.error) { + // if the http response was a json object with an error attribute + throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + data.error); + } + // eslint-disable-next-line no-console + if (debug) console.log('XXX: getToken response: ', data); + return data; } catch (err) { 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); - } else { - // eslint-disable-next-line no-console - if (debug) console.log('XXX: getToken response: ', response.data); - return response.data; - } }; } -var getUserInfo = function (accessToken) { +var getUserInfo = async function (accessToken) { var debug = process.env.DEBUG === 'true'; - var config = getConfiguration(); + var config = await 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. + var serverUserinfoEndpoint; if (config.userinfoEndpoint.includes("https://")) { - var serverUserinfoEndpoint = config.userinfoEndpoint; + serverUserinfoEndpoint = config.userinfoEndpoint; } else { - var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint; + serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint; } - var response; + try { - var getOptions = { - headers: { - "User-Agent": userAgent, - "Authorization": "Bearer " + accessToken - } - }; + var fetchOptions = { + method: 'GET', + headers: new Headers({ + 'User-Agent': userAgent, + 'Authorization': 'Bearer ' + accessToken + }) + }; + if (httpCa) { - getOptions['npmRequestOptions'] = { ca: httpCa }; + fetchOptions.agent = new https.Agent({ ca: httpCa }); } - response = HTTP.get(serverUserinfoEndpoint, getOptions); + + var response = await fetch(serverUserinfoEndpoint, fetchOptions); + + if (!response.ok) { + throw new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + response.statusText); + } + + var data = await response.json(); + if (debug) console.log('XXX: getUserInfo response: ', data); + return data; } 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; }; -var getConfiguration = function () { - var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' }); +var getConfiguration = async function () { + var config = await ServiceConfiguration.configurations.findOneAsync({ service: 'oidc' }); if (!config) { throw new ServiceConfiguration.ConfigError('Service oidc not configured.'); } @@ -295,24 +323,24 @@ var getTokenContent = function (token) { return content; } Meteor.methods({ - 'groupRoutineOnLogin': function(info, userId) + 'groupRoutineOnLogin': async 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}); + user = await users.findOneAsync({'services.oidc.id': userId}); if(user) { //updates/creates Groups and user admin privileges accordingly if not undefined if (info.groups) { - addGroupsWithAttributes(user, info.groups); + await 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) await addEmail(user, info.email); + if(info.fullname) await changeFullname(user, info.fullname); + if(info.username) await changeUsername(user, info.username); } } } @@ -328,8 +356,9 @@ Meteor.methods({ const defaultBoardId = defaultBoardParams.shift() if (!defaultBoardId) return - const board = Boards.findOne(defaultBoardId) - const userId = Users.findOne({ 'services.oidc.id': oidcUserId })?._id + const board = await Boards.findOneAsync(defaultBoardId) + const user = await Users.findOneAsync({ 'services.oidc.id': oidcUserId }) + const userId = user?._id const memberIndex = _.pluck(board?.members, 'userId').indexOf(userId); if(!board || !userId || memberIndex > -1) return diff --git a/packages/wekan-oidc/package.js b/packages/wekan-oidc/package.js index fee31e566..83d68bde7 100644 --- a/packages/wekan-oidc/package.js +++ b/packages/wekan-oidc/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "OpenID Connect (OIDC) flow for Meteor", - version: "1.0.12", + version: "1.1.0", name: "wekan-oidc", git: "https://github.com/wekan/wekan-oidc.git", }); @@ -8,7 +8,7 @@ Package.describe({ Package.onUse(function(api) { api.use('oauth2', ['client', 'server']); api.use('oauth', ['client', 'server']); - api.use('http', ['server']); + api.use('fetch', ['server']); api.use('underscore', 'client'); api.use('ecmascript'); api.use('templating', 'client');