Merge pull request #6111 from harryadel/wekan-oidc-async-migration

Migrate wekan-oidc to async API for Meteor 3.0
This commit is contained in:
Lauri Ojansivu 2026-02-11 15:17:51 +02:00 committed by GitHub
commit 4d80dab633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 154 additions and 125 deletions

View file

@ -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});
}
}
}

View file

@ -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

View file

@ -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');