From 025e43fc1bcb303d48b8db342f1ed5d632f6a0cd Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Thu, 17 Nov 2016 16:54:27 +0100 Subject: [PATCH 01/13] Prototype SAML integration --- lib/config.yml | 7 ++++++- lib/controllers/index.js | 3 ++- lib/controllers/initiate-saml.js | 22 ++++++++++++++++++++++ lib/controllers/login.js | 6 +++++- lib/helpers/get-form-view-model.js | 4 +++- lib/helpers/index.js | 3 ++- lib/helpers/saml.js | 10 ++++++++++ lib/stormpath.js | 4 ++++ lib/views/login.jade | 5 ++++- lib/views/saml_login_form.jade | 7 +++++++ 10 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 lib/controllers/initiate-saml.js create mode 100644 lib/helpers/saml.js create mode 100644 lib/views/saml_login_form.jade diff --git a/lib/config.yml b/lib/config.yml index f25ab415..389cb043 100644 --- a/lib/config.yml +++ b/lib/config.yml @@ -205,6 +205,11 @@ web: uri: "/callbacks/linkedin" scope: "r_basicprofile r_emailaddress" + # SAML stuff + saml: + enabled: true + uri: "/initiate-saml-auth" + # The /me route is for front-end applications, it returns a JSON object with # the current user object. The developer can opt-in to expanding account # resources on this enpdoint. @@ -236,4 +241,4 @@ web: view: null unauthorized: - view: "unauthorized" \ No newline at end of file + view: "unauthorized" diff --git a/lib/controllers/index.js b/lib/controllers/index.js index a8235a59..38405bd9 100644 --- a/lib/controllers/index.js +++ b/lib/controllers/index.js @@ -14,5 +14,6 @@ module.exports = { login: require('./login'), logout: require('./logout'), register: require('./register'), - verifyEmail: require('./verify-email') + verifyEmail: require('./verify-email'), + initiateSaml: require('./initiate-saml') }; diff --git a/lib/controllers/initiate-saml.js b/lib/controllers/initiate-saml.js new file mode 100644 index 00000000..a5647bea --- /dev/null +++ b/lib/controllers/initiate-saml.js @@ -0,0 +1,22 @@ +'use strict'; + +var stormpath = require('stormpath'); + +module.exports = function(req, res) { + var application = req.app.get('stormpathApplication'); + var builder = new stormpath.SamlIdpUrlBuilder(application); + + builder.build(function(err, url) { + if (err) { + throw err; + } + + res.writeHead(302, { + 'Cache-Control': 'no-store', + 'Location': url, + 'Pragma': 'no-cache' + }); + + res.end(); + }); +}; diff --git a/lib/controllers/login.js b/lib/controllers/login.js index 7c8d5e9b..0426b1a0 100644 --- a/lib/controllers/login.js +++ b/lib/controllers/login.js @@ -21,6 +21,7 @@ var oauth = require('../oauth'); */ module.exports = function (req, res, next) { var config = req.app.get('stormpathConfig'); + var application = req.app.get('stormpathApplication'); res.locals.status = req.query.status; @@ -76,6 +77,7 @@ module.exports = function (req, res, next) { var view = config.web.login.view; var oauthStateToken = oauth.common.resolveStateToken(req, res); var formActionUri = (config.web.login.uri + (nextUri ? ('?next=' + nextUri) : '')); + var hasSamlProvider = config.saml.enabled; var hasSocialProviders = _.some(config.web.social, function (socialProvider) { return socialProvider.enabled; @@ -85,7 +87,9 @@ module.exports = function (req, res, next) { form: form, formActionUri: formActionUri, oauthStateToken: oauthStateToken, - hasSocialProviders: hasSocialProviders + hasSocialProviders: hasSocialProviders, + hasSamlProvider: hasSamlProvider, + initiateSamlAuth: helpers.initiateSamlAuth }); helpers.render(req, res, view, options); diff --git a/lib/helpers/get-form-view-model.js b/lib/helpers/get-form-view-model.js index b0771ae5..a1a0683d 100644 --- a/lib/helpers/get-form-view-model.js +++ b/lib/helpers/get-form-view-model.js @@ -158,7 +158,9 @@ function getAccountStoreModel(accountStore) { href: provider.href, providerId: provider.providerId, callbackUri: provider.callbackUri, - clientId: provider.clientId + clientId: provider.clientId, + ssoLoginUrl: provider.ssoLoginUrl, + ssoLogoutUrl: provider.ssoLogoutUrl } }; } diff --git a/lib/helpers/index.js b/lib/helpers/index.js index 69957ce7..8e9e19d6 100644 --- a/lib/helpers/index.js +++ b/lib/helpers/index.js @@ -24,5 +24,6 @@ module.exports = { xsrfValidator: require('./xsrf-validator'), revokeToken: require('./revoke-token'), getFormViewModel: require('./get-form-view-model').default, - strippedAccountResponse: require('./stripped-account-response') + strippedAccountResponse: require('./stripped-account-response'), + initiateSamlAuth: require('./saml') }; diff --git a/lib/helpers/saml.js b/lib/helpers/saml.js new file mode 100644 index 00000000..d49ccac7 --- /dev/null +++ b/lib/helpers/saml.js @@ -0,0 +1,10 @@ +'use strict'; + +var stormpath = require('stormpath'); +var SamlIdpUrlBuilder = stormpath.SamlIdpUrlBuilder; + +module.exports = function initiateSamlAuth(application, callback) { + var builder = new SamlIdpUrlBuilder(application); + + builder.build(callback); +}; diff --git a/lib/stormpath.js b/lib/stormpath.js index 75e1c207..91383533 100644 --- a/lib/stormpath.js +++ b/lib/stormpath.js @@ -211,6 +211,10 @@ module.exports.init = function (app, opts) { router.all(web.oauth2.uri, bodyParser.form(), stormpathMiddleware, controllers.getToken); } + if (web.saml.enabled) { + addGetRoute(web.saml.uri, controllers.initiateSaml); + } + client.getApplication(config.application.href, function (err, application) { if (err) { throw new Error('Cannot fetch application ' + config.application.href); diff --git a/lib/views/login.jade b/lib/views/login.jade index 0047e7a3..279c48f3 100644 --- a/lib/views/login.jade +++ b/lib/views/login.jade @@ -5,6 +5,7 @@ block vars - var description = 'Log into your account!' - var bodytag = 'login' - var socialProviders = stormpathConfig.web.social + - var samlProvider = stormpathConfig.saml - var loginFields = stormpathConfig.web.login.form.fields block body @@ -98,7 +99,7 @@ block body div button.login.btn.btn-login.btn-sp-green(type='submit') Log In - if hasSocialProviders + if hasSocialProviders || hasSamlProvider .social-area.col-xs-12.col-sm-4 .header   label Easy 1-click login: @@ -110,6 +111,8 @@ block body include linkedin_login_form.jade if socialProviders.github && socialProviders.github.enabled include github_login_form.jade + if samlProvider && samlProvider.enabled + include saml_login_form.jade if stormpathConfig.web.verifyEmail.enabled a.forgot(style="float:left", href="#{stormpathConfig.web.verifyEmail.uri}") Resend Verification Email? diff --git a/lib/views/saml_login_form.jade b/lib/views/saml_login_form.jade new file mode 100644 index 00000000..38dadae3 --- /dev/null +++ b/lib/views/saml_login_form.jade @@ -0,0 +1,7 @@ +button.btn.btn-saml(onclick='samlLogin()') SAML +script(type='text/javascript'). + function samlLogin() { + var url = '#{stormpathConfig.web.saml.uri}'; + console.log('going to', url); + window.location = url; + } From cedea979dc5d3a6a76f3f4cb1af956b041b21bc8 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Fri, 18 Nov 2016 14:47:05 +0100 Subject: [PATCH 02/13] Update SAML, start work on SAML response verification --- lib/config.yml | 2 + lib/controllers/index.js | 3 +- lib/controllers/initiate-saml.js | 8 +- lib/controllers/verify-saml.js | 129 +++++++++++++++++++++++++++++++ lib/stormpath.js | 1 + 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 lib/controllers/verify-saml.js diff --git a/lib/config.yml b/lib/config.yml index 389cb043..6ec0954a 100644 --- a/lib/config.yml +++ b/lib/config.yml @@ -209,6 +209,8 @@ web: saml: enabled: true uri: "/initiate-saml-auth" + verifyUri: "/verify-saml" + nextUri: "/" # The /me route is for front-end applications, it returns a JSON object with # the current user object. The developer can opt-in to expanding account diff --git a/lib/controllers/index.js b/lib/controllers/index.js index 38405bd9..c939468e 100644 --- a/lib/controllers/index.js +++ b/lib/controllers/index.js @@ -15,5 +15,6 @@ module.exports = { logout: require('./logout'), register: require('./register'), verifyEmail: require('./verify-email'), - initiateSaml: require('./initiate-saml') + initiateSaml: require('./initiate-saml'), + verifySaml: require('./verify-saml') }; diff --git a/lib/controllers/initiate-saml.js b/lib/controllers/initiate-saml.js index a5647bea..50272522 100644 --- a/lib/controllers/initiate-saml.js +++ b/lib/controllers/initiate-saml.js @@ -5,8 +5,14 @@ var stormpath = require('stormpath'); module.exports = function(req, res) { var application = req.app.get('stormpathApplication'); var builder = new stormpath.SamlIdpUrlBuilder(application); + var config = req.app.get('stormpathConfig'); + var cbUri = req.protocol + '://' + req.get('host') + config.web.saml.verifyUri; - builder.build(function(err, url) { + var samlOptions = { + cb_uri: cbUri + }; + + builder.build(samlOptions, function(err, url) { if (err) { throw err; } diff --git a/lib/controllers/verify-saml.js b/lib/controllers/verify-saml.js new file mode 100644 index 00000000..69946b1d --- /dev/null +++ b/lib/controllers/verify-saml.js @@ -0,0 +1,129 @@ +'use strict'; + +var url = require('url'); +var nJwt = require('njwt'); +var stormpath = require('stormpath'); + +var helpers = require('../helpers'); +var middleware = require('../middleware'); + +/** + * This controller handles a Stormpath SAML authentication. Once a user is + * authenticated, they'll be returned to the site. + * + * @method + * + * @param {Object} req - The http request. + * @param {Object} res - The http response. + */ +module.exports = function (req, res) { + var application = req.app.get('stormpathApplication'); + var config = req.app.get('stormpathConfig'); + var logger = req.app.get('stormpathLogger'); + + var params = req.query || {}; + var stormpathToken = params.jwtResponse || ''; + var assertionAuthenticator = new stormpath.StormpathAssertionAuthenticator(application); + + logger.info('Token: ' + stormpathToken); + + try { + logger.info(nJwt.verify(stormpathToken, config.client.apiKey.secret)); + } catch (e) { + logger.info('Well, this failed...'); + } + + assertionAuthenticator.authenticate(stormpathToken, function (err) { + if (err) { + logger.info('During a SAML login attempt, we were unable to verify the JWT response.'); + return helpers.writeJsonError(res, err); + } + + var parsedToken = nJwt.verify(stormpathToken, config.client.apiKey.secret); + var tokenStatus = parsedToken.body.status; + + function redirectNext() { + var nextUri = config.web.saml.nextUri; + var nextQueryPath = url.parse(params.next || '').path; + res.redirect(302, nextQueryPath || nextUri); + } + + function authenticateToken(callback) { + var stormpathTokenAuthenticator = new stormpath.OAuthStormpathTokenAuthenticator(application); + + stormpathTokenAuthenticator.authenticate({ stormpath_token: stormpathToken }, function (err, authenticationResult) { + if (err) { + logger.info('During a SAML login attempt, we were unable to create a Stormpath session.'); + return helpers.writeJsonError(res, err); + } + + authenticationResult.getAccount(function (err, account) { + if (err) { + logger.info('During a SAML login attempt, we were unable to retrieve an account from the authentication result.'); + return helpers.writeJsonError(res, err); + } + + helpers.expandAccount(account, config.expand, logger, function (err, expandedAccount) { + if (err) { + logger.info('During a SAML login attempt, we were unable to expand the Stormpath account.'); + return helpers.writeJsonError(res, err); + } + + helpers.createSession(authenticationResult, expandedAccount, req, res); + + callback(null, expandedAccount); + }); + }); + }); + } + + function handleAuthRequest(type, callback) { + var handler; + + switch (type) { + case 'registration': + handler = config.postRegistrationHandler; + break; + case 'login': + handler = config.postLoginHandler; + break; + default: + return callback(new Error('Invalid authentication request type: ' + type)); + } + + if (handler) { + authenticateToken(function (err, expandedAccount) { + if (err) { + return callback(err); + } + handler(expandedAccount, req, res, callback); + }); + } else { + authenticateToken(callback); + } + } + + switch (tokenStatus) { + case 'REGISTERED': + if (!config.web.register.autoLogin) { + return redirectNext(); + } + handleAuthRequest('registration', redirectNext); + break; + + case 'AUTHENTICATED': + handleAuthRequest('login', redirectNext); + break; + + case 'LOGOUT': + middleware.revokeTokens(req, res); + middleware.deleteCookies(req, res); + redirectNext(); + break; + + default: + res.status(500).end('Unknown ID site result status: ' + tokenStatus); + break; + } + }); +}; diff --git a/lib/stormpath.js b/lib/stormpath.js index 91383533..9ccfcce6 100644 --- a/lib/stormpath.js +++ b/lib/stormpath.js @@ -213,6 +213,7 @@ module.exports.init = function (app, opts) { if (web.saml.enabled) { addGetRoute(web.saml.uri, controllers.initiateSaml); + addGetRoute(web.saml.verifyUri, controllers.verifySaml); } client.getApplication(config.application.href, function (err, application) { From 9f452f81ea0944658a6d371b41af26c6a4e8fae1 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 15:20:04 +0100 Subject: [PATCH 03/13] Update SAML, add and test logout --- lib/controllers/initiate-saml.js | 46 +++++++++++++++++++------------- lib/controllers/verify-saml.js | 12 ++------- lib/stormpath.js | 6 ++++- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/controllers/initiate-saml.js b/lib/controllers/initiate-saml.js index 50272522..56a120f1 100644 --- a/lib/controllers/initiate-saml.js +++ b/lib/controllers/initiate-saml.js @@ -2,27 +2,35 @@ var stormpath = require('stormpath'); -module.exports = function(req, res) { - var application = req.app.get('stormpathApplication'); - var builder = new stormpath.SamlIdpUrlBuilder(application); - var config = req.app.get('stormpathConfig'); - var cbUri = req.protocol + '://' + req.get('host') + config.web.saml.verifyUri; - - var samlOptions = { - cb_uri: cbUri - }; +module.exports = function(options) { + return function(req, res) { + var application = req.app.get('stormpathApplication'); + var builder = new stormpath.SamlIdpUrlBuilder(application); + var config = req.app.get('stormpathConfig'); + var cbUri = req.protocol + '://' + req.get('host') + config.web.saml.verifyUri; + + var samlOptions = { + cb_uri: cbUri + }; - builder.build(samlOptions, function(err, url) { - if (err) { - throw err; + options = options || {}; + + if (options.logout) { + samlOptions.state = 'LOGOUT'; } - res.writeHead(302, { - 'Cache-Control': 'no-store', - 'Location': url, - 'Pragma': 'no-cache' - }); + builder.build(samlOptions, function(err, url) { + if (err) { + throw err; + } - res.end(); - }); + res.writeHead(302, { + 'Cache-Control': 'no-store', + 'Location': url, + 'Pragma': 'no-cache' + }); + + res.end(); + }); + }; }; diff --git a/lib/controllers/verify-saml.js b/lib/controllers/verify-saml.js index 69946b1d..1941dfa7 100644 --- a/lib/controllers/verify-saml.js +++ b/lib/controllers/verify-saml.js @@ -25,14 +25,6 @@ module.exports = function (req, res) { var stormpathToken = params.jwtResponse || ''; var assertionAuthenticator = new stormpath.StormpathAssertionAuthenticator(application); - logger.info('Token: ' + stormpathToken); - - try { - logger.info(nJwt.verify(stormpathToken, config.client.apiKey.secret)); - } catch (e) { - logger.info('Well, this failed...'); - } - assertionAuthenticator.authenticate(stormpathToken, function (err) { if (err) { logger.info('During a SAML login attempt, we were unable to verify the JWT response.'); @@ -40,7 +32,7 @@ module.exports = function (req, res) { } var parsedToken = nJwt.verify(stormpathToken, config.client.apiKey.secret); - var tokenStatus = parsedToken.body.status; + var tokenStatus = parsedToken.body.state || parsedToken.body.status; function redirectNext() { var nextUri = config.web.saml.nextUri; @@ -122,7 +114,7 @@ module.exports = function (req, res) { break; default: - res.status(500).end('Unknown ID site result status: ' + tokenStatus); + res.status(500).end('Unknown SAML result status: ' + tokenStatus); break; } }); diff --git a/lib/stormpath.js b/lib/stormpath.js index 9ccfcce6..644701a9 100644 --- a/lib/stormpath.js +++ b/lib/stormpath.js @@ -181,6 +181,10 @@ module.exports.init = function (app, opts) { } if (web.logout.enabled) { + if (web.saml.enabled) { + addGetRoute(web.logout.uri, controllers.initiateSaml({logout: true})); + } + addPostRoute(web.logout.uri, controllers.logout); } @@ -212,7 +216,7 @@ module.exports.init = function (app, opts) { } if (web.saml.enabled) { - addGetRoute(web.saml.uri, controllers.initiateSaml); + addGetRoute(web.saml.uri, controllers.initiateSaml()); addGetRoute(web.saml.verifyUri, controllers.verifySaml); } From bbcd6365365bdfa9a79242890c3e2baf4afa07c7 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 16:05:27 +0100 Subject: [PATCH 04/13] Cleanup --- lib/controllers/index.js | 4 +-- lib/controllers/initiate-saml.js | 36 ------------------- lib/controllers/saml-redirect.js | 30 ++++++++++++++++ .../{verify-saml.js => saml-verify.js} | 0 lib/stormpath.js | 8 ++--- 5 files changed, 34 insertions(+), 44 deletions(-) delete mode 100644 lib/controllers/initiate-saml.js create mode 100644 lib/controllers/saml-redirect.js rename lib/controllers/{verify-saml.js => saml-verify.js} (100%) diff --git a/lib/controllers/index.js b/lib/controllers/index.js index c939468e..dfb6bbd4 100644 --- a/lib/controllers/index.js +++ b/lib/controllers/index.js @@ -15,6 +15,6 @@ module.exports = { logout: require('./logout'), register: require('./register'), verifyEmail: require('./verify-email'), - initiateSaml: require('./initiate-saml'), - verifySaml: require('./verify-saml') + samlRedirect: require('./saml-redirect'), + samlVerify: require('./saml-verify') }; diff --git a/lib/controllers/initiate-saml.js b/lib/controllers/initiate-saml.js deleted file mode 100644 index 56a120f1..00000000 --- a/lib/controllers/initiate-saml.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var stormpath = require('stormpath'); - -module.exports = function(options) { - return function(req, res) { - var application = req.app.get('stormpathApplication'); - var builder = new stormpath.SamlIdpUrlBuilder(application); - var config = req.app.get('stormpathConfig'); - var cbUri = req.protocol + '://' + req.get('host') + config.web.saml.verifyUri; - - var samlOptions = { - cb_uri: cbUri - }; - - options = options || {}; - - if (options.logout) { - samlOptions.state = 'LOGOUT'; - } - - builder.build(samlOptions, function(err, url) { - if (err) { - throw err; - } - - res.writeHead(302, { - 'Cache-Control': 'no-store', - 'Location': url, - 'Pragma': 'no-cache' - }); - - res.end(); - }); - }; -}; diff --git a/lib/controllers/saml-redirect.js b/lib/controllers/saml-redirect.js new file mode 100644 index 00000000..6f051f55 --- /dev/null +++ b/lib/controllers/saml-redirect.js @@ -0,0 +1,30 @@ +'use strict'; + +var stormpath = require('stormpath'); + +module.exports = function(req, res) { + var application = req.app.get('stormpathApplication'); + var builder = new stormpath.SamlIdpUrlBuilder(application); + var config = req.app.get('stormpathConfig'); + var cbUri = req.protocol + '://' + req.get('host') + config.web.saml.verifyUri; + + var samlOptions = { + cb_uri: cbUri + }; + + options = options || {}; + + builder.build(samlOptions, function(err, url) { + if (err) { + throw err; + } + + res.writeHead(302, { + 'Cache-Control': 'no-store', + 'Location': url, + 'Pragma': 'no-cache' + }); + + res.end(); + }); +}; diff --git a/lib/controllers/verify-saml.js b/lib/controllers/saml-verify.js similarity index 100% rename from lib/controllers/verify-saml.js rename to lib/controllers/saml-verify.js diff --git a/lib/stormpath.js b/lib/stormpath.js index 644701a9..2f567682 100644 --- a/lib/stormpath.js +++ b/lib/stormpath.js @@ -181,10 +181,6 @@ module.exports.init = function (app, opts) { } if (web.logout.enabled) { - if (web.saml.enabled) { - addGetRoute(web.logout.uri, controllers.initiateSaml({logout: true})); - } - addPostRoute(web.logout.uri, controllers.logout); } @@ -216,8 +212,8 @@ module.exports.init = function (app, opts) { } if (web.saml.enabled) { - addGetRoute(web.saml.uri, controllers.initiateSaml()); - addGetRoute(web.saml.verifyUri, controllers.verifySaml); + addGetRoute(web.saml.uri, controllers.samlRedirect); + addGetRoute(web.saml.verifyUri, controllers.samlVerify); } client.getApplication(config.application.href, function (err, application) { From 052e6c752befc4fe5383a95a91ad397b936a4c02 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 16:07:17 +0100 Subject: [PATCH 05/13] Further cleanup --- lib/controllers/saml-verify.js | 42 ++++------------------------------ 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/lib/controllers/saml-verify.js b/lib/controllers/saml-verify.js index 1941dfa7..8cb81a2f 100644 --- a/lib/controllers/saml-verify.js +++ b/lib/controllers/saml-verify.js @@ -31,9 +31,6 @@ module.exports = function (req, res) { return helpers.writeJsonError(res, err); } - var parsedToken = nJwt.verify(stormpathToken, config.client.apiKey.secret); - var tokenStatus = parsedToken.body.state || parsedToken.body.status; - function redirectNext() { var nextUri = config.web.saml.nextUri; var nextQueryPath = url.parse(params.next || '').path; @@ -69,25 +66,15 @@ module.exports = function (req, res) { }); } - function handleAuthRequest(type, callback) { - var handler; - - switch (type) { - case 'registration': - handler = config.postRegistrationHandler; - break; - case 'login': - handler = config.postLoginHandler; - break; - default: - return callback(new Error('Invalid authentication request type: ' + type)); - } + function handleAuthRequest(callback) { + var handler = config.postLoginHandler; if (handler) { authenticateToken(function (err, expandedAccount) { if (err) { return callback(err); } + handler(expandedAccount, req, res, callback); }); } else { @@ -95,27 +82,6 @@ module.exports = function (req, res) { } } - switch (tokenStatus) { - case 'REGISTERED': - if (!config.web.register.autoLogin) { - return redirectNext(); - } - handleAuthRequest('registration', redirectNext); - break; - - case 'AUTHENTICATED': - handleAuthRequest('login', redirectNext); - break; - - case 'LOGOUT': - middleware.revokeTokens(req, res); - middleware.deleteCookies(req, res); - redirectNext(); - break; - - default: - res.status(500).end('Unknown SAML result status: ' + tokenStatus); - break; - } + handleAuthRequest('login', redirectNext); }); }; From 2bec5ae5f17fd7da257604d0ecc0817042d297ba Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 16:23:17 +0100 Subject: [PATCH 06/13] Disable saml by default --- lib/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config.yml b/lib/config.yml index 6ec0954a..6e2c71f0 100644 --- a/lib/config.yml +++ b/lib/config.yml @@ -207,7 +207,7 @@ web: # SAML stuff saml: - enabled: true + enabled: false uri: "/initiate-saml-auth" verifyUri: "/verify-saml" nextUri: "/" From e96e7873b8aadb5a4fb3d0362c02f4ee55530489 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 19:00:37 +0100 Subject: [PATCH 07/13] Update settings and basic docs, start work on tests --- lib/config.yml | 10 +- lib/helpers/index.js | 3 +- lib/helpers/saml.js | 10 -- test/controllers/test-saml.js | 198 ++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 15 deletions(-) delete mode 100644 lib/helpers/saml.js create mode 100644 test/controllers/test-saml.js diff --git a/lib/config.yml b/lib/config.yml index 6e2c71f0..3337a30c 100644 --- a/lib/config.yml +++ b/lib/config.yml @@ -205,11 +205,15 @@ web: uri: "/callbacks/linkedin" scope: "r_basicprofile r_emailaddress" - # SAML stuff + # If SAML is enabled, the user should be served a SAML button in the login + # form in traditional websites, and the SAML provider in SPA application + # /login requests. Traditional applications should route login requests + # through the redirect uri, and these requests should automatically be + # verified or rejected through the verification uri. saml: enabled: false - uri: "/initiate-saml-auth" - verifyUri: "/verify-saml" + uri: "/saml-redirect" + verifyUri: "/saml-verify" nextUri: "/" # The /me route is for front-end applications, it returns a JSON object with diff --git a/lib/helpers/index.js b/lib/helpers/index.js index 8e9e19d6..69957ce7 100644 --- a/lib/helpers/index.js +++ b/lib/helpers/index.js @@ -24,6 +24,5 @@ module.exports = { xsrfValidator: require('./xsrf-validator'), revokeToken: require('./revoke-token'), getFormViewModel: require('./get-form-view-model').default, - strippedAccountResponse: require('./stripped-account-response'), - initiateSamlAuth: require('./saml') + strippedAccountResponse: require('./stripped-account-response') }; diff --git a/lib/helpers/saml.js b/lib/helpers/saml.js deleted file mode 100644 index d49ccac7..00000000 --- a/lib/helpers/saml.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -var stormpath = require('stormpath'); -var SamlIdpUrlBuilder = stormpath.SamlIdpUrlBuilder; - -module.exports = function initiateSamlAuth(application, callback) { - var builder = new SamlIdpUrlBuilder(application); - - builder.build(callback); -}; diff --git a/test/controllers/test-saml.js b/test/controllers/test-saml.js new file mode 100644 index 00000000..863e8036 --- /dev/null +++ b/test/controllers/test-saml.js @@ -0,0 +1,198 @@ +'use strict'; + +var request = require('supertest'); +var uuid = require('uuid'); + +var helpers = require('../helpers'); + +function isSamlRedirect(res) { + var location = res && res.headers && res.headers.location; + var error = new Error('Expected Location header with redirect to api.stormpath.com/sso, but got ' + location); + + if (location) { + var match = location.match(/api.stormpath.com\/sso/); + return match ? null : error; + } + + return error; +} + +function prepareSaml(app, callbackUri, cb) { + if (app.authorizedCallbackUris.indexOf(callbackUri) === -1) { + app.authorizedCallbackUris.push(callbackUri); + } + + app.save(cb); +} + +function revertSaml(app, callbackUri, cb) { + var index = app.authorizedCallbackUris.indexOf(callbackUri); + + if (index !== -1) { + app.authorizedCallbackUris.splice(index, 1); + } + + app.save(cb); +} + +function initSamlApp(application, options, cb) { + var webOpts = { + login: { + enabled: true + }, + register: { + enabled: true + }, + saml: { + enabled: true + } + }; + + Object.keys(options).forEach(function (key) { + webOpts[key] = options[key]; + }); + + var app = helpers.createStormpathExpressApp({ + application: { + href: app.href + }, + web: webOpts + }); + + app.on('stormpath.ready', function () { + var config = app.get('stormpathConfig'); + var server = app.listen(function () { + var address = server.address().address === '::' ? 'http://localhost' : server.address().address; + address = address === '0.0.0.0' ? 'http://localhost' : address; + var host = address + ':' + server.address().port; + var callbackUri = host + config.web.saml.verifyUri; + prepareSaml(app.get('stormpathApplication'), callbackUri, function (err) { + if (err) { + return cb(err); + } + + cb({ + application: app, + config: config, + host: host + }); + }); + }); + }); +} + +describe('saml', function () { + describe('traditional website', function () { + var stormpathApplication, app, host, config, callbackUri; + + var accountData = { + givenName: uuid.v4(), + surname: uuid.v4(), + email: uuid.v4() + '@test.com', + password: uuid.v4() + uuid.v4().toUpperCase() + '!' + }; + + before(function (done) { + var client = helpers.createClient().on('ready', function () { + helpers.createApplication(client, function (err, _app) { + if (err) { + return done(err); + } + + stormpathApplication = _app; + + stormpathApplication.createAccount(accountData, function (err) { + if (err) { + return done(err); + } + + initSamlApp(stormpathApplication, {}, function (err, data) { + if (err) { + return done(err); + } + + app = data.application; + config = data.config; + host = data.host; + }); + }); + }); + }); + }); + + after(function (done) { + revertSaml(app.get('stormpathApplication'), callbackUri, function () { + helpers.destroyApplication(stormpathApplication, done); + }); + }); + + it('should redirect to idsite for login, if idsite is enabled', function (done) { + request(host).get(config.web.login.uri) + .expect(302) + .expect(isSamlRedirect) + .end(function (err, res) { + request(res.headers.location) + .get('') + .expect('Location', new RegExp(/\/?jwt=/)) + .end(done); + }); + }); + }); + + describe('spa', function () { + var stormpathApplication, app, host, config, callbackUri; + + var accountData = { + givenName: uuid.v4(), + surname: uuid.v4(), + email: uuid.v4() + '@test.com', + password: uuid.v4() + uuid.v4().toUpperCase() + '!' + }; + + before(function (done) { + var client = helpers.createClient().on('ready', function () { + helpers.createApplication(client, function (err, _app) { + if (err) { + return done(err); + } + + stormpathApplication = _app; + + stormpathApplication.createAccount(accountData, function (err) { + if (err) { + return done(err); + } + + initSamlApp(stormpathApplication, {}, function (err, data) { + if (err) { + return done(err); + } + + app = data.application; + config = data.config; + host = data.host; + }); + }); + }); + }); + }); + + after(function (done) { + revertSaml(app.get('stormpathApplication'), callbackUri, function () { + helpers.destroyApplication(stormpathApplication, done); + }); + }); + + it('should redirect to idsite for login, if idsite is enabled', function (done) { + request(host).get(config.web.login.uri) + .expect(302) + .expect(isSamlRedirect) + .end(function (err, res) { + request(res.headers.location) + .get('') + .expect('Location', new RegExp(/\/?jwt=/)) + .end(done); + }); + }); + }); +}); From e62cd04763ed15ff7ae1d4a1c384e28834f31085 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 21 Nov 2016 19:02:27 +0100 Subject: [PATCH 08/13] Remove dead code, change docs to match SAML --- lib/controllers/saml-redirect.js | 15 +++++++++++---- lib/controllers/saml-verify.js | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/controllers/saml-redirect.js b/lib/controllers/saml-redirect.js index 6f051f55..452afd86 100644 --- a/lib/controllers/saml-redirect.js +++ b/lib/controllers/saml-redirect.js @@ -2,7 +2,16 @@ var stormpath = require('stormpath'); -module.exports = function(req, res) { +/** + * This controller initiates a SAML login process, allowing the user to register + * via a registered SAML provider. + * + * @method + * + * @param {Object} req - The http request. + * @param {Object} res - The http response. + */ +module.exports = function (req, res) { var application = req.app.get('stormpathApplication'); var builder = new stormpath.SamlIdpUrlBuilder(application); var config = req.app.get('stormpathConfig'); @@ -12,9 +21,7 @@ module.exports = function(req, res) { cb_uri: cbUri }; - options = options || {}; - - builder.build(samlOptions, function(err, url) { + builder.build(samlOptions, function (err, url) { if (err) { throw err; } diff --git a/lib/controllers/saml-verify.js b/lib/controllers/saml-verify.js index 8cb81a2f..7d0fc7fc 100644 --- a/lib/controllers/saml-verify.js +++ b/lib/controllers/saml-verify.js @@ -82,6 +82,6 @@ module.exports = function (req, res) { } } - handleAuthRequest('login', redirectNext); + handleAuthRequest(redirectNext); }); }; From 4d983ac512850f657db02480bfd8e323355d4dbb Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Mon, 12 Dec 2016 15:19:40 +0100 Subject: [PATCH 09/13] Documentation tweaks --- lib/controllers/saml-redirect.js | 3 +++ lib/controllers/saml-verify.js | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/controllers/saml-redirect.js b/lib/controllers/saml-redirect.js index 452afd86..41336725 100644 --- a/lib/controllers/saml-redirect.js +++ b/lib/controllers/saml-redirect.js @@ -6,6 +6,9 @@ var stormpath = require('stormpath'); * This controller initiates a SAML login process, allowing the user to register * via a registered SAML provider. * + * When the user logs in through the SAML provider, they will be redirected back + * (to the SAML verification URL). + * * @method * * @param {Object} req - The http request. diff --git a/lib/controllers/saml-verify.js b/lib/controllers/saml-verify.js index 7d0fc7fc..6b97f95a 100644 --- a/lib/controllers/saml-verify.js +++ b/lib/controllers/saml-verify.js @@ -1,16 +1,18 @@ 'use strict'; var url = require('url'); -var nJwt = require('njwt'); var stormpath = require('stormpath'); var helpers = require('../helpers'); -var middleware = require('../middleware'); /** * This controller handles a Stormpath SAML authentication. Once a user is * authenticated, they'll be returned to the site. * + * The returned JWT is verified and an attempt is made to exchange it for an + * access token and refresh token, using the `stormpath_token` grant type, and + * recording the user in the session. + * * @method * * @param {Object} req - The http request. From cba7cd4b0680ba19dbc1904d7228369f4afe4eb7 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Tue, 13 Dec 2016 12:32:27 +0100 Subject: [PATCH 10/13] Fixes and cleanup --- lib/controllers/login.js | 3 +-- lib/views/login.jade | 2 +- lib/views/saml_login_form.jade | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/controllers/login.js b/lib/controllers/login.js index 0426b1a0..aa535358 100644 --- a/lib/controllers/login.js +++ b/lib/controllers/login.js @@ -21,7 +21,6 @@ var oauth = require('../oauth'); */ module.exports = function (req, res, next) { var config = req.app.get('stormpathConfig'); - var application = req.app.get('stormpathApplication'); res.locals.status = req.query.status; @@ -77,7 +76,7 @@ module.exports = function (req, res, next) { var view = config.web.login.view; var oauthStateToken = oauth.common.resolveStateToken(req, res); var formActionUri = (config.web.login.uri + (nextUri ? ('?next=' + nextUri) : '')); - var hasSamlProvider = config.saml.enabled; + var hasSamlProvider = config.web.saml.enabled; var hasSocialProviders = _.some(config.web.social, function (socialProvider) { return socialProvider.enabled; diff --git a/lib/views/login.jade b/lib/views/login.jade index 279c48f3..8bfa72c7 100644 --- a/lib/views/login.jade +++ b/lib/views/login.jade @@ -5,7 +5,7 @@ block vars - var description = 'Log into your account!' - var bodytag = 'login' - var socialProviders = stormpathConfig.web.social - - var samlProvider = stormpathConfig.saml + - var samlProvider = stormpathConfig.web.saml - var loginFields = stormpathConfig.web.login.form.fields block body diff --git a/lib/views/saml_login_form.jade b/lib/views/saml_login_form.jade index 38dadae3..5129a04a 100644 --- a/lib/views/saml_login_form.jade +++ b/lib/views/saml_login_form.jade @@ -1,7 +1,5 @@ button.btn.btn-saml(onclick='samlLogin()') SAML script(type='text/javascript'). function samlLogin() { - var url = '#{stormpathConfig.web.saml.uri}'; - console.log('going to', url); - window.location = url; + window.location = '#{stormpathConfig.web.saml.uri}'; } From 0484074786957b85107a3df7e3d885289da30819 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Tue, 13 Dec 2016 12:32:36 +0100 Subject: [PATCH 11/13] Update SAML tests --- test/controllers/test-saml.js | 148 ++++++++++++---------------------- 1 file changed, 51 insertions(+), 97 deletions(-) diff --git a/test/controllers/test-saml.js b/test/controllers/test-saml.js index 863e8036..b1a51b91 100644 --- a/test/controllers/test-saml.js +++ b/test/controllers/test-saml.js @@ -1,16 +1,18 @@ 'use strict'; +var assert = require('assert'); +var cheerio = require('cheerio'); var request = require('supertest'); var uuid = require('uuid'); var helpers = require('../helpers'); -function isSamlRedirect(res) { +function isSamlRedirect(res, config) { var location = res && res.headers && res.headers.location; - var error = new Error('Expected Location header with redirect to api.stormpath.com/sso, but got ' + location); + var error = new Error('Expected Location header with redirect to saml/sso, but got ' + location); if (location) { - var match = location.match(/api.stormpath.com\/sso/); + var match = location.match(/\/saml\/sso\/idpRedirect/); return match ? null : error; } @@ -54,7 +56,7 @@ function initSamlApp(application, options, cb) { var app = helpers.createStormpathExpressApp({ application: { - href: app.href + href: application.href }, web: webOpts }); @@ -71,7 +73,7 @@ function initSamlApp(application, options, cb) { return cb(err); } - cb({ + cb(null, { application: app, config: config, host: host @@ -82,117 +84,69 @@ function initSamlApp(application, options, cb) { } describe('saml', function () { - describe('traditional website', function () { - var stormpathApplication, app, host, config, callbackUri; - - var accountData = { - givenName: uuid.v4(), - surname: uuid.v4(), - email: uuid.v4() + '@test.com', - password: uuid.v4() + uuid.v4().toUpperCase() + '!' - }; - - before(function (done) { - var client = helpers.createClient().on('ready', function () { - helpers.createApplication(client, function (err, _app) { - if (err) { - return done(err); - } - - stormpathApplication = _app; - - stormpathApplication.createAccount(accountData, function (err) { - if (err) { - return done(err); - } - - initSamlApp(stormpathApplication, {}, function (err, data) { - if (err) { - return done(err); - } - - app = data.application; - config = data.config; - host = data.host; - }); - }); - }); - }); - }); - - after(function (done) { - revertSaml(app.get('stormpathApplication'), callbackUri, function () { - helpers.destroyApplication(stormpathApplication, done); - }); - }); + var stormpathApplication, app, host, config, callbackUri; - it('should redirect to idsite for login, if idsite is enabled', function (done) { - request(host).get(config.web.login.uri) - .expect(302) - .expect(isSamlRedirect) - .end(function (err, res) { - request(res.headers.location) - .get('') - .expect('Location', new RegExp(/\/?jwt=/)) - .end(done); - }); - }); - }); + var accountData = { + givenName: uuid.v4(), + surname: uuid.v4(), + email: uuid.v4() + '@test.com', + password: uuid.v4() + uuid.v4().toUpperCase() + '!' + }; - describe('spa', function () { - var stormpathApplication, app, host, config, callbackUri; + before(function (done) { + var client = helpers.createClient().on('ready', function () { + helpers.createApplication(client, function (err, _app) { + if (err) { + return done(err); + } - var accountData = { - givenName: uuid.v4(), - surname: uuid.v4(), - email: uuid.v4() + '@test.com', - password: uuid.v4() + uuid.v4().toUpperCase() + '!' - }; + stormpathApplication = _app; - before(function (done) { - var client = helpers.createClient().on('ready', function () { - helpers.createApplication(client, function (err, _app) { + stormpathApplication.createAccount(accountData, function (err) { if (err) { return done(err); } - stormpathApplication = _app; - - stormpathApplication.createAccount(accountData, function (err) { + initSamlApp(stormpathApplication, {}, function (err, data) { if (err) { return done(err); } - initSamlApp(stormpathApplication, {}, function (err, data) { - if (err) { - return done(err); - } + app = data.application; + config = data.config; + host = data.host; - app = data.application; - config = data.config; - host = data.host; - }); + done(); }); }); }); }); + }); + + after(function (done) { + revertSaml(app.get('stormpathApplication'), callbackUri, function () { + helpers.destroyApplication(stormpathApplication, done); + }); + }); + + it('should contain the saml link in the login form, if saml is enabled', function (done) { + request(host) + .get(config.web.login.uri) + .expect(200) + .end(function (err, res) { + var $ = cheerio.load(res.text); + assert.equal($('.social-area').length, 1); + assert.equal($('.btn-saml').length, 1); - after(function (done) { - revertSaml(app.get('stormpathApplication'), callbackUri, function () { - helpers.destroyApplication(stormpathApplication, done); + done(err); }); - }); + }); - it('should redirect to idsite for login, if idsite is enabled', function (done) { - request(host).get(config.web.login.uri) - .expect(302) - .expect(isSamlRedirect) - .end(function (err, res) { - request(res.headers.location) - .get('') - .expect('Location', new RegExp(/\/?jwt=/)) - .end(done); - }); - }); + it('should perform a redirect in the SAML verification flow', function (done) { + request(host) + .get(config.web.saml.uri) + .expect(302) + .expect(isSamlRedirect) + .end(done); }); }); From 35dc063dc9ab43441d5171af6e587f642dcdf8d0 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Tue, 13 Dec 2016 12:36:07 +0100 Subject: [PATCH 12/13] Remove unused variable --- test/controllers/test-saml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/test-saml.js b/test/controllers/test-saml.js index b1a51b91..8ef10bf1 100644 --- a/test/controllers/test-saml.js +++ b/test/controllers/test-saml.js @@ -7,7 +7,7 @@ var uuid = require('uuid'); var helpers = require('../helpers'); -function isSamlRedirect(res, config) { +function isSamlRedirect(res) { var location = res && res.headers && res.headers.location; var error = new Error('Expected Location header with redirect to saml/sso, but got ' + location); From 6f56d76fd1dec548fa89f8f10d36485cf58feee0 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Tue, 13 Dec 2016 13:21:08 +0100 Subject: [PATCH 13/13] Remove unused prop --- lib/controllers/login.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/controllers/login.js b/lib/controllers/login.js index aa535358..dd546ba4 100644 --- a/lib/controllers/login.js +++ b/lib/controllers/login.js @@ -87,8 +87,7 @@ module.exports = function (req, res, next) { formActionUri: formActionUri, oauthStateToken: oauthStateToken, hasSocialProviders: hasSocialProviders, - hasSamlProvider: hasSamlProvider, - initiateSamlAuth: helpers.initiateSamlAuth + hasSamlProvider: hasSamlProvider }); helpers.render(req, res, view, options);