summaryrefslogtreecommitdiffstats
path: root/packages/meteor-accounts-cas
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2019-04-20 15:18:33 +0300
committerLauri Ojansivu <x@xet7.org>2019-04-20 15:18:33 +0300
commit73e265d8fd050ae3daa67472b4465a5c49d68910 (patch)
tree677b233934a43d8f873e24c794ce289d85e3a9b7 /packages/meteor-accounts-cas
parent6117097a93bfb11c8bd4c87a23c44a50e22ceb87 (diff)
downloadwekan-73e265d8fd050ae3daa67472b4465a5c49d68910.tar.gz
wekan-73e265d8fd050ae3daa67472b4465a5c49d68910.tar.bz2
wekan-73e265d8fd050ae3daa67472b4465a5c49d68910.zip
Include to Wekan packages directory contents, so that meteor command would build all directly.
This also simplifies build scripts. Thanks to xet7 !
Diffstat (limited to 'packages/meteor-accounts-cas')
-rw-r--r--packages/meteor-accounts-cas/.gitignore2
-rw-r--r--packages/meteor-accounts-cas/LICENSE21
-rw-r--r--packages/meteor-accounts-cas/README.md88
-rw-r--r--packages/meteor-accounts-cas/cas_client.js112
-rw-r--r--packages/meteor-accounts-cas/cas_client_cordova.js71
-rw-r--r--packages/meteor-accounts-cas/cas_server.js281
-rw-r--r--packages/meteor-accounts-cas/package.js29
7 files changed, 604 insertions, 0 deletions
diff --git a/packages/meteor-accounts-cas/.gitignore b/packages/meteor-accounts-cas/.gitignore
new file mode 100644
index 00000000..bed7713f
--- /dev/null
+++ b/packages/meteor-accounts-cas/.gitignore
@@ -0,0 +1,2 @@
+.build*
+node_modules/
diff --git a/packages/meteor-accounts-cas/LICENSE b/packages/meteor-accounts-cas/LICENSE
new file mode 100644
index 00000000..c2d69158
--- /dev/null
+++ b/packages/meteor-accounts-cas/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2019 The Wekan Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/meteor-accounts-cas/README.md b/packages/meteor-accounts-cas/README.md
new file mode 100644
index 00000000..3e246c4f
--- /dev/null
+++ b/packages/meteor-accounts-cas/README.md
@@ -0,0 +1,88 @@
+This is a merged repository of useful forks of: atoy40:accounts-cas
+===================
+([(https://atmospherejs.com/atoy40/accounts-cas](https://atmospherejs.com/atoy40/accounts-cas))
+
+## Essential improvements by ppoulard to atoy40 and xaionaro versions
+
+* Added support of CAS attributes
+
+With this plugin, you can pick CAS attributes : https://github.com/joshchan/node-cas/wiki/CAS-Attributes
+
+Moved to Wekan GitHub org from from https://github.com/ppoulard/meteor-accounts-cas
+
+## Install
+
+```
+cd ~site
+mkdir packages
+cd packages
+git clone https://github.com/wekan/meteor-accounts-cas
+cd ~site
+meteor add wekan:accounts-cas
+```
+
+## Usage
+
+Put CAS settings in Meteor.settings (for example using METEOR_SETTINGS env or --settings) like so:
+
+If casVersion is not defined, it will assume you use CAS 1.0. (note by xaionaro: option `casVersion` seems to be just ignored in the code, ATM).
+
+Server side settings:
+
+```
+Meteor.settings = {
+ "cas": {
+ "baseUrl": "https://cas.example.com/cas",
+ "autoClose": true,
+ "validateUrl":"https://cas.example.com/cas/p3/serviceValidate",
+ "casVersion": 3.0,
+ "attributes": {
+ "debug" : true
+ }
+ },
+}
+```
+
+CAS `attributes` settings :
+
+* `attributes`: by default `{}` : all default values below will apply
+* * `debug` : by default `false` ; `true` will print to the server console the CAS attribute names to map, the CAS attributes values retrieved, if necessary the new user account created, and finally the user to use
+* * `id` : by default, the CAS user is used for the user account, but you can specified another CAS attribute
+* * `firstname` : by default `cas:givenName` ; but you can use your own CAS attribute
+* * `lastname` : by default `cas:sn` (respectively) ; but you can use your own CAS attribute
+* * `fullname` : by default unused, but if you specify your own CAS attribute, it will be used instead of the `firstname` + `lastname`
+* * `mail` : by default `cas:mail`
+
+Client side settings:
+
+```
+Meteor.settings = {
+ "public": {
+ "cas": {
+ "loginUrl": "https://cas.example.com/login",
+ "serviceParam": "service",
+ "popupWidth": 810,
+ "popupHeight": 610,
+ "popup": true,
+ }
+ }
+}
+```
+
+`proxyUrl` is not required. Setup [ROOT_URL](http://docs.meteor.com/api/core.html#Meteor-absoluteUrl) environment variable instead.
+
+Then, to start authentication, you have to call the following method from the client (for example in a click handler) :
+
+```
+Meteor.loginWithCas([callback]);
+```
+
+It must open a popup containing you CAS login form or redirect to the CAS login form (depending on "popup" setting).
+
+If popup is disabled (== false), then it's required to execute `Meteor.initCas([callback])` in `Meteor.startup` of the client side. ATM, `Meteor.initCas()` completes authentication.
+
+## Examples
+
+* [https://devel.mephi.ru/dyokunev/start-mephi-ru](https://devel.mephi.ru/dyokunev/start-mephi-ru)
+
+
diff --git a/packages/meteor-accounts-cas/cas_client.js b/packages/meteor-accounts-cas/cas_client.js
new file mode 100644
index 00000000..bd94be6b
--- /dev/null
+++ b/packages/meteor-accounts-cas/cas_client.js
@@ -0,0 +1,112 @@
+
+function addParameterToURL(url, param){
+ var urlSplit = url.split('?');
+ return url+(urlSplit.length>0 ? '?':'&') + param;
+}
+
+Meteor.initCas = function(callback) {
+ const casTokenMatch = window.location.href.match(/[?&]casToken=([^&]+)/);
+ if (casTokenMatch == null) {
+ return;
+ }
+
+ window.history.pushState('', document.title, window.location.href.replace(/([&?])casToken=[^&]+[&]?/, '$1').replace(/[?&]+$/g, ''));
+
+ Accounts.callLoginMethod({
+ methodArguments: [{ cas: { credentialToken: casTokenMatch[1] } }],
+ userCallback: function(err){
+ if (err == null) {
+ // should we do anything on success?
+ }
+ if (callback != null) {
+ callback(err);
+ }
+ }
+ });
+}
+
+Meteor.loginWithCas = function(options, callback) {
+
+ var credentialToken = Random.id();
+
+ if (!Meteor.settings.public &&
+ !Meteor.settings.public.cas &&
+ !Meteor.settings.public.cas.loginUrl) {
+ return;
+ }
+
+ var settings = Meteor.settings.public.cas;
+
+ var backURL = window.location.href.replace('#', '');
+ if (options != null && options.redirectUrl != null)
+ backURL = options.redirectUrl;
+
+ var serviceURL = addParameterToURL(backURL, 'casToken='+credentialToken);
+
+ var loginUrl = settings.loginUrl +
+ "?" + (settings.serviceParam || "service") + "=" +
+ encodeURIComponent(serviceURL)
+
+ if (settings.popup == false) {
+ window.location = loginUrl;
+ return;
+ }
+
+ var popup = openCenteredPopup(
+ loginUrl,
+ settings.width || 800,
+ settings.height || 600
+ );
+
+ var checkPopupOpen = setInterval(function() {
+ try {
+ if(popup && popup.document && popup.document.getElementById('popupCanBeClosed')) {
+ popup.close();
+ }
+ // Fix for #328 - added a second test criteria (popup.closed === undefined)
+ // to humour this Android quirk:
+ // http://code.google.com/p/android/issues/detail?id=21061
+ var popupClosed = popup.closed || popup.closed === undefined;
+ } catch (e) {
+ // For some unknown reason, IE9 (and others?) sometimes (when
+ // the popup closes too quickly?) throws "SCRIPT16386: No such
+ // interface supported" when trying to read 'popup.closed'. Try
+ // again in 100ms.
+ return;
+ }
+
+ if (popupClosed) {
+ clearInterval(checkPopupOpen);
+
+ // check auth on server.
+ Accounts.callLoginMethod({
+ methodArguments: [{ cas: { credentialToken: credentialToken } }],
+ userCallback: callback
+ });
+ }
+ }, 100);
+};
+
+var openCenteredPopup = function(url, width, height) {
+ var screenX = typeof window.screenX !== 'undefined'
+ ? window.screenX : window.screenLeft;
+ var screenY = typeof window.screenY !== 'undefined'
+ ? window.screenY : window.screenTop;
+ var outerWidth = typeof window.outerWidth !== 'undefined'
+ ? window.outerWidth : document.body.clientWidth;
+ var outerHeight = typeof window.outerHeight !== 'undefined'
+ ? window.outerHeight : (document.body.clientHeight - 22);
+ // XXX what is the 22?
+
+ // Use `outerWidth - width` and `outerHeight - height` for help in
+ // positioning the popup centered relative to the current window
+ var left = screenX + (outerWidth - width) / 2;
+ var top = screenY + (outerHeight - height) / 2;
+ var features = ('width=' + width + ',height=' + height +
+ ',left=' + left + ',top=' + top + ',scrollbars=yes');
+
+ var newwindow = window.open(url, '_blank', features);
+ if (newwindow.focus)
+ newwindow.focus();
+ return newwindow;
+};
diff --git a/packages/meteor-accounts-cas/cas_client_cordova.js b/packages/meteor-accounts-cas/cas_client_cordova.js
new file mode 100644
index 00000000..c7f95b50
--- /dev/null
+++ b/packages/meteor-accounts-cas/cas_client_cordova.js
@@ -0,0 +1,71 @@
+
+Meteor.loginWithCas = function(callback) {
+
+ var credentialToken = Random.id();
+
+ if (!Meteor.settings.public &&
+ !Meteor.settings.public.cas &&
+ !Meteor.settings.public.cas.loginUrl) {
+ return;
+ }
+
+ var settings = Meteor.settings.public.cas;
+
+ var loginUrl = settings.loginUrl +
+ "?" + (settings.service || "service") + "=" +
+ Meteor.absoluteUrl('_cas/') +
+ credentialToken;
+
+
+ var fail = function (err) {
+ Meteor._debug("Error from OAuth popup: " + JSON.stringify(err));
+ };
+
+ // When running on an android device, we sometimes see the
+ // `pageLoaded` callback fire twice for the final page in the OAuth
+ // popup, even though the page only loads once. This is maybe an
+ // Android bug or maybe something intentional about how onPageFinished
+ // works that we don't understand and isn't well-documented.
+ var oauthFinished = false;
+
+ var pageLoaded = function (event) {
+ if (oauthFinished) {
+ return;
+ }
+
+ if (event.url.indexOf(Meteor.absoluteUrl('_cas')) === 0) {
+
+ oauthFinished = true;
+
+ // On iOS, this seems to prevent "Warning: Attempt to dismiss from
+ // view controller <MainViewController: ...> while a presentation
+ // or dismiss is in progress". My guess is that the last
+ // navigation of the OAuth popup is still in progress while we try
+ // to close the popup. See
+ // https://issues.apache.org/jira/browse/CB-2285.
+ //
+ // XXX Can we make this timeout smaller?
+ setTimeout(function () {
+ popup.close();
+ // check auth on server.
+ Accounts.callLoginMethod({
+ methodArguments: [{ cas: { credentialToken: credentialToken } }],
+ userCallback: callback
+ });
+ }, 100);
+ }
+ };
+
+ var onExit = function () {
+ popup.removeEventListener('loadstop', pageLoaded);
+ popup.removeEventListener('loaderror', fail);
+ popup.removeEventListener('exit', onExit);
+ };
+
+ var popup = window.open(loginUrl, '_blank', 'location=no,hidden=no');
+ popup.addEventListener('loadstop', pageLoaded);
+ popup.addEventListener('loaderror', fail);
+ popup.addEventListener('exit', onExit);
+ popup.show();
+
+}; \ No newline at end of file
diff --git a/packages/meteor-accounts-cas/cas_server.js b/packages/meteor-accounts-cas/cas_server.js
new file mode 100644
index 00000000..15c1b174
--- /dev/null
+++ b/packages/meteor-accounts-cas/cas_server.js
@@ -0,0 +1,281 @@
+"use strict";
+
+const Fiber = Npm.require('fibers');
+const https = Npm.require('https');
+const url = Npm.require('url');
+const xmlParser = Npm.require('xml2js');
+
+// Library
+class CAS {
+ constructor(options) {
+ options = options || {};
+
+ if (!options.validate_url) {
+ throw new Error('Required CAS option `validateUrl` missing.');
+ }
+
+ if (!options.service) {
+ throw new Error('Required CAS option `service` missing.');
+ }
+
+ const cas_url = url.parse(options.validate_url);
+ if (cas_url.protocol != 'https:' ) {
+ throw new Error('Only https CAS servers are supported.');
+ } else if (!cas_url.hostname) {
+ throw new Error('Option `validateUrl` must be a valid url like: https://example.com/cas/serviceValidate');
+ } else {
+ this.hostname = cas_url.host;
+ this.port = 443;// Should be 443 for https
+ this.validate_path = cas_url.pathname;
+ }
+
+ this.service = options.service;
+ }
+
+ validate(ticket, callback) {
+ const httparams = {
+ host: this.hostname,
+ port: this.port,
+ path: url.format({
+ pathname: this.validate_path,
+ query: {ticket: ticket, service: this.service},
+ }),
+ };
+
+ https.get(httparams, (res) => {
+ res.on('error', (e) => {
+ console.log('error' + e);
+ callback(e);
+ });
+
+ // Read result
+ res.setEncoding('utf8');
+ let response = '';
+ res.on('data', (chunk) => {
+ response += chunk;
+ });
+
+ res.on('end', (error) => {
+ if (error) {
+ console.log('error callback');
+ console.log(error);
+ callback(undefined, false);
+ } else {
+ xmlParser.parseString(response, (err, result) => {
+ if (err) {
+ console.log('Bad response format.');
+ callback({message: 'Bad response format. XML could not parse it'});
+ } else {
+ if (result['cas:serviceResponse'] == null) {
+ console.log('Empty response.');
+ callback({message: 'Empty response.'});
+ }
+ if (result['cas:serviceResponse']['cas:authenticationSuccess']) {
+ var userData = {
+ id: result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:user'][0].toLowerCase(),
+ }
+ const attributes = result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:attributes'][0];
+ for (var fieldName in attributes) {
+ userData[fieldName] = attributes[fieldName][0];
+ };
+ callback(undefined, true, userData);
+ } else {
+ callback(undefined, false);
+ }
+ }
+ });
+ }
+ });
+ });
+ }
+}
+////// END OF CAS MODULE
+
+let _casCredentialTokens = {};
+let _userData = {};
+
+//RoutePolicy.declare('/_cas/', 'network');
+
+// Listen to incoming OAuth http requests
+WebApp.connectHandlers.use((req, res, next) => {
+ // Need to create a Fiber since we're using synchronous http calls and nothing
+ // else is wrapping this in a fiber automatically
+
+ Fiber(() => {
+ middleware(req, res, next);
+ }).run();
+});
+
+const middleware = (req, res, next) => {
+ // Make sure to catch any exceptions because otherwise we'd crash
+ // the runner
+ try {
+ urlParsed = url.parse(req.url, true);
+
+ // Getting the ticket (if it's defined in GET-params)
+ // If no ticket, then request will continue down the default
+ // middlewares.
+ const query = urlParsed.query;
+ if (query == null) {
+ next();
+ return;
+ }
+ const ticket = query.ticket;
+ if (ticket == null) {
+ next();
+ return;
+ }
+
+ const serviceUrl = Meteor.absoluteUrl(urlParsed.href.replace(/^\//g, '')).replace(/([&?])ticket=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');
+ const redirectUrl = serviceUrl;//.replace(/([&?])casToken=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');
+
+ // get auth token
+ const credentialToken = query.casToken;
+ if (!credentialToken) {
+ end(res, redirectUrl);
+ return;
+ }
+
+ // validate ticket
+ casValidate(req, ticket, credentialToken, serviceUrl, () => {
+ end(res, redirectUrl);
+ });
+
+ } catch (err) {
+ console.log("account-cas: unexpected error : " + err.message);
+ end(res, redirectUrl);
+ }
+};
+
+const casValidate = (req, ticket, token, service, callback) => {
+ // get configuration
+ if (!Meteor.settings.cas/* || !Meteor.settings.cas.validate*/) {
+ throw new Error('accounts-cas: unable to get configuration.');
+ }
+
+ const cas = new CAS({
+ validate_url: Meteor.settings.cas.validateUrl,
+ service: service,
+ version: Meteor.settings.cas.casVersion
+ });
+
+ cas.validate(ticket, (err, status, userData) => {
+ if (err) {
+ console.log("accounts-cas: error when trying to validate " + err);
+ console.log(err);
+ } else {
+ if (status) {
+ console.log(`accounts-cas: user validated ${userData.id}
+ (${JSON.stringify(userData)})`);
+ _casCredentialTokens[token] = { id: userData.id };
+ _userData = userData;
+ } else {
+ console.log("accounts-cas: unable to validate " + ticket);
+ }
+ }
+ callback();
+ });
+
+ return;
+};
+
+/*
+ * Register a server-side login handle.
+ * It is call after Accounts.callLoginMethod() is call from client.
+ */
+ Accounts.registerLoginHandler((options) => {
+ if (!options.cas)
+ return undefined;
+
+ if (!_hasCredential(options.cas.credentialToken)) {
+ throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
+ 'no matching login attempt found');
+ }
+
+ const result = _retrieveCredential(options.cas.credentialToken);
+
+ const attrs = Meteor.settings.cas.attributes || {};
+ // CAS keys
+ const fn = attrs.firstname || 'cas:givenName';
+ const ln = attrs.lastname || 'cas:sn';
+ const full = attrs.fullname;
+ const mail = attrs.mail || 'cas:mail'; // or 'email'
+ const uid = attrs.id || 'id';
+ if (attrs.debug) {
+ if (full) {
+ console.log(`CAS fields : id:"${uid}", fullname:"${full}", mail:"${mail}"`);
+ } else {
+ console.log(`CAS fields : id:"${uid}", firstname:"${fn}", lastname:"${ln}", mail:"${mail}"`);
+ }
+ }
+ const name = full ? _userData[full] : _userData[fn] + ' ' + _userData[ln];
+ // https://docs.meteor.com/api/accounts.html#Meteor-users
+ options = {
+ // _id: Meteor.userId()
+ username: _userData[uid], // Unique name
+ emails: [
+ { address: _userData[mail], verified: true }
+ ],
+ createdAt: new Date(),
+ profile: {
+ // The profile is writable by the user by default.
+ name: name,
+ fullname : name,
+ email : _userData[mail]
+ },
+ active: true,
+ globalRoles: ['user']
+ };
+ if (attrs.debug) {
+ console.log(`CAS response : ${JSON.stringify(result)}`);
+ }
+ let user = Meteor.users.findOne({ 'username': options.username });
+ if (! user) {
+ if (attrs.debug) {
+ console.log(`Creating user account ${JSON.stringify(options)}`);
+ }
+ const userId = Accounts.insertUserDoc({}, options);
+ user = Meteor.users.findOne(userId);
+ }
+ if (attrs.debug) {
+ console.log(`Using user account ${JSON.stringify(user)}`);
+ }
+ return { userId: user._id };
+});
+
+const _hasCredential = (credentialToken) => {
+ return _.has(_casCredentialTokens, credentialToken);
+}
+
+/*
+ * Retrieve token and delete it to avoid replaying it.
+ */
+const _retrieveCredential = (credentialToken) => {
+ const result = _casCredentialTokens[credentialToken];
+ delete _casCredentialTokens[credentialToken];
+ return result;
+}
+
+const closePopup = (res) => {
+ if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {
+ return;
+ }
+ res.writeHead(200, {'Content-Type': 'text/html'});
+ const content = '<html><body><div id="popupCanBeClosed"></div></body></html>';
+ res.end(content, 'utf-8');
+}
+
+const redirect = (res, whereTo) => {
+ res.writeHead(302, {'Location': whereTo});
+ const content = '<html><head><meta http-equiv="refresh" content="0; url='+whereTo+'" /></head><body>Redirection to <a href='+whereTo+'>'+whereTo+'</a></body></html>';
+ res.end(content, 'utf-8');
+ return
+}
+
+const end = (res, whereTo) => {
+ if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {
+ redirect(res, whereTo);
+ } else {
+ closePopup(res);
+ }
+}
diff --git a/packages/meteor-accounts-cas/package.js b/packages/meteor-accounts-cas/package.js
new file mode 100644
index 00000000..670fe687
--- /dev/null
+++ b/packages/meteor-accounts-cas/package.js
@@ -0,0 +1,29 @@
+Package.describe({
+ summary: "CAS support for accounts",
+ version: "0.1.0",
+ name: "wekan:accounts-cas",
+ git: "https://github.com/wekan/meteor-accounts-cas"
+});
+
+Package.onUse(function(api) {
+ api.versionsFrom('METEOR@1.3.5.1');
+ api.use('routepolicy', 'server');
+ api.use('webapp', 'server');
+ api.use('accounts-base', ['client', 'server']);
+ // Export Accounts (etc) to packages using this one.
+ api.imply('accounts-base', ['client', 'server']);
+ api.use('underscore');
+ api.add_files('cas_client.js', 'web.browser');
+ api.add_files('cas_client_cordova.js', 'web.cordova');
+ api.add_files('cas_server.js', 'server');
+
+});
+
+Npm.depends({
+ xml2js: "0.4.17",
+ cas: "https://github.com/anrizal/node-cas/tarball/2baed530842e7a437f8f71b9346bcac8e84773cc"
+});
+
+Cordova.depends({
+ 'cordova-plugin-inappbrowser': '1.2.0'
+});