diff options
Diffstat (limited to 'packages')
m--------- | packages/wekan-iframe | 0 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/.gitignore | 1 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/LICENSE.txt | 14 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/README.md | 75 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/oidc.js | 22 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/oidc_login_button.css | 3 | ||||
-rw-r--r-- | packages/wekan_accounts-oidc/package.js | 19 | ||||
-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 |
15 files changed, 413 insertions, 0 deletions
diff --git a/packages/wekan-iframe b/packages/wekan-iframe new file mode 160000 +Subproject e105dcc9c3424beee0ff0a9db9ca543a6d4b7f8 diff --git a/packages/wekan_accounts-oidc/.gitignore b/packages/wekan_accounts-oidc/.gitignore new file mode 100644 index 00000000..5379d4c3 --- /dev/null +++ b/packages/wekan_accounts-oidc/.gitignore @@ -0,0 +1 @@ +.versions diff --git a/packages/wekan_accounts-oidc/LICENSE.txt b/packages/wekan_accounts-oidc/LICENSE.txt new file mode 100644 index 00000000..c7be3264 --- /dev/null +++ b/packages/wekan_accounts-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_accounts-oidc/README.md b/packages/wekan_accounts-oidc/README.md new file mode 100644 index 00000000..ce0b5738 --- /dev/null +++ b/packages/wekan_accounts-oidc/README.md @@ -0,0 +1,75 @@ +# salleman:accounts-oidc package + +A Meteor login service for OpenID Connect (OIDC). + +## Installation + + meteor add salleman:accounts-oidc + +## Usage + +`Meteor.loginWithOidc(options, callback)` +* `options` - object containing options, see below (optional) +* `callback` - callback function (optional) + +#### Example + +```js +Template.myTemplateName.events({ + 'click #login-button': function() { + Meteor.loginWithOidc(); + } +); +``` + + +## Options + +These options override service configuration stored in the database. + +* `loginStyle`: `redirect` or `popup` +* `redirectUrl`: Where to redirect after successful login. Only used if `loginStyle` is set to `redirect` + +## Manual Configuration Setup + +You can manually configure this package by upserting the service configuration on startup. First, add the `service-configuration` package: + + meteor add service-configuration + +### Service Configuration + +The following service configuration are available: + +* `clientId`: OIDC client identifier +* `secret`: OIDC client shared secret +* `serverUrl`: URL of the OIDC server. e.g. `https://openid.example.org:8443` +* `authorizationEndpoint`: Endpoint of the OIDC authorization service, e.g. `/oidc/authorize` +* `tokenEndpoint`: Endpoint of the OIDC token service, e.g. `/oidc/token` +* `userinfoEndpoint`: Endpoint of the OIDC userinfo service, e.g. `/oidc/userinfo` +* `idTokenWhitelistFields`: A list of fields from IDToken to be added to Meteor.user().services.oidc object + +### Project Configuration + +Then in your project: + +```js +if (Meteor.isServer) { + Meteor.startup(function () { + ServiceConfiguration.configurations.upsert( + { service: 'oidc' }, + { + $set: { + loginStyle: 'redirect', + clientId: 'my-client-id-registered-with-the-oidc-server', + secret: 'my-client-shared-secret', + serverUrl: 'https://openid.example.org', + authorizationEndpoint: '/oidc/authorize', + tokenEndpoint: '/oidc/token', + userinfoEndpoint: '/oidc/userinfo', + idTokenWhitelistFields: [] + } + } + ); + }); +} +``` diff --git a/packages/wekan_accounts-oidc/oidc.js b/packages/wekan_accounts-oidc/oidc.js new file mode 100644 index 00000000..75cd89ae --- /dev/null +++ b/packages/wekan_accounts-oidc/oidc.js @@ -0,0 +1,22 @@ +Accounts.oauth.registerService('oidc'); + +if (Meteor.isClient) { + Meteor.loginWithOidc = function(options, callback) { + // support a callback without options + if (! callback && typeof options === "function") { + callback = options; + options = null; + } + + var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); + Oidc.requestCredential(options, credentialRequestCompleteCallback); + }; +} else { + Accounts.addAutopublishFields({ + // not sure whether the OIDC api can be used from the browser, + // thus not sure if we should be sending access tokens; but we do it + // for all other oauth2 providers, and it may come in handy. + forLoggedInUser: ['services.oidc'], + forOtherUsers: ['services.oidc.id'] + }); +} diff --git a/packages/wekan_accounts-oidc/oidc_login_button.css b/packages/wekan_accounts-oidc/oidc_login_button.css new file mode 100644 index 00000000..da42120b --- /dev/null +++ b/packages/wekan_accounts-oidc/oidc_login_button.css @@ -0,0 +1,3 @@ +#login-buttons-image-oidc { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACQUlEQVQ4T5WTy2taQRTGv4kSXw0IoYIihCjFmhhfhUqW9a+o0I2LInTRRbtw05V2I9KQuuimi24KXQqChIhQQcQgGGNz0YpvMCG1yL1tGqvBZsKMIIXcjQcOcznnfL+ZOecOEUVx4/Ly8mQ6neqxhKlUKmltbc1Nut2uqJ/bEnJAkiTmEhEEgVqtViiVyjuAP70j/Pj6Htbglzu52WyGdrsNUq1Wqc1mk939+9sHPP7wTVM232g0QMrlMrXb7bIFndgcbAk3ZPP1eh2kVCrRra2tRcFoNEK1WoXf78fg3Rxsfl3H3t4e3G43dnd3wWrMZjNqtRpIsVhcAFKpFPL5PBfF43H8TDj49/2XAvb393F2dgaNRgNKKaLR6ByQz+epw+HAwcEBisUijEYjgsEg1Go1pA9ODtC/+MZFDCKKIo9FIhEIggCSy+Xozs4OYrEY2ChDoRAIIVww/ujhxdrnFTSbTX6Cfr+Pi4sLhMNhnJ6egmSzWepyuZBIJGAwGBAIBLiY2ezTI74qg2UoFIqFr6ys4OrqiveKHB4eckAmk8FgMMD29jZ8Ph8XKj4/5uu/ZyXZKXBAOp2mHo+H/0isD6zDOp0Om5ubsAuvcA+/8ffpkSygUqmApFIp6vV6+b2ZsNfrodVqYTgcwqXtwul04pfhiSzg+PgYJJlMUovFwhvIbHV1lTs70c3NDSaTCa6vr+8A2FvodDr8CmwuepPJtIDIbvdfkInPz89ZRCKFQmFjNBqdjMfjpZ6jVquV1tfX3bcYegI7CyIWlgAAAABJRU5ErkJggg=='); +} diff --git a/packages/wekan_accounts-oidc/package.js b/packages/wekan_accounts-oidc/package.js new file mode 100644 index 00000000..251fb265 --- /dev/null +++ b/packages/wekan_accounts-oidc/package.js @@ -0,0 +1,19 @@ +Package.describe({ + summary: "OpenID Connect (OIDC) for Meteor accounts", + version: "1.0.10", + name: "wekan-accounts-oidc", + git: "https://github.com/wekan/meteor-accounts-oidc.git", + +}); + +Package.onUse(function(api) { + api.use('accounts-base@1.2.0', ['client', 'server']); + // Export Accounts (etc) to packages using this one. + api.imply('accounts-base', ['client', 'server']); + api.use('accounts-oauth@1.1.0', ['client', 'server']); + api.use('wekan-oidc@1.0.10', ['client', 'server']); + + api.addFiles('oidc_login_button.css', 'client'); + + api.addFiles('oidc.js'); +}); 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..faf4a68d --- /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/meteor-accounts-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'); +}); |