Skip to content

Commit

Permalink
Merge f170d05 into d15a53c
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewglover committed Oct 4, 2016
2 parents d15a53c + f170d05 commit f6da649
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 25 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,52 @@ A hapi authentication plugin using Json Web Tokens.
To create a simple way to add web token authentication to routes, and to learn more about JWTs, Hapi plugins, auth schemes and strategies.

## How
tbd.
To install, run npm install --save @matthewglover/hapi-jwt.

A simple implementation:

```javascript
const { createServer, setConnection, registerPlugins, addRoutes, startServer } =
require('@matthewglover/hapi-wrapper');
const hapiJwt = require('@matthewglover/hapi-jwt');

const options = {
strategyName: 'jwt', // Name of strategy (defaults to jwt)
createTokenPath: '/create-token', // Path for token creation
prepareTokenData: req => req.query, // Function to prepare token payload data
issueTokenPath: '/issue-token', // Path which will issue token (as /issue-token.html?jwt=[token])
verifyTokenPath: '/verify-token', // Path which will verify token (as /verify-token?jwt=[token])
jwtOptions: { algorithm: 'HS256' }, // jwt creation options (as per jsonwebtoken.sign)
jwtSecret: 'your-secret', // secret for creating token
validateCredentials: v => v, // Function to validateCredentials decoded from payload
};

const issueTokenRoute = {
method: 'GET',
path: '/issue-token',
handler: (req, reply) => reply(req.query),
}

createServer()
.then(setConnection({ port: 3000 }))
.then(registerPlugins([{ register: hapiJwt, options }]))
.then(addRoutes([issueTokenRoute]))
.then(startServer)
.then(s => console.log(`Server running at: ${s.info.uri}`))
.catch(err => console.error(err));

```

The only required option properties are:

- `jwtSecret` - your private secret used to encrypt the token
- `issueTokenPath` - the path to receive the json web token (passed as jwt=[token])

The following params are optional:

- `strategyName` - (default `jwt`) the name associated with your strategy
- `createTokenPath` - (default `/create-token`) the path which will create the token
- `prepareTokenData` - (default `req => req.query`) a function to prepare any data before being encoded (recieves the Hapi request object)
- `verifyTokenPath` - (default `/verify-token`) a path which will verify the token (expects token to be passed as jwt=[token])
- `jwtOptions` - (default `{ algorithm: 'HS256' }`) the jwt options (as per jsonwebtoken.sign)
- `validateCredentials` - (default `v => v`) a function to validate decoded payload of valid jwt
10 changes: 5 additions & 5 deletions integration/jwt_plugin.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
require('env2')('./config.env');
const hapiJwt = require('../');

const validateCredentials = () => {
throw new Error('bummer');
};

/* eslint-disable max-len */
const options = {
strategyName: 'jwt', // Name of strategy (defaults to jwt)

createTokenPath: '/create-token', // Path for token creation
prepareTokenData: req => req.query, // Function to prepare token payload data

issueTokenPath: '/issue-token.html', // Path which will issue token (as /issue-token.html?jwt=[token])

verifyTokenPath: '/verify-token', // Path which will verify token (as /verify-token?jwt=[token])

jwtOptions: { algorithm: 'HS256' }, // jwt creation options (as per jsonwebtoken.sign)
jwtVerificationOptions: { algorithm: 'HS256' }, // jwt verification options (as per jsonwebtoken.verify)
jwtSecret: process.env.JWT_SECRET, // secret for creating token
validateCredentials, // Function to validateCredentials decoded from payload
};
/* eslint-enable max-len */

Expand Down
4 changes: 2 additions & 2 deletions lib/create_token_route.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const signJWT = require('../util/sign_jwt');
// jwtSecret: String,
// }

const JWT_DEFAULT_OPTIONS = { algorithm: 'HS256' };
// const JWT_DEFAULT_OPTIONS = { algorithm: 'HS256' };

// createTokenRoute :: Options -> Hapi.Route
const createTokenRoute = opts =>
Expand All @@ -17,7 +17,7 @@ const createTokenRoute = opts =>
path: opts.createTokenPath,
handler: (req, reply) =>
Promise.resolve(opts.prepareTokenData(req))
.then(signJWT(opts.jwtOptions || JWT_DEFAULT_OPTIONS, opts.jwtSecret))
.then(signJWT(opts.jwtOptions, opts.jwtSecret))
.then(token => reply.redirect(`${opts.issueTokenPath}?jwt=${token}`)),
});

Expand Down
25 changes: 22 additions & 3 deletions lib/register.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
const { merge, prop, identity } = require('ramda');
const createTokenRoute = require('./create_token_route');
const verifyTokenRoute = require('./verify_token_route');
const jwtScheme = require('./jwt_scheme');


const OPTION_DEFAULTS = {
jwtOptions: { algorithm: 'HS256' },
createTokenPath: '/create-token',
verifyTokenPath: '/verify-token',
strategyName: 'jwt',
jwtVerificationOptions: { algorithm: 'HS256' },
prepareTokenData: prop('query'),
validateCredentials: identity,
};

const applyDefaults = merge(OPTION_DEFAULTS);

// register :: (Hapi.Server, Options, Function) -> void
const register = (server, opts, next) => {
server.route([createTokenRoute(opts), verifyTokenRoute(opts)]);
if (!opts.jwtSecret) return next(new Error('Options must specify a jwtSecret'));
if (!opts.issueTokenPath) return next(new Error('Options must specify an issueTokenPath'));

const completeOpts = applyDefaults(opts);

server.route([createTokenRoute(completeOpts), verifyTokenRoute(completeOpts)]);
server.auth.scheme('jwtScheme', jwtScheme);
server.auth.strategy(opts.strategyName || 'jwt', 'jwtScheme', opts);
next();
server.auth.strategy(completeOpts.strategyName, 'jwtScheme', completeOpts);
return next();
};

module.exports = register;
5 changes: 1 addition & 4 deletions lib/verify_token_route.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ const wrapBoom = require('../util/wrap_boom');
// jwtVerificationOptions: Object?,
// }

const JWT_VERIFICATION_OPTIONS = { algorithm: 'HS256' };

// createTokenRoute :: Options -> Hapi.Route
const createTokenRoute = opts =>
({
method: 'GET',
path: opts.verifyTokenPath,
handler: (req, reply) =>
verifyJWT(opts.jwtVerificationOptions || JWT_VERIFICATION_OPTIONS,
opts.jwtSecret, req.query.jwt)
verifyJWT(opts.jwtOptions, opts.jwtSecret, req.query.jwt)
.then(reply)
.catch(compose(reply, wrapBoom({ statusCode: 400 }))),
});
Expand Down
31 changes: 31 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import test from 'ava';
import Hapi from 'hapi';
import { omit } from 'ramda';
import { createServer, setConnection, registerPlugins } from '@matthewglover/hapi-wrapper';
import HapiJwt from '../';

// server :: [Hapi.Plugin] -> Promise Hapi.Server Error
const server = plugins =>
createServer()
.then(setConnection())
.then(registerPlugins(plugins));

const options = {
jwtSecret: 'my-jwt-secret',
issueTokenPath: '/issue-token',
};

// eslint-disable-next-line max-len
test('plugin loads without error provided opts.jwtSecret and opts.issueTokenPath are specified', async t => {
t.true(await server([{ register: HapiJwt, options }]) instanceof Hapi.Server);
});

// eslint-disable-next-line max-len
test('plugin throws error if opts.jwtSecret not specified', async t => {
t.throws(server([{ register: HapiJwt, options: omit(['jwtSecret'], options) }]));
});

// eslint-disable-next-line max-len
test('plugin throws error if opts.issueTokenPath not specified', async t => {
t.throws(server([{ register: HapiJwt, options: omit(['issueTokenPath'], options) }]));
});
2 changes: 1 addition & 1 deletion test/lib/jwt_scheme/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test('extractAuthHeaderFromRequest gets value of req.headers.authorization', asy
t.is(await extractAuthHeaderFromRequest(req), 'Bearer my-token');
});

test('extractAuthHeaderFromRequest gets value of req.headers.authorization', async t => {
test('extractAuthHeaderFromRequest throws error if req.headers.authorization not set', async t => {
const req = { headers: {} };
t.throws(extractAuthHeaderFromRequest(req));
});
Expand Down
38 changes: 35 additions & 3 deletions test/lib/jwt_scheme/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'ava';
import { omit } from 'ramda';
import { omit, merge } from 'ramda';
import { createServer, setConnection, addRoutes } from '@matthewglover/hapi-wrapper';
import provisionInjectPromise from '../../../test_helpers/provision_inject_promise';
import signJWT from '../../../util/sign_jwt';
Expand Down Expand Up @@ -31,7 +31,7 @@ const testRoute = {
const testOptions = {
verificationOptions: { algorithm: 'HS256' },
jwtSecret: 'my-secret',
validateCredentials: (credentials) => Promise.resolve(credentials),
validateCredentials: credentials => Promise.resolve(credentials),
};

const failingCredentialsOptions =
Expand Down Expand Up @@ -75,7 +75,7 @@ test('jwtScheme blocks access if no token provided', async t => {
t.regex(reply.result.message, /Property authorization not found on object/);
});

test('jwtScheme blocks access if credentials fail validation', async t => {
test('jwtScheme blocks access if credentials fail jwt validation', async t => {
const validToken =
await signJWT({ algorithm: 'HS256' }, 'my-secret', testCredentials);

Expand All @@ -86,3 +86,35 @@ test('jwtScheme blocks access if credentials fail validation', async t => {
t.is(reply.statusCode, 400);
t.regex(reply.result.message, /Invalid credentials/);
});

// eslint-disable-next-line max-len
test('jwtScheme blocks access if credentials pass jwt validation but fail custom validation via Promise rejection', async t => {
const validToken =
await signJWT({ algorithm: 'HS256' }, 'my-secret', testCredentials);

const invalidateCredentials = () => Promise.reject(new Error('Invalid options'));
const invalidOptions = merge(testOptions, { validateCredentials: invalidateCredentials });

const headers = { Authorization: `Bearer ${validToken}` };
const server = await testServer([testRoute], invalidOptions);
const reply = await server.injectPromise({ method: 'GET', url: '/test', headers });

t.is(reply.statusCode, 400);
t.regex(reply.result.message, /Invalid options/);
});

// eslint-disable-next-line max-len
test('jwtScheme blocks access if credentials pass jwt validation but fail custom validation via thrown error', async t => {
const validToken =
await signJWT({ algorithm: 'HS256' }, 'my-secret', testCredentials);

const invalidateCredentials = () => { throw new Error('Invalid options'); };
const invalidOptions = merge(testOptions, { validateCredentials: invalidateCredentials });

const headers = { Authorization: `Bearer ${validToken}` };
const server = await testServer([testRoute], invalidOptions);
const reply = await server.injectPromise({ method: 'GET', url: '/test', headers });

t.is(reply.statusCode, 400);
t.regex(reply.result.message, /Invalid options/);
});
6 changes: 3 additions & 3 deletions test/lib/verify_token_route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const payload = {

const tokenOptions = {
verifyTokenPath: '/verify-token',
jwtVerificationOptions: { algorithm: 'HS256' },
jwtOptions: { algorithm: 'HS256' },
jwtSecret: 'jwt-secret',
};

Expand Down Expand Up @@ -76,11 +76,11 @@ test('verifyTokenRoute returns Boom error if jwt is an expired token', async t =
});

// eslint-disable-next-line max-len
test('verifyTokenRoute - jwtVerificationOptions can be undefined (defaults to { algorithm: "HS256" })', async t => {
test('verifyTokenRoute - jwtOptions can be undefined (defaults to { algorithm: "HS256" })', async t => {
const validToken =
await signJWT({ algorithm: 'HS256' }, 'jwt-secret', payload);

const tokenRoute = verifyTokenRoute(omit('jwtVerificationOptions', tokenOptions));
const tokenRoute = verifyTokenRoute(omit('jwtOptions', tokenOptions));
const server = await testServer([tokenRoute]);
const reply =
await server.injectPromise({
Expand Down
6 changes: 3 additions & 3 deletions util/p_prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const { curry } = require('ramda');
// pProp :: String -> Object -> Promise any Error
const pProp = curry((p, o) =>
new Promise((resolve, reject) =>
(o[p] !== undefined ?
resolve(o[p]) :
reject(new TypeError(`Property ${p} not found on object`)))));
(o[p] !== undefined
? resolve(o[p])
: reject(new TypeError(`Property ${p} not found on object`)))));

module.exports = pProp;

0 comments on commit f6da649

Please sign in to comment.