diff options
Diffstat (limited to 'packages/wekan-oidc')
-rw-r--r-- | packages/wekan-oidc/.gitignore | 1 | ||||
-rw-r--r-- | packages/wekan-oidc/LICENSE.txt | 14 | ||||
-rw-r--r-- | packages/wekan-oidc/README.md | 7 | ||||
-rw-r--r-- | packages/wekan-oidc/oidc_client.js | 68 | ||||
-rw-r--r-- | packages/wekan-oidc/oidc_configure.html | 6 | ||||
-rw-r--r-- | packages/wekan-oidc/oidc_configure.js | 17 | ||||
-rw-r--r-- | packages/wekan-oidc/oidc_server.js | 143 | ||||
-rw-r--r-- | packages/wekan-oidc/package.js | 23 |
8 files changed, 279 insertions, 0 deletions
diff --git a/packages/wekan-oidc/.gitignore b/packages/wekan-oidc/.gitignore new file mode 100644 index 00000000..5379d4c3 --- /dev/null +++ b/packages/wekan-oidc/.gitignore @@ -0,0 +1 @@ +.versions diff --git a/packages/wekan-oidc/LICENSE.txt b/packages/wekan-oidc/LICENSE.txt new file mode 100644 index 00000000..c7be3264 --- /dev/null +++ b/packages/wekan-oidc/LICENSE.txt @@ -0,0 +1,14 @@ +Copyright (C) 2016 SWITCH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/packages/wekan-oidc/README.md b/packages/wekan-oidc/README.md new file mode 100644 index 00000000..8948971c --- /dev/null +++ b/packages/wekan-oidc/README.md @@ -0,0 +1,7 @@ +# salleman:oidc package + +A Meteor implementation of OpenID Connect Login flow + +## Usage and Documentation + +Look at the `salleman:accounts-oidc` package for the documentation about using OpenID Connect with Meteor. diff --git a/packages/wekan-oidc/oidc_client.js b/packages/wekan-oidc/oidc_client.js new file mode 100644 index 00000000..744bd841 --- /dev/null +++ b/packages/wekan-oidc/oidc_client.js @@ -0,0 +1,68 @@ +Oidc = {}; + +// Request OpenID Connect credentials for the user +// @param options {optional} +// @param credentialRequestCompleteCallback {Function} Callback function to call on +// completion. Takes one argument, credentialToken on success, or Error on +// error. +Oidc.requestCredential = function (options, credentialRequestCompleteCallback) { + // support both (options, callback) and (callback). + if (!credentialRequestCompleteCallback && typeof options === 'function') { + credentialRequestCompleteCallback = options; + options = {}; + } + + var config = ServiceConfiguration.configurations.findOne({service: 'oidc'}); + if (!config) { + credentialRequestCompleteCallback && credentialRequestCompleteCallback( + new ServiceConfiguration.ConfigError('Service oidc not configured.')); + return; + } + + var credentialToken = Random.secret(); + var loginStyle = OAuth._loginStyle('oidc', config, options); + var scope = config.requestPermissions || ['openid', 'profile', 'email']; + + // options + options = options || {}; + options.client_id = config.clientId; + options.response_type = options.response_type || 'code'; + options.redirect_uri = OAuth._redirectUri('oidc', config); + options.state = OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl); + options.scope = scope.join(' '); + + if (config.loginStyle && config.loginStyle == 'popup') { + options.display = 'popup'; + } + + var loginUrl = config.serverUrl + config.authorizationEndpoint; + // check if the loginUrl already contains a "?" + var first = loginUrl.indexOf('?') === -1; + for (var k in options) { + if (first) { + loginUrl += '?'; + first = false; + } + else { + loginUrl += '&' + } + loginUrl += encodeURIComponent(k) + '=' + encodeURIComponent(options[k]); + } + + //console.log('XXX: loginURL: ' + loginUrl) + + options.popupOptions = options.popupOptions || {}; + var popupOptions = { + width: options.popupOptions.width || 320, + height: options.popupOptions.height || 450 + }; + + OAuth.launchLogin({ + loginService: 'oidc', + loginStyle: loginStyle, + loginUrl: loginUrl, + credentialRequestCompleteCallback: credentialRequestCompleteCallback, + credentialToken: credentialToken, + popupOptions: popupOptions, + }); +}; diff --git a/packages/wekan-oidc/oidc_configure.html b/packages/wekan-oidc/oidc_configure.html new file mode 100644 index 00000000..49282fc1 --- /dev/null +++ b/packages/wekan-oidc/oidc_configure.html @@ -0,0 +1,6 @@ +<template name="configureLoginServiceDialogForOidc"> + <p> + You'll need to create an OpenID Connect client configuration with your provider. + Set App Callbacks URLs to: <span class="url">{{siteUrl}}_oauth/oidc</span> + </p> +</template> diff --git a/packages/wekan-oidc/oidc_configure.js b/packages/wekan-oidc/oidc_configure.js new file mode 100644 index 00000000..5eedaa04 --- /dev/null +++ b/packages/wekan-oidc/oidc_configure.js @@ -0,0 +1,17 @@ +Template.configureLoginServiceDialogForOidc.helpers({ + siteUrl: function () { + return Meteor.absoluteUrl(); + } +}); + +Template.configureLoginServiceDialogForOidc.fields = function () { + return [ + { property: 'clientId', label: 'Client ID'}, + { property: 'secret', label: 'Client Secret'}, + { property: 'serverUrl', label: 'OIDC Server URL'}, + { property: 'authorizationEndpoint', label: 'Authorization Endpoint'}, + { property: 'tokenEndpoint', label: 'Token Endpoint'}, + { property: 'userinfoEndpoint', label: 'Userinfo Endpoint'}, + { property: 'idTokenWhitelistFields', label: 'Id Token Fields'} + ]; +}; diff --git a/packages/wekan-oidc/oidc_server.js b/packages/wekan-oidc/oidc_server.js new file mode 100644 index 00000000..fb948c52 --- /dev/null +++ b/packages/wekan-oidc/oidc_server.js @@ -0,0 +1,143 @@ +Oidc = {}; + +OAuth.registerService('oidc', 2, null, function (query) { + + var debug = process.env.DEBUG || false; + 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 userinfo = getUserInfo(accessToken); + if (debug) console.log('XXX: userinfo:', userinfo); + + var serviceData = {}; + 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; + serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email]; + + 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); + + var profile = {}; + profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP] || userinfo[displayName]; + profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email]; + if (debug) console.log('XXX: profile:', profile); + + return { + serviceData: serviceData, + options: { profile: profile } + }; +}); + +var userAgent = "Meteor"; +if (Meteor.release) { + userAgent += "/" + Meteor.release; +} + +var getToken = function (query) { + var debug = process.env.DEBUG || false; + var config = getConfiguration(); + var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint; + var response; + + try { + response = HTTP.post( + serverTokenEndpoint, + { + 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 + } + } + ); + } 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; + } +}; + +var getUserInfo = function (accessToken) { + var debug = process.env.DEBUG || false; + 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 { + response = HTTP.get( + serverUserinfoEndpoint, + { + headers: { + "User-Agent": userAgent, + "Authorization": "Bearer " + accessToken + } + } + ); + } 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' }); + if (!config) { + throw new ServiceConfiguration.ConfigError('Service oidc not configured.'); + } + return config; +}; + +var getTokenContent = function (token) { + var content = null; + if (token) { + try { + var parts = token.split('.'); + var header = JSON.parse(new Buffer(parts[0], 'base64').toString()); + content = JSON.parse(new Buffer(parts[1], 'base64').toString()); + var signature = new Buffer(parts[2], 'base64'); + var signed = parts[0] + '.' + parts[1]; + } catch (err) { + this.content = { + exp: 0 + }; + } + } + return content; +} + +Oidc.retrieveCredential = function (credentialToken, credentialSecret) { + return OAuth.retrieveCredential(credentialToken, credentialSecret); +}; diff --git a/packages/wekan-oidc/package.js b/packages/wekan-oidc/package.js new file mode 100644 index 00000000..273ef612 --- /dev/null +++ b/packages/wekan-oidc/package.js @@ -0,0 +1,23 @@ +Package.describe({ + summary: "OpenID Connect (OIDC) flow for Meteor", + version: "1.0.12", + name: "wekan-oidc", + git: "https://github.com/wekan/wekan-oidc.git", +}); + +Package.onUse(function(api) { + api.use('oauth2@1.1.0', ['client', 'server']); + api.use('oauth@1.1.0', ['client', 'server']); + api.use('http@1.1.0', ['server']); + api.use('underscore@1.0.0', 'client'); + api.use('templating@1.1.0', 'client'); + api.use('random@1.0.0', 'client'); + api.use('service-configuration@1.0.0', ['client', 'server']); + + api.export('Oidc'); + + api.addFiles(['oidc_configure.html', 'oidc_configure.js'], 'client'); + + api.addFiles('oidc_server.js', 'server'); + api.addFiles('oidc_client.js', 'client'); +}); |