├── .gitignore ├── favicon.ico ├── README └── SAML2Proxy.png ├── package.json ├── proxy.example.json ├── README.md └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2ebridge/saml2-proxy/HEAD/favicon.ico -------------------------------------------------------------------------------- /README/SAML2Proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2ebridge/saml2-proxy/HEAD/README/SAML2Proxy.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saml2-proxy", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "dependencies": { 9 | "connect-ensure-login": "0.1.1", 10 | "express": "3.2.5", 11 | "http-proxy": "0.10.x", 12 | "passport": "0.2.x", 13 | "passport-saml": "^0.5.2", 14 | "winston": "0.7.x", 15 | "winston-stderr": "0.0.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /proxy.example.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | // run proxy on this port. Default is 3000, can be overriden with PORT environment variable 4 | "port" : 3000, 5 | // protocol the proxy uses. Can be 'http' or 'https' 6 | "externalProtocol" : "https", 7 | // only required if externalProtocol === 'https' 8 | "ssl" : { 9 | // path to SSL private key file. Can be absolute or relative to certDir. PEM format, unencrypted. 10 | "privateKeyFile" : "node_https_proxy/private_key.pem", 11 | // path to SSL certificate file. Can be absolute or relative to certDir. PEM format, unencrypted. 12 | "certificateFile" : "node_https_proxy/cert.pem" 13 | }, 14 | // name of HTTP header that will transport credentials 15 | "credentialsHeader" : "X-Auth-Credentials", 16 | // name of HTTP header that will transport credentials' signature 17 | "signatureHeader" : "X-Auth-Signature", 18 | // path to private key file to be used to sign credentials. Can be absolute or relative to certDir. PEM format, unencrypted. 19 | // if not given, no signing will be performed 20 | "credentialsSigningKeyFile" : "privateKey.pem", 21 | // path to directory containing certificates and keys. Absolute or relative to application root. 22 | "certDir" : "cert", 23 | //settings for SAML authentication strategy 24 | "saml" : { 25 | // where to redirect user 26 | "entryPoint": "http://sso.local/simplesaml/saml2/idp/SSOService.php", 27 | // how do the proxy introduce itself to IdP 28 | "issuer": "E2ESSOWebTest", 29 | // in most cases should be equal to (externalProtocol + "://") 30 | "protocol": "https://", 31 | // certificate of IdP. PEM format without prolog and epilogue. If given, will be used to verify SAML response signature 32 | "cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo", 33 | // private key file used to sign SAML request. If given, must match a certificate registered in IdP. 34 | // Can be absolute path or relative to certDir 35 | "privateKeyFile": "privateKey.pem" 36 | }, 37 | // In case the authentication takes longer than our session (see sessionDuration), we forget where to redirect user to 38 | // in that case this will be used as a safe default 39 | "indexPath" : "/proxy/ui/HelloWorldUI.html", 40 | // increase security by puttin some random characters here 41 | "sessionSecret" : "x&^RYg97c7dngw97dcg7&6T&^t967NC69WDQ0W89", 42 | // session duration in milliseconds. 0 means "browser session" 43 | "sessionDuration" : 300000, 44 | //define your routes here 45 | "routes": [ 46 | { 47 | "routedPrefix": "/proxy", 48 | "destination": "services.local", 49 | "destinationPort": 13333 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #saml2-proxy 2 | --- 3 | A node.js-based proxy server that performs authentication using SAML2 protocol. Put between the service and identity provider, it follows the SAML2 protocol to authenticate user against the identity provider and passes user identity to the services in request header. 4 | 5 | The best effects can be achieved by combining with firewall configuration to prevent direct access to the service and signing user data using RSA-SHA1 algorithm. 6 | 7 | ![Example request with authentication](README/SAML2Proxy.png) 8 | 9 | It assumes, that implementing reading header values and signature checking is easy in the service. In fact that was the reason to write this proxy. 10 | 11 | ##Configuration 12 | 13 | ###Environment variables: 14 | - **CONFIG_FILE** - full path (including file name) to configuration file that will be used instead of default proxy.json 15 | - **NODE_ENV** - if you put `production` or simply `prod` here, verbose debug logging will be suppressed. It's recommended to do so when you're finished with development. 16 | - **PORT** - start proxy on specified port. This overrides the port given in configuration file. 17 | 18 | ###Configuration file: 19 | Below is the content of `proxy.example.json`. This file provides all possible options with comments. The default location of the actual configuration file is `${PROGRAM_DIR}/proxy.json` which has to be created by user based on the example: 20 | 21 | ```javascript 22 | 23 | { 24 | // run proxy on this port. Default is 3000, can be overriden with PORT environment variable 25 | "port" : 3000, 26 | // protocol the proxy uses. Can be 'http' or 'https' 27 | "externalProtocol" : "https", 28 | // only required if externalProtocol === 'https' 29 | "ssl" : { 30 | // path to SSL private key file. Can be absolute or relative to certDir. PEM format, unencrypted. 31 | "privateKeyFile" : "node_https_proxy/private_key.pem", 32 | // path to SSL certificate file. Can be absolute or relative to certDir. PEM format, unencrypted. 33 | "certificateFile" : "node_https_proxy/cert.pem" 34 | }, 35 | // name of HTTP header that will transport credentials 36 | "credentialsHeader" : "X-Auth-Credentials", 37 | // name of HTTP header that will transport credentials' signature 38 | "signatureHeader" : "X-Auth-Signature", 39 | // path to private key file to be used to sign credentials. Can be absolute or relative to certDir. PEM format, unencrypted. 40 | // if not given, no signing will be performed 41 | "credentialsSigningKeyFile" : "privateKey.pem", 42 | // path to directory containing certificates and keys. Absolute or relative to application root. 43 | "certDir" : "cert", 44 | //settings for SAML authentication strategy 45 | "saml" : { 46 | // where to redirect user 47 | "entryPoint": "http://sso.local/simplesaml/saml2/idp/SSOService.php", 48 | // how do the proxy introduce itself to IdP 49 | "issuer": "E2ESSOWebTest", 50 | // in most cases should be equal to (externalProtocol + "://") 51 | "protocol": "https://", 52 | // certificate of IdP. PEM format without prolog and epilogue. If given, will be used to verify SAML response signature 53 | "cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo", 54 | // private key file used to sign SAML request. If given, must match a certificate registered in IdP. 55 | // Can be absolute path or relative to certDir 56 | "privateKeyFile": "privateKey.pem" 57 | }, 58 | // In case the authentication takes longer than our session (see sessionDuration), we forget where to redirect user to 59 | // in that case this will be used as a safe default 60 | "indexPath" : "/proxy/ui/HelloWorldUI.html", 61 | // increase security by puttin some random characters here 62 | "sessionSecret" : "x&^RYg97c7dngw97dcg7&6T&^t967NC69WDQ0W89", 63 | // session duration in milliseconds. 0 means "browser session" 64 | "sessionDuration" : 300000, 65 | //define your routes here 66 | "routes": [ 67 | { 68 | "routedPrefix": "/proxy", 69 | "destination": "services.local", 70 | "destinationPort": 13333 71 | } 72 | ] 73 | } 74 | 75 | ``` 76 | --- 77 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************************************** 2 | * SAML2 SSO Proxy 3 | * This code supports SingleSignOn scenario 5.1.2 as described in: 4 | * https://www.oasis-open.org/committees/download.php/27819/sstc-saml-tech-overview-2.0-cd-02.pdf 5 | * 6 | * author: Jakub Zakrzewski 7 | * copyright: E2E Technologies Ltd. 8 | **********************************************************************************************************************/ 9 | 10 | var express = require('express') 11 | , passport = require('passport') 12 | , samlStrategy = require('passport-saml').Strategy 13 | , ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn 14 | , httpProxy = require('http-proxy') 15 | , fs = require('fs') 16 | , path = require('path') 17 | , crypto = require('crypto') 18 | , winston = require('winston'); 19 | 20 | // read configuration 21 | var configFile = process.env.CONFIG_FILE || path.join( __dirname, 'proxy.json'); 22 | var config = JSON.parse(fs.readFileSync( configFile)); 23 | 24 | var devEnv = !( process.env.NODE_ENV === 'prod' || process.env.NODE_ENV === 'production'); 25 | 26 | // Passport session setup. 27 | // To support persistent login sessions, Passport needs to be able to 28 | // serialize users into and deserialize users out of the session. 29 | // For simplicity I serialize entire JSON structure here. 30 | // It's probable, that we won't need anything more sophisticated 31 | passport.serializeUser(function(req, user, done) { 32 | done(null, JSON.stringify(user)); 33 | }); 34 | 35 | passport.deserializeUser(function(req, id, done) { 36 | done(null, JSON.parse(id)); 37 | }); 38 | 39 | // set up logger 40 | var logger = new (winston.Logger)({ 41 | transports: [ 42 | new (require('winston-stderr'))({level: devEnv ? "debug" : "warn"}) 43 | ] 44 | }); 45 | 46 | // enable web server logging; pipe those log messages through winston 47 | var loggerStream = { 48 | write: function(message, encoding){ 49 | logger.debug(message.trim()); 50 | } 51 | }; 52 | 53 | var strategy = new samlStrategy( 54 | { 55 | entryPoint: config.saml.entryPoint, 56 | issuer: config.saml.issuer, 57 | protocol: config.saml.protocol, 58 | path: '/saml', 59 | cert: config.saml.cert, 60 | privateCert: config.saml.privateKeyFile ? fs.readFileSync(path.resolve( __dirname, config.certDir, config.saml.privateKeyFile), 'utf-8') : undefined 61 | }, 62 | function(profile, done) { 63 | // simply accept everything - bridge will verify this 64 | logger.debug("Auth with: ", profile); 65 | return done(null, profile); 66 | } 67 | ); 68 | passport.use(strategy); 69 | 70 | var app = express(); 71 | app.use(express.logger({stream: loggerStream})); 72 | app.use(express.favicon(path.join(__dirname, 'favicon.ico'))); 73 | app.use(express.cookieParser()); 74 | app.use(express.bodyParser()); 75 | app.use(express.methodOverride()); 76 | app.use(express.session({ secret: config.sessionSecret, cookie: { maxAge: config.sessionDuration }})); 77 | app.use(passport.initialize()); 78 | app.use(passport.session()); 79 | 80 | app.all('/saml', 81 | passport.authenticate('saml', { 82 | samlFallback: 'login-request', 83 | successReturnToOrRedirect: config.indexPath || '/', 84 | failureRedirect: '/401', 85 | failureFlash: true 86 | }), 87 | function(req, res) { 88 | throw new Error("This callback should never be invoked"); 89 | } 90 | ); 91 | 92 | app.get('/401', function(req, res){ 93 | res.status(401).send("Authentication failed"); 94 | }); 95 | 96 | var proxy = new httpProxy.RoutingProxy(); 97 | var signingKey = config.credentialsSigningKeyFile ? fs.readFileSync(path.resolve(__dirname, config.certDir, config.credentialsSigningKeyFile)).toString() : null; 98 | 99 | config.routes.forEach(function(route){ 100 | app.all( route.routedPrefix + '/*', ensureLoggedIn('/saml'), function(req, res) { 101 | var credentials = JSON.stringify({userData: req.user, timestamp: new Date()}); 102 | req.headers[config.credentialsHeader] = encodeURIComponent(credentials); 103 | if( signingKey) { 104 | var signer = crypto.createSign("RSA-SHA1"); 105 | signer.end( credentials); 106 | var signature = signer.sign( signingKey, 'base64'); 107 | req.headers[config.signatureHeader] = encodeURIComponent(signature); 108 | } 109 | req.url = req.url.substring(route.routedPrefix.length); //strip route prefix from url 110 | logger.debug('Routing: ', req.url, ' to: ', JSON.stringify(route)); 111 | proxy.proxyRequest(req, res, { 112 | host: route.destination, 113 | port: route.destinationPort 114 | }); 115 | }); 116 | }); 117 | 118 | var port = process.env.PORT || config.port || 3000; 119 | 120 | if( config.externalProtocol === 'http') { 121 | require('http').createServer(app).listen(port, function () { 122 | console.log("Server listening on port: " + port); 123 | }); 124 | } else if( config.externalProtocol === 'https') { 125 | require('https').createServer( 126 | { 127 | key: fs.readFileSync(path.resolve(__dirname, config.certDir, config.ssl.privateKeyFile)).toString(), 128 | cert: fs.readFileSync(path.resolve(__dirname, config.certDir, config.ssl.certificateFile)).toString() 129 | }, 130 | app 131 | ).listen(port, function () { 132 | console.log("Secure server listening on port: " + port); 133 | }); 134 | } else { 135 | logger.error("Unknown protocol: ", config.externalProtocol); 136 | } 137 | 138 | --------------------------------------------------------------------------------