├── .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 [](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 | 
64 | 
65 | 
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 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/basic/templates/authy/verify.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Enter Two-Factor Authentication Token
4 |
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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------