diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index 9dd3902d..0d207bcd 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -661,6 +661,41 @@ ExecStart=/usr/bin/docker run --rm \ ExecStop=-/usr/bin/docker stop -t 10 %p-%i ``` +### `stf-auth@.service` (SAML2.0) + +This is one of the multiple options for authentication provided by STF. It uses [SAML 2.0](http://saml.xml.org/saml-specifications) protocol. If your company uses [Okta](https://www.okta.com/) or some other SAML2.0 supported id provider, you can use it. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-auth@3200.service` runs on port 3200). You can have multiple instances running on the same host by using different ports. + +** NOTE** Don't forget to change `--app-url` parameter for `stf-app` unit. It will become `https://stf.example.org/auth/saml/` + +```ini +[Unit] +Description=STF auth +After=docker.service +Requires=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:latest +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -v /srv/ssl/id_provider.cert:/etc/id_provider.cert:ro \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + -e "SAML_ID_PROVIDER_ENTRY_POINT_URL=YOUR_ID_PROVIDER_ENTRY_POINT" \ + -e "SAML_ID_PROVIDER_ISSUER=YOUR_ID_PROVIDER_ISSUER" \ + -e "SAML_ID_PROVIDER_CERT_PATH=/etc/id_proider.cert" \ + -p %i:3000 \ + openstf/stf:latest \ + stf auth-saml2 --port 3000 \ + --app-url https://stf.example.org/ +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + ## Nginx configuration Now that you've got all the units ready, it's time to set up [nginx](http://nginx.org/) to tie all the processes together with a clean URL. diff --git a/lib/cli.js b/lib/cli.js index 6206a73d..30a61b94 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -482,6 +482,63 @@ program }) }) + program + .command('auth-saml2') + .description('start SAML 2.0 auth client') + .option('-p, --port ' + , 'port (or $PORT)' + , Number + , process.env.PORT || 7120) + .option('-s, --secret ' + , 'secret (or $SECRET)' + , String + , process.env.SECRET) + .option('-i, --ssid ' + , 'session SSID (or $SSID)' + , String + , process.env.SSID || 'ssid') + .option('-a, --app-url ' + , 'URL to app' + , String) + .option('--saml-id-provider-entry-point-url ' + , 'SAML 2.0 identity provider URL (or $SAML_ID_PROVIDER_ENTRY_POINT_URL)' + , String + , process.env.SAML_ID_PROVIDER_ENTRY_POINT_URL) + .option('--saml-id-provider-issuer ' + , 'SAML 2.0 identity provider issuer (or $SAML_ID_PROVIDER_ISSUER)' + , String + , process.env.SAML_ID_PROVIDER_ISSUER) + .option('--saml-id-provider-cert-path ' + , 'SAML 2.0 identity provider certificate file path (or $SAML_ID_PROVIDER_CERT_PATH)' + , String + , process.env.SAML_ID_PROVIDER_CERT_PATH) + .action(function(options) { + if (!options.secret) { + this.missingArgument('--secret') + } + if (!options.appUrl) { + this.missingArgument('--app-url') + } + if (!options.samlIdProviderEntryPointUrl) { + this.missingArgument('--saml-id-provider-entry-point-url') + } + if (!options.samlIdProviderIssuer) { + this.missingArgument('--saml-id-provider-issuer') + } + + require('./units/auth/saml2')({ + port: options.port + , secret: options.secret + , ssid: options.ssid + , appUrl: options.appUrl + , saml: { + entryPoint: options.samlIdProviderEntryPointUrl + , issuer: options.samlIdProviderIssuer + , certPath: options.samlIdProviderCertPath + } + }) + }) + program .command('auth-mock') .description('start mock auth client') @@ -918,7 +975,7 @@ program , 'device pull endpoint' , String , 'tcp://127.0.0.1:7116') - .option('--auth-type ' + .option('--auth-type ' , 'auth type' , String , 'mock') @@ -1099,7 +1156,7 @@ program 'http://%s:%d/auth/%s/' , options.publicIp , options.poorxyPort - , ({oauth2: 'oauth'}[options.authType]) || options.authType + , ({oauth2: 'oauth', saml2: 'saml'}[options.authType]) || options.authType ) , '--websocket-url', util.format( 'http://%s:%d/' diff --git a/lib/units/auth/saml2.js b/lib/units/auth/saml2.js new file mode 100644 index 00000000..a27144c6 --- /dev/null +++ b/lib/units/auth/saml2.js @@ -0,0 +1,80 @@ +var fs = require('fs') +var http = require('http') + +var express = require('express') +var passport = require('passport') +var SamlStrategy = require('passport-saml').Strategy +var bodyParser = require('body-parser') +var _ = require('lodash') + +var logger = require('../../util/logger') +var urlutil = require('../../util/urlutil') +var jwtutil = require('../../util/jwtutil') + +module.exports = function(options) { + var log = logger.createLogger('auth-saml2') + , app = express() + , server = http.createServer(app) + + app.set('strict routing', true) + app.set('case sensitive routing', true) + app.use(bodyParser.urlencoded({ extended: false })) + app.use(passport.initialize()) + + passport.serializeUser(function(user, done) { + done(null, user); + }); + passport.deserializeUser(function(user, done) { + done(null, user); + }); + + var verify = function(profile, done) { + return done(null, profile) + } + + var samlConfig = { + path: '/auth/saml/callback' + , entryPoint: options.saml.entryPoint + , issuer: options.saml.issuer + } + + if (options.saml.certPath) { + samlConfig = _.merge(samlConfig, { + cert: fs.readFileSync(options.saml.certPath).toString() + }) + } + + passport.use(new SamlStrategy(samlConfig, verify)) + + app.use(passport.authenticate('saml', { + failureRedirect: '/auth/saml/' + , session: false + })) + + app.post( + '/auth/saml/callback' + , function(req, res) { + if (req.user.email) { + res.redirect(urlutil.addParams(options.appUrl, { + jwt: jwtutil.encode({ + payload: { + email: req.user.email + , name: req.user.email.split('@', 1).join('') + } + , secret: options.secret + , header: { + exp: Date.now() + 24 * 3600 + } + }) + })) + } + else { + log.warn('Missing email in profile', req.user) + res.redirect('/auth/saml/') + } + } + ) + + server.listen(options.port) + log.info('Listening on port %d', options.port) +} diff --git a/package.json b/package.json index 6b50b8c6..6a113f16 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "node-uuid": "^1.4.3", "passport": "^0.3.2", "passport-oauth2": "^1.1.2", + "passport-saml": "^0.14.0", "protobufjs": "^3.8.2", "proxy-addr": "^1.0.10", "request": "^2.67.0",