From 04e94b2d7f4d9e188e41113536d5de57f4db18f8 Mon Sep 17 00:00:00 2001 From: ivaosthu Date: Thu, 4 Jan 2018 15:48:48 +0100 Subject: [PATCH] Document how to secure Unleash. closes: #233 --- docs/securing-unleash.md | 72 +++++++++++++++++++++++++++- examples/basic-auth-hook.js | 30 ++++++++++++ examples/basic-auth-unleash.js | 19 ++++++++ examples/client-auth-unleash.js | 27 +++++++++++ examples/google-auth-hook.js | 85 +++++++++++++++++++++++++++++++++ examples/google-auth-unleash.js | 19 ++++++++ 6 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 examples/basic-auth-hook.js create mode 100644 examples/basic-auth-unleash.js create mode 100644 examples/client-auth-unleash.js create mode 100644 examples/google-auth-hook.js create mode 100644 examples/google-auth-unleash.js diff --git a/docs/securing-unleash.md b/docs/securing-unleash.md index bc657af3644e..5dbc954adf3b 100644 --- a/docs/securing-unleash.md +++ b/docs/securing-unleash.md @@ -1,3 +1,71 @@ -# Securing Unleash +# Secure Unleash +The Unleash API is split in two different paths: `/api/client` and `/api/admin`. +This makes it easy to have different authentication strategy for the admin interface and the client-api used by the applications integrating with Unleash. -TODO: write about how to secure `/api/client` and `/api/admin` \ No newline at end of file +## General settings +Unleash uses an encrypted cookie to maintain a user session. This allows users to be logged in across instances of Unleash. To protect this cookie you should specify the `secret` option when starting unleash.- + +## Securing the Admin API +In order to secure the Admin API you have to tell Unleash that you are using a custom admin authentication and implement your authentication logic as a preHook. You should also set the secret option to a protected secret in your system. + +```javascript +const unleash = require('unleash-server'); +const myCustomAdminAuth = require('./auth-hook'); + +unleash.start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: myCustomAdminAuth +}).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +}); + +``` + +Examples on custom authentication hooks: +- [google-auth-hook.js](https://github.com/Unleash/unleash/blob/master/examples/google-auth-hook.js) +- [basic-auth-hook.js](https://github.com/Unleash/unleash/blob/master/examples/basic-auth-hook.js) + + +## Securing the Client API +A common way to support client access is to use pre shared secrets. This can be solved by having clients send a shared key in a http header with every client requests to the Unleash API. All official Unleash clients should support this. + +In the [Java client](https://github.com/Unleash/unleash-client-java#custom-http-headers) this looks like: + +```java +UnleashConfig unleashConfig = UnleashConfig.builder() + .appName("my-app") + .instanceId("my-instance-1") + .unleashAPI(unleashAPI) + .customHttpHeader("Authorization", "12312Random") + .build(); +``` + +On the unleash server side you need to implement a preRouterHook hook which verifies that all calls to `/api/client` includes this pre shared key in the defined header. This could look something like this: + +```javascript +const unleash = require('unleash-server'); +const sharedSecret = '12312Random'; + +unleash.start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + enableLegacyRoutes: false, + preRouterHook: (app) => { + app.use('/api/client', (req, res, next) => { + if(req.headers.authorization !== sharedSecret) { + res.sendStatus(401); + } else { + next() + } + }); + } +}).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +}); +``` + +[client-auth-unleash.js](https://github.com/Unleash/unleash/blob/master/examples/client-auth-unleash.js) + + +PS! Remember to disable legacy route with by setting the `enableLegacyRoutes` option to false. This will require all your clients to be on v3.x. diff --git a/examples/basic-auth-hook.js b/examples/basic-auth-hook.js new file mode 100644 index 000000000000..7d09b2727885 --- /dev/null +++ b/examples/basic-auth-hook.js @@ -0,0 +1,30 @@ +'use strict'; + +const auth = require('basic-auth'); +const { User } = require('../lib/server-impl.js'); + +function basicAuthentication(app) { + app.use('/api/admin/', (req, res, next) => { + const credentials = auth(req); + + if (credentials) { + // you will need to do some verification of credentials here. + const user = new User({ email: `${credentials.name}@domain.com` }); + req.user = user; + next(); + } else { + return res + .status('401') + .set({ 'WWW-Authenticate': 'Basic realm="example"' }) + .end('access denied'); + } + }); + + app.use((req, res, next) => { + // Updates active sessions every hour + req.session.nowInHours = Math.floor(Date.now() / 3600e3); + next(); + }); +} + +module.exports = basicAuthentication; diff --git a/examples/basic-auth-unleash.js b/examples/basic-auth-unleash.js new file mode 100644 index 000000000000..0b525d87c67c --- /dev/null +++ b/examples/basic-auth-unleash.js @@ -0,0 +1,19 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +const basicAuth = require('./basic-auth-hook'); + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: basicAuth, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + }); diff --git a/examples/client-auth-unleash.js b/examples/client-auth-unleash.js new file mode 100644 index 000000000000..13ad5b741eb6 --- /dev/null +++ b/examples/client-auth-unleash.js @@ -0,0 +1,27 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +// You typically will not hard-code this value in your code! +const sharedSecret = '12312Random'; + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + enableLegacyRoutes: false, + preRouterHook: app => { + app.use('/api/client', (req, res, next) => { + if (req.headers.authorization === sharedSecret) { + next(); + } else { + res.sendStatus(401); + } + }); + }, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + }); diff --git a/examples/google-auth-hook.js b/examples/google-auth-hook.js new file mode 100644 index 000000000000..635b7e1cc487 --- /dev/null +++ b/examples/google-auth-hook.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Google OAath 2.0 + * + * You should read Using OAuth 2.0 to Access Google APIs: + * https://developers.google.com/identity/protocols/OAuth2 + * + * This example assumes that all users authenticating via + * google should have access. You would proably limit access + * to users you trust. + * + * The implementation assumes the following environement variables: + * + * - GOOGLE_CLIENT_ID + * - GOOGLE_CLIENT_SECRET + * - GOOGLE_CALLBACK_URL + */ + +// const { User, AuthenticationRequired } = require('unleash-server'); +const { User, AuthenticationRequired } = require('../lib/server-impl.js'); + +const passport = require('passport'); +const GoogleOAuth2Strategy = require('passport-google-auth').Strategy; + +passport.use( + new GoogleOAuth2Strategy( + { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL, + }, + + (accessToken, refreshToken, profile, done) => { + done( + null, + new User({ + name: profile.displayName, + email: profile.emails[0].value, + }) + ); + } + ) +); + +function enableGoogleOauth(app) { + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser((user, done) => done(null, user)); + passport.deserializeUser((user, done) => done(null, user)); + app.get('/api/admin/login', passport.authenticate('google')); + + app.get( + '/api/auth/callback', + passport.authenticate('google', { + failureRedirect: '/api/admin/error-login', + }), + (req, res) => { + // Successful authentication, redirect to your app. + res.redirect('/'); + } + ); + + app.use('/api/admin/', (req, res, next) => { + if (req.user) { + next(); + } else { + // Instruct unleash-frontend to pop-up auth dialog + return res + .status('401') + .json( + new AuthenticationRequired({ + path: '/api/admin/login', + type: 'custom', + message: `You have to identify yourself in order to use Unleash. + Click the button and follow the instructions.`, + }) + ) + .end(); + } + }); +} + +module.exports = enableGoogleOauth; diff --git a/examples/google-auth-unleash.js b/examples/google-auth-unleash.js new file mode 100644 index 000000000000..7b0e2a26dafd --- /dev/null +++ b/examples/google-auth-unleash.js @@ -0,0 +1,19 @@ +'use strict'; + +// const unleash = require('unleash-server'); +const unleash = require('../lib/server-impl.js'); + +const enableGoogleOauth = require('./google-auth-hook'); + +unleash + .start({ + databaseUrl: 'postgres://unleash_user:passord@localhost:5432/unleash', + secret: 'super-duper-secret', + adminAuthentication: 'custom', + preRouterHook: enableGoogleOauth, + }) + .then(server => { + console.log( + `Unleash started on http://localhost:${server.app.get('port')}` + ); + });