diff --git a/docs/guides/google-auth-hook.md b/docs/guides/google-auth-hook.md new file mode 100644 index 0000000000..a1a19445a2 --- /dev/null +++ b/docs/guides/google-auth-hook.md @@ -0,0 +1,227 @@ +--- +id: google_auth +title: Google Auth Hook +--- + +This part of the tutorial shows how to create a sign-in flow for users and integrate with Unleash server project. The implementation assumes that I am working in `localhost` with `4242` port. + +This is a simple `index.js` server file. +```javascript +const unleash = require('unleash-server'); + +if (process.env.DATABASE_URL_FILE) { + options.databaseUrl = fs.readFileSync(process.env.DATABASE_URL_FILE); +} + +unleash.start(options).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +});; +``` + +### Creating a web application client ID + +1. Go to the credentials section in the [Google Cloud Platform Console](https://console.cloud.google.com/apis/credentials?_ga=2.77615956.-1991581217.1542834301). + +2. Click **OAuth consent screen**. Type a product name. Fill in any relevant optional fields. Click **Save**. + +3. Click **Create credentials > OAuth client ID**. + +4. Under **Application type**, select **Web Application**. + +5. Type the **Name**. + +6. Under **Authorized redirect URIs** enter the following URLs, one at a time. +``` +http://localhost:4242/api/auth/callback +``` + +7. Click **Create**. + +8. Copy the **CLIENT ID** and **CLIENT SECRET** and save them for later use. + + +### Add dependencies + +Add two dependencies [`passport`](https://www.npmjs.com/package/passport) and [`passport-google-oauth20`](https://www.npmjs.com/package/passport-google-oauth20) inside `index.js` file +```js +const unleash = require('unleash-server'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +``` +### Configure the Google strategy for use by Passport.js + +OAuth 2-based strategies require a `verify` function which receives the credential (`accessToken`) for accessing the Google API on the user's behalf, along with the user's profile. The function must invoke `cb` with a user object, which will be set at `req.user` in route handlers after authentication. +```js +const GOOGLE_CLIENT_ID = '...'; +const GOOGLE_CLIENT_SECRET = '...'; +const GOOGLE_CALLBACK_URL = 'http://localhost:4242/api/auth/callback'; + +passport.use(new GoogleStrategy( + { + clientID: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL: GOOGLE_CALLBACK_URL + }, + function(accessToken, refreshToken, profile, cb) { + // Extract the minimal profile information we need from the profile object + // and connect with Unleash to get name and email. + cb(null, new unleash.User({ + name: profile.displayName, + email: profile.emails[0].value, + })); + } +)); +``` + +Add `googleAdminAuth()` function and other options +```js +function googleAdminAuth(app) {} + +let options = { + enableLegacyRoutes: false, + adminAuthentication: 'custom', + preRouterHook: googleAdminAuth +}; + +unleash.start(options).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +});; +``` + +### In `googleAdminAuth` function + +Configure `passport` package. +```js +function googleAdminAuth(app) { + app.use(passport.initialize()); + app.use(passport.session()); + passport.serializeUser((user, done) => done(null, user)); + passport.deserializeUser((user, done) => done(null, user)); + // ... +} +``` + +Implement a preRouter hook for `/api/admin/login`. It's neccesary for login with Google. +```js +function googleAdminAuth(app) { + // ... + app.get('/api/admin/login', passport.authenticate('google', { scope: ['profile', 'email'] })); + // ... +} +``` + +Implement a preRouter hook for `/api/auth/callback`. It's a callback when the login is executed. +```js +function googleAdminAuth(app) { + // ... + app.get('/api/auth/callback', + passport.authenticate('google', { + failureRedirect: '/api/admin/error-login', + }), + (req, res) => { + // Successful authentication, redirect to your app. + res.redirect('/'); + } + ); + // ... +} +``` + +Implement a preRouter hook for `/api/admin`. +```js +function googleAdminAuth(app) { + // ... + 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 unleash.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(); + } + }); + // ... +} +``` + +### The complete code +The `index.js` server file. +```js +'use strict'; + +const unleash = require('unleash-server'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; + +const GOOGLE_CLIENT_ID = '...'; +const GOOGLE_CLIENT_SECRET = '...'; +const GOOGLE_CALLBACK_URL = 'http://localhost:4242/api/auth/callback'; + +passport.use(new GoogleStrategy( + { + clientID: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL: GOOGLE_CALLBACK_URL + }, + function(accessToken, refreshToken, profile, cb) { + cb(null, new unleash.User({ + name: profile.displayName, + email: profile.emails[0].value, + })); + } +)); + +function googleAdminAuth(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', { scope: ['profile', 'email'] })); + app.get('/api/auth/callback', + passport.authenticate('google', { + failureRedirect: '/api/admin/error-login', + }), + (req, res) => { + res.redirect('/'); + } + ); + + app.use('/api/admin/', (req, res, next) => { + if (req.user) { + next(); + } else { + return res + .status('401') + .json( + new unleash.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(); + } + }); +} + +let options = { + enableLegacyRoutes: false, + adminAuthentication: 'custom', + preRouterHook: googleAdminAuth +}; + +if (process.env.DATABASE_URL_FILE) { + options.databaseUrl = fs.readFileSync(process.env.DATABASE_URL_FILE); +} + +unleash.start(options).then(unleash => { + console.log(`Unleash started on http://localhost:${unleash.app.get('port')}`); +});; +``` diff --git a/website/sidebars.json b/website/sidebars.json index 93b204ce18..fa174b1355 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -1,12 +1,13 @@ { "documentation": { "Getting Started": ["getting_started", "securing_unleash", "unleash_context", "activation_strategy", "client_specification", "migration_guide"], - "Developer Guide": ["developer_guide", "database_schema", "database_backup"] + "Developer Guide": ["developer_guide", "database_schema", "database_backup"], + "Guides": ["guides/google_auth"] }, "api": { "Client": ["api/client/features", "api/client/register", "api/client/metrics"], "Admin": ["api/admin/features", "api/admin/strategies", "api/admin/metrics", "api/admin/events" ], "Internal": ["api/internal" ] - + } }