├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── basic │ ├── index.js │ └── templates │ ├── authy │ ├── register.hbs │ └── verify.hbs │ ├── index.hbs │ ├── layout.hbs │ └── login.hbs ├── index.js ├── package.json └── test ├── index.js └── templates └── authy ├── register.hbs └── verify.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 4.0 5 | - 4 6 | - 5 7 | 8 | sudo: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Matt Harrison 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of hapi-authy nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-authy [![Build Status](https://travis-ci.org/mtharrison/hapi-authy.svg)](https://travis-ci.org/mtharrison/hapi-authy) 2 | ## Two-Factor Authentication with Authy and hapi 3 | 4 | This is a plugin that you can use to add 2fa to your hapi apps with ease. It works with the [Authy](https://www.authy.com/) service. Head over to Authy and register for an account. 5 | 6 | Check out the example under `examples/basic` for a full working example of form based email/password and authy authentication (Authy API Key required) 7 | 8 | ### Getting started 9 | 10 | 1. Register with Authy 11 | 2. Create an app 12 | 3. Grab your api key 13 | 14 | ### Installation 15 | 16 | npm install --save hapi-authy 17 | 18 | ### Usage 19 | 20 | This would normally be used to implement the second step in a login process. After a successful step 1 (usually username/password login), a user with a 2fa-enabled account would be redirected to the 2fa route. Everything is then handled by the plugin. 21 | 22 | This plugins defines a hapi auth scheme called authy. To get started, create a strategy from this scheme: 23 | 24 | ```javascript 25 | server.auth.strategy('authy', 'authy', { 26 | apiKey: 'your api key', 27 | sandbox: false, 28 | cookieOptions: { 29 | isSecure: false, 30 | path: '/', 31 | encoding: 'iron', 32 | password: 'cookiepass' 33 | } 34 | }); 35 | ``` 36 | 37 | Then define the 2FA route where you will redirect users to: 38 | 39 | ```javascript 40 | server.route({ 41 | method: ['GET', 'POST'], 42 | path: '/authy', 43 | config: { 44 | auth: { 45 | strategies: ['authy'], 46 | payload: true 47 | }, 48 | handler: function (request, reply) { 49 | 50 | const credentials = request.auth.credentials; // user's email and authyId 51 | const user = users[credentials.email]; 52 | user.requireTfa = true; // user's account updated to use 2fa 53 | user.authyId = credentials.authyId; // authyId saved for future logins 54 | request.auth.session.set(user); // user logged in 55 | return reply.redirect('/'); 56 | } 57 | } 58 | }); 59 | ``` 60 | 61 | The plugin will then take over fetching the relevant information from the user. The handler for this route will be finally executed once the user has successfully entered their 2FA token, either via SMS or the vis from the Authy app. 62 | 63 | ![step1](http://matt-github.s3.amazonaws.com/hapi-authy/step1.png) 64 | ![step2](http://matt-github.s3.amazonaws.com/hapi-authy/step2.png) 65 | ![step3](http://matt-github.s3.amazonaws.com/hapi-authy/step3.png) 66 | 67 | ### Configuration/customisation 68 | 69 | Section coming soon. Please checkout examples for now. 70 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boom = require('boom'); 4 | const Bcrypt = require('bcryptjs'); 5 | const Hapi = require('hapi'); 6 | const Joi = require('joi'); 7 | const Path = require('path'); 8 | 9 | const server = new Hapi.Server(); 10 | server.connection({ port: 4000 }); 11 | 12 | const users = { 13 | 'hi@matt-harrison.com': { 14 | password: '$2a$08$.sI.S6l9lL0crviIOn/EUuAc/0oTlBA9R0b6rGEJYRD2p2h76bKK.', // 'secret' 15 | requireTfa: false, 16 | authyId: null 17 | } 18 | }; 19 | 20 | server.register([ 21 | { register: require('vision') }, 22 | { register: require('hapi-auth-cookie') }, 23 | { register: require('../..') } 24 | ], (err) => { 25 | 26 | if (err) { 27 | throw err; 28 | } 29 | 30 | server.views({ 31 | engines: { 32 | hbs: require('handlebars') 33 | }, 34 | path: Path.join(__dirname, 'templates'), 35 | layout: true 36 | }); 37 | 38 | // Email/password login stage 39 | 40 | server.auth.strategy('session', 'cookie', { 41 | password: 'password', 42 | cookie: 'sid-example', 43 | redirectTo: '/login', 44 | isSecure: false 45 | }); 46 | 47 | server.route([{ 48 | method: 'GET', 49 | path: '/', 50 | config: { 51 | auth: 'session', 52 | handler: { 53 | view: 'index' 54 | } 55 | } 56 | }, { 57 | method: 'GET', 58 | path: '/login', 59 | handler: { 60 | view: 'login' 61 | } 62 | }, { 63 | method: 'POST', 64 | path: '/login', 65 | config: { 66 | validate: { 67 | payload: { 68 | email: Joi.string().email().required(), 69 | password: Joi.string().required(), 70 | enableTfa: Joi.boolean().default(false) 71 | } 72 | } 73 | }, 74 | handler: function (request, reply) { 75 | 76 | const email = request.payload.email; 77 | const password = request.payload.password; 78 | const user = users[email]; 79 | 80 | if (!user) { 81 | return reply(Boom.unauthorized()); 82 | } 83 | 84 | Bcrypt.compare(password, user.password, (err, valid) => { 85 | 86 | if (err || !valid) { 87 | return reply(Boom.unauthorized()); 88 | } 89 | 90 | if (request.payload.enableTfa || user.requireTfa) { 91 | return reply.redirect('/authy').state('authy', { 92 | email: email, 93 | authyId: user.authyId 94 | }); 95 | } 96 | 97 | request.auth.session.set(user); 98 | return reply.redirect('/'); 99 | }); 100 | } 101 | }]); 102 | 103 | // Authy 2FA stage 104 | 105 | server.auth.strategy('authy', 'authy', { 106 | apiKey: 'AUTHY_API_KEY', 107 | sandbox: false, 108 | cookieOptions: { 109 | isSecure: false, 110 | path: '/', 111 | encoding: 'iron', 112 | password: 'password' 113 | } 114 | }); 115 | 116 | server.route({ 117 | method: ['GET', 'POST'], 118 | path: '/authy', 119 | config: { 120 | auth: { 121 | strategies: ['authy'], 122 | payload: true 123 | }, 124 | handler: function (request, reply) { 125 | 126 | const credentials = request.auth.credentials; 127 | const user = users[credentials.email]; 128 | user.requireTfa = true; 129 | user.authyId = credentials.authyId; 130 | request.auth.session.set(user); 131 | return reply.redirect('/'); 132 | } 133 | } 134 | }); 135 | 136 | server.start(() => { 137 | 138 | console.log('Started server'); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /examples/basic/templates/authy/register.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Register for Two-Factor Authentication

4 |
5 |
6 | 7 | + 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 |
-------------------------------------------------------------------------------- /examples/basic/templates/authy/verify.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Enter Two-Factor Authentication Token

4 |
5 |
6 | 7 | 8 |
9 |
10 | Request Token 11 |
12 | 13 |
14 |
15 |
-------------------------------------------------------------------------------- /examples/basic/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Logged in!

4 |
5 |
-------------------------------------------------------------------------------- /examples/basic/templates/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2FA Example 6 | 7 | 8 | 9 | {{{content}}} 10 | 11 | -------------------------------------------------------------------------------- /examples/basic/templates/login.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 17 |
18 | 19 |
20 |
21 |
-------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boom = require('boom'); 4 | const Hoek = require('hoek'); 5 | const Joi = require('joi'); 6 | const Package = require('./package'); 7 | 8 | const internals = { 9 | defaults: { 10 | register: (request, reply) => { 11 | 12 | reply.view('authy/register', { path: request.path }); 13 | }, 14 | verify: (request, reply) => { 15 | 16 | reply.view('authy/verify', { 17 | path: request.path, 18 | requestTokenUrl: request.plugins.authy.requestTokenUrl 19 | }); 20 | }, 21 | failRegister: (err, request, reply) => { 22 | 23 | reply(Boom.unauthorized('Could\'t register user')); 24 | }, 25 | failVerify: (err, request, reply) => { 26 | 27 | reply(Boom.unauthorized('Could\'t validate token')); 28 | }, 29 | tokenRequested: (err, request, reply) => { 30 | 31 | reply.redirect(request.query.returnUrl); 32 | } 33 | } 34 | }; 35 | 36 | 37 | internals.schemeOptionsSchema = { 38 | apiKey: Joi.string().required(), 39 | cookieName: Joi.string().default('authy'), 40 | requestTokenUrl: Joi.string().default('/authy-request-token'), 41 | sandbox: Joi.boolean().default(false), 42 | cookieOptions: Joi.object().keys({ 43 | encoding: Joi.string().valid('iron') 44 | }).options({ allowUnknown: true }), 45 | sandboxUrl: Joi.string().default('http://sandbox-api.authy.com'), 46 | funcs: Joi.object().keys({ 47 | register: Joi.func().default(internals.defaults.register), 48 | verify: Joi.func().default(internals.defaults.verify), 49 | failRegister: Joi.func().default(internals.defaults.failRegister), 50 | failVerify: Joi.func().default(internals.defaults.failVerify), 51 | tokenRequested: Joi.func().default(internals.defaults.tokenRequested) 52 | }).default({ 53 | register: internals.defaults.register, 54 | verify: internals.defaults.verify, 55 | failRegister: internals.defaults.failRegister, 56 | failVerify: internals.defaults.failVerify, 57 | tokenRequested: internals.defaults.tokenRequested 58 | }), 59 | client: Joi.func().default(require('authy')), 60 | requestTokenRouteConfig: Joi.object().default({}) 61 | }; 62 | 63 | 64 | internals.scheme = function (server, options) { 65 | 66 | const result = Joi.validate(options, internals.schemeOptionsSchema); 67 | Hoek.assert(!result.error, result.error); 68 | const settings = result.value; 69 | const authy = settings.client(settings.apiKey, settings.sandbox ? settings.sandboxUrl : null); 70 | 71 | server.state(settings.cookieName, settings.cookieOptions); 72 | 73 | server.route({ 74 | config: settings.requestTokenRouteConfig, 75 | method: 'GET', 76 | path: settings.requestTokenUrl, 77 | handler: function (request, reply) { 78 | 79 | authy.request_sms(request.state[settings.cookieName].authyId, (err, res) => { 80 | 81 | settings.funcs.tokenRequested(err, request, reply); 82 | }); 83 | } 84 | }); 85 | 86 | return { 87 | authenticate: function (request, reply) { 88 | 89 | request.plugins.authy = request.plugins.authy || {}; 90 | request.plugins.authy.requestTokenUrl = settings.requestTokenUrl; 91 | 92 | const cookie = request.state[settings.cookieName]; 93 | 94 | if (!cookie) { 95 | return reply(Boom.unauthorized('Missing authy cookie')); 96 | } 97 | 98 | // Route to appropriate stage 99 | 100 | if (request.method === 'get') { 101 | if (!cookie.authyId) { 102 | return settings.funcs.register(request, reply); 103 | } 104 | 105 | if (!cookie.verified) { 106 | return settings.funcs.verify(request, reply); 107 | } 108 | } 109 | 110 | // Success 111 | 112 | reply.continue({ credentials: cookie }); 113 | }, 114 | payload: function (request, reply) { 115 | 116 | const cookie = request.state[settings.cookieName]; 117 | const payload = request.payload; 118 | 119 | // Registration payload 120 | 121 | if (!cookie.authyId) { 122 | const schema = { 123 | country: Joi.number().required(), 124 | phone: Joi.number().required() 125 | }; 126 | 127 | const payloadResult = Joi.validate(request.payload, schema); 128 | 129 | if (payloadResult.error) { 130 | 131 | return settings.funcs.failRegister(payloadResult.error, request, reply); 132 | } 133 | 134 | return authy.register_user(cookie.email, payload.phone, payload.country, true, (err, res) => { 135 | 136 | if (err) { 137 | return settings.funcs.failRegister(err, request, reply); 138 | } 139 | 140 | cookie.authyId = res.user.id; 141 | reply.redirect(request.path).state(settings.cookieName, cookie); 142 | }); 143 | } 144 | 145 | // Verification payload 146 | 147 | const schema = { token: Joi.number().required() }; 148 | const payloadResult = Joi.validate(request.payload, schema); 149 | 150 | if (payloadResult.error) { 151 | return settings.funcs.failVerify(payloadResult.error, request, reply); 152 | } 153 | 154 | return authy.verify(cookie.authyId, payload.token, (err, res) => { 155 | 156 | if (err) { 157 | return settings.funcs.failVerify(err, request, reply); 158 | } 159 | 160 | cookie.verified = true; 161 | reply.redirect(request.path).state(settings.cookieName, cookie); 162 | }); 163 | } 164 | }; 165 | }; 166 | 167 | 168 | exports.register = function (server, options, next) { 169 | 170 | server.auth.scheme('authy', internals.scheme); 171 | next(); 172 | }; 173 | 174 | 175 | exports.register.attributes = { name: Package.name, version: Package.version }; 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-authy", 3 | "version": "1.0.4", 4 | "description": "Authy 2FA with hapi", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "lab -a code -t 100 -L" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/mtharrison/hapi-authy.git" 12 | }, 13 | "keywords": [ 14 | "2fa", 15 | "two-factor", 16 | "auth", 17 | "authentication", 18 | "hapi", 19 | "security" 20 | ], 21 | "engines": { 22 | "node": ">=4.0.0" 23 | }, 24 | "author": "Matt Harrison", 25 | "license": "BSD-3-Clause", 26 | "bugs": { 27 | "url": "https://github.com/mtharrison/hapi-authy/issues" 28 | }, 29 | "homepage": "https://github.com/mtharrison/hapi-authy#readme", 30 | "devDependencies": { 31 | "bcryptjs": "^2.3.0", 32 | "code": "^2.0.1", 33 | "handlebars": "^4.0.4", 34 | "hapi": "^11.0.3", 35 | "hapi-auth-cookie": "^3.1.0", 36 | "iron": "^2.1.3", 37 | "lab": "^7.1.0", 38 | "vision": "^3.0.0" 39 | }, 40 | "dependencies": { 41 | "authy": "^1.1.2", 42 | "boom": "^2.10.0", 43 | "hoek": "^3.0.0", 44 | "joi": "^6.10.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Load modules 4 | 5 | const Code = require('code'); 6 | const Hapi = require('hapi'); 7 | const Iron = require('iron'); 8 | const Lab = require('lab'); 9 | const Path = require('path'); 10 | 11 | 12 | // Declare internals 13 | 14 | const internals = { 15 | password: 'Q3QJIcIIvKcMwG7c' 16 | }; 17 | 18 | 19 | // Test shortcuts 20 | 21 | const lab = exports.lab = Lab.script(); 22 | const describe = lab.describe; 23 | const it = lab.it; 24 | const expect = Code.expect; 25 | const beforeEach = lab.beforeEach; 26 | 27 | 28 | internals.makeCookie = function (obj, callback) { 29 | 30 | Iron.seal(obj, internals.password, Iron.defaults, (err, sealed) => { 31 | 32 | callback(sealed); 33 | }); 34 | }; 35 | 36 | 37 | internals.mockClient = function () { 38 | 39 | return { 40 | request_sms: function (id, callback) { 41 | 42 | callback(); 43 | }, 44 | register_user: function (email, phone, country, sms, callback) { 45 | 46 | callback(internals.clientError, { 47 | user: { id: 123456 } 48 | }); 49 | }, 50 | verify: function (id, token, callback) { 51 | 52 | callback(internals.clientError, { 53 | user: { id: 123456 } 54 | }); 55 | } 56 | }; 57 | }; 58 | 59 | 60 | describe('hapi-authy', () => { 61 | 62 | let server; 63 | 64 | beforeEach((done) => { 65 | 66 | internals.clientError = null; 67 | 68 | server = new Hapi.Server(); 69 | server.connection({ port: 4000 }); 70 | server.register(require('vision'), (err) => {}); 71 | server.register(require('../'), (err) => {}); 72 | 73 | server.auth.strategy('authy', 'authy', { 74 | apiKey: 'aDfI6YR2qFF6Klsl6eEJTBLqAfphO9AG', 75 | sandbox: false, 76 | cookieOptions: { 77 | isSecure: false, 78 | path: '/', 79 | encoding: 'iron', 80 | password: 'Q3QJIcIIvKcMwG7c' 81 | }, 82 | client: internals.mockClient 83 | }); 84 | 85 | server.views({ 86 | engines: { 87 | hbs: require('handlebars') 88 | }, 89 | path: Path.join(__dirname, 'templates') 90 | }); 91 | 92 | server.route({ 93 | method: ['GET', 'POST'], 94 | path: '/authy', 95 | config: { 96 | auth: { 97 | strategies: ['authy'], 98 | payload: true 99 | }, 100 | handler: function (request, reply) { 101 | 102 | reply('SUCESS!'); 103 | } 104 | } 105 | }); 106 | 107 | done(); 108 | }); 109 | 110 | it('expects an cookie to be set', (done) => { 111 | 112 | server.inject('/authy', (res) => { 113 | 114 | expect(res.statusCode).to.equal(401); 115 | expect(res.result.message).to.equal('Missing authy cookie'); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('passes through when verified', (done) => { 121 | 122 | const obj = { 123 | email: 'bob@jones.com', 124 | authyId: 123456, 125 | verified: true 126 | }; 127 | 128 | internals.makeCookie(obj, (cookie) => { 129 | 130 | server.inject({ 131 | method: 'GET', 132 | url: 'http://localhost:4000/authy', 133 | headers: { 134 | cookie: 'authy=' + cookie 135 | } 136 | }, (res) => { 137 | 138 | expect(res.statusCode).to.equal(200); 139 | expect(res.result).to.equal('SUCESS!'); 140 | done(); 141 | }); 142 | }); 143 | }); 144 | 145 | it('can be set to sandbox', (done) => { 146 | 147 | server.auth.strategy('authy2', 'authy', { 148 | apiKey: 'aDfI6YR2qFF6Klsl6eEJTBLqAfphO9AG', 149 | sandbox: true, 150 | cookieName: 'authy2', 151 | requestTokenUrl: '/request', 152 | cookieOptions: { 153 | isSecure: false, 154 | path: '/', 155 | encoding: 'iron', 156 | password: 'Q3QJIcIIvKcMwG7c' 157 | }, 158 | client: internals.mockClient 159 | }); 160 | 161 | server.inject('/authy', (res) => { 162 | 163 | expect(res.statusCode).to.equal(401); 164 | expect(res.result.message).to.equal('Missing authy cookie'); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('prompts for registration if required', (done) => { 170 | 171 | const obj = { 172 | email: 'bob@jones.com', 173 | authyId: null 174 | }; 175 | 176 | internals.makeCookie(obj, (cookie) => { 177 | 178 | server.inject({ 179 | method: 'GET', 180 | url: 'http://localhost:4000/authy', 181 | headers: { 182 | cookie: 'authy=' + cookie 183 | } 184 | }, (res) => { 185 | 186 | expect(res.statusCode).to.equal(200); 187 | expect(res.result).to.equal('register'); 188 | done(); 189 | }); 190 | }); 191 | }); 192 | 193 | it('prompts for verification if required', (done) => { 194 | 195 | const obj = { 196 | email: 'bob@jones.com', 197 | authyId: 123456 198 | }; 199 | 200 | internals.makeCookie(obj, (cookie) => { 201 | 202 | server.inject({ 203 | method: 'GET', 204 | url: 'http://localhost:4000/authy', 205 | headers: { 206 | cookie: 'authy=' + cookie 207 | } 208 | }, (res) => { 209 | 210 | expect(res.statusCode).to.equal(200); 211 | expect(res.result).to.equal('verify'); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | 217 | it('performs registration on proper payload', (done) => { 218 | 219 | const obj = { 220 | email: 'bob@jones.com', 221 | authyId: null 222 | }; 223 | 224 | internals.makeCookie(obj, (cookie) => { 225 | 226 | server.inject({ 227 | method: 'POST', 228 | url: 'http://localhost:4000/authy', 229 | headers: { 230 | cookie: 'authy=' + cookie 231 | }, 232 | payload: JSON.stringify({ country: '1', phone: '123546789' }) 233 | }, (res) => { 234 | 235 | expect(res.statusCode).to.equal(302); 236 | done(); 237 | }); 238 | }); 239 | }); 240 | 241 | it('fails registration on client error', (done) => { 242 | 243 | internals.clientError = new Error('error'); 244 | 245 | const obj = { 246 | email: 'bob@jones.com', 247 | authyId: null 248 | }; 249 | 250 | internals.makeCookie(obj, (cookie) => { 251 | 252 | server.inject({ 253 | method: 'POST', 254 | url: 'http://localhost:4000/authy', 255 | headers: { 256 | cookie: 'authy=' + cookie 257 | }, 258 | payload: JSON.stringify({ country: '1', phone: '123546789' }) 259 | }, (res) => { 260 | 261 | expect(res.statusCode).to.equal(401); 262 | done(); 263 | }); 264 | }); 265 | }); 266 | 267 | it('fails registration on bad payload', (done) => { 268 | 269 | const obj = { 270 | email: 'bob@jones.com', 271 | authyId: null 272 | }; 273 | 274 | internals.makeCookie(obj, (cookie) => { 275 | 276 | server.inject({ 277 | method: 'POST', 278 | url: 'http://localhost:4000/authy', 279 | headers: { 280 | cookie: 'authy=' + cookie 281 | }, 282 | payload: JSON.stringify({ a: 1 }) 283 | }, (res) => { 284 | 285 | expect(res.statusCode).to.equal(401); 286 | expect(res.result.message).to.equal('Could\'t register user'); 287 | done(); 288 | }); 289 | }); 290 | }); 291 | 292 | it('performs verification on proper payload', (done) => { 293 | 294 | const obj = { 295 | email: 'bob@jones.com', 296 | authyId: 123456 297 | }; 298 | 299 | internals.makeCookie(obj, (cookie) => { 300 | 301 | server.inject({ 302 | method: 'POST', 303 | url: 'http://localhost:4000/authy', 304 | headers: { 305 | cookie: 'authy=' + cookie 306 | }, 307 | payload: JSON.stringify({ token: '1234567' }) 308 | }, (res) => { 309 | 310 | expect(res.statusCode).to.equal(302); 311 | done(); 312 | }); 313 | }); 314 | }); 315 | 316 | it('fails verification on client error', (done) => { 317 | 318 | internals.clientError = new Error('error'); 319 | 320 | const obj = { 321 | email: 'bob@jones.com', 322 | authyId: 123456 323 | }; 324 | 325 | internals.makeCookie(obj, (cookie) => { 326 | 327 | server.inject({ 328 | method: 'POST', 329 | url: 'http://localhost:4000/authy', 330 | headers: { 331 | cookie: 'authy=' + cookie 332 | }, 333 | payload: JSON.stringify({ token: '1234567' }) 334 | }, (res) => { 335 | 336 | expect(res.statusCode).to.equal(401); 337 | done(); 338 | }); 339 | }); 340 | }); 341 | 342 | it('fails verification on bad payload', (done) => { 343 | 344 | const obj = { 345 | email: 'bob@jones.com', 346 | authyId: 123456 347 | }; 348 | 349 | internals.makeCookie(obj, (cookie) => { 350 | 351 | server.inject({ 352 | method: 'POST', 353 | url: 'http://localhost:4000/authy', 354 | headers: { 355 | cookie: 'authy=' + cookie 356 | }, 357 | payload: JSON.stringify({ a: 1 }) 358 | }, (res) => { 359 | 360 | expect(res.statusCode).to.equal(401); 361 | expect(res.result.message).to.equal('Could\'t validate token'); 362 | done(); 363 | }); 364 | }); 365 | }); 366 | 367 | it('can request a token', (done) => { 368 | 369 | const obj = { 370 | email: 'bob@jones.com', 371 | authyId: 123456 372 | }; 373 | 374 | internals.makeCookie(obj, (cookie) => { 375 | 376 | server.inject({ 377 | url: '/authy-request-token', 378 | headers: { 379 | cookie: 'authy=' + cookie 380 | } 381 | }, (res) => { 382 | 383 | expect(res.statusCode).to.equal(302); 384 | done(); 385 | }); 386 | }); 387 | }); 388 | 389 | it('request.plugins doesn\'t get clobbered', (done) => { 390 | 391 | server.ext('onPreAuth', (request, reply) => { 392 | 393 | request.plugins.authy = { a: 1 }; 394 | reply.continue(); 395 | }); 396 | 397 | server.inject('/authy', (res) => { 398 | 399 | expect(res.statusCode).to.equal(401); 400 | expect(res.result.message).to.equal('Missing authy cookie'); 401 | done(); 402 | }); 403 | }); 404 | }); 405 | -------------------------------------------------------------------------------- /test/templates/authy/register.hbs: -------------------------------------------------------------------------------- 1 | register -------------------------------------------------------------------------------- /test/templates/authy/verify.hbs: -------------------------------------------------------------------------------- 1 | verify --------------------------------------------------------------------------------