Skip to content

Commit

Permalink
Merge pull request #62 from dwyl/cookie-support-expansion-pack
Browse files Browse the repository at this point in the history
Cookie support expansion pack
  • Loading branch information
benjaminlees committed Jul 20, 2015
2 parents 00ee71b + f3b9bea commit 227d932
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 129 deletions.
153 changes: 104 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# Hapi Auth with JSON Web Tokens (JWT)

The ***simplest*** authentication scheme/plugin for
[Hapi.js](http://hapijs.com/) apps using JSON Web Tokens.
***The*** authentication scheme/plugin for
[**Hapi.js**](http://hapijs.com/) apps using **JSON Web Tokens**

[![Build Status](https://travis-ci.org/dwyl/hapi-auth-jwt2.svg "Build Status = Tests Passing")](https://travis-ci.org/dwyl/hapi-auth-jwt2)
[![Test Coverage](https://codeclimate.com/github/dwyl/hapi-auth-jwt2/badges/coverage.svg "All Lines Tested")](https://codeclimate.com/github/dwyl/hapi-auth-jwt2)
[![Code Climate](https://codeclimate.com/github/dwyl/hapi-auth-jwt2/badges/gpa.svg "No Nasty Code")](https://codeclimate.com/github/dwyl/hapi-auth-jwt2)
[![bitHound Score](https://www.bithound.io/github/dwyl/hapi-auth-jwt2/badges/score.svg)](https://www.bithound.io/github/dwyl/hapi-auth-jwt2)
[![Dependency Status](https://david-dm.org/dwyl/hapi-auth-jwt2.svg "Dependencies Checked & Updated Regularly (Security is Important!)")](https://david-dm.org/dwyl/hapi-auth-jwt2)
[![Node.js Version](https://img.shields.io/node/v/hapi-auth-jwt2.svg?style=flat "Node.js 10 & 12 and io.js latest both supported")](http://nodejs.org/download/)
[![NPM Version](https://badge.fury.io/js/hapi-auth-jwt2.svg?style=flat)](https://npmjs.org/package/hapi-auth-jwt2)
[![HAPI 8.5](http://img.shields.io/badge/hapi-8.6-brightgreen.svg "Latest Hapi.js")](http://hapijs.com)
[![npm](https://img.shields.io/npm/v/hapi-auth-jwt2.svg)](https://www.npmjs.com/package/hapi-auth-jwt2)
[![HAPI 8.8](http://img.shields.io/badge/hapi-8.8-brightgreen.svg "Latest Hapi.js")](http://hapijs.com)


This node.js module (Hapi plugin) lets you use JSON Web Tokens (JWTs)
for authentication in your [Hapi.js](http://hapijs.com/)
web application.

If you are totally new to JWTs, we wrote an introductory post explaining
the concepts & benefits: https://github.com/docdis/learn-json-web-tokens
the concepts & benefits: https://github.com/dwyl/learn-json-web-tokens

If you (or anyone on your team) are unfamiliar with **Hapi.js** we have a
quick guide for that too: https://github.com/nelsonic/learn-hapi
Expand All @@ -27,7 +27,6 @@ quick guide for that too: https://github.com/nelsonic/learn-hapi

We tried to make this plugin as user (developer) friendly as possible,
but if anything is unclear, please submit any questions as issues on GitHub:

https://github.com/dwyl/hapi-auth-jwt2/issues

### Install from NPM
Expand Down Expand Up @@ -130,42 +129,6 @@ That's it.
Write your own `validateFunc` with what ever checks you want to perform
on the **decoded** token before allowing the visitor to proceed.

### Real World Example ?

If you would like to see a "***real world example***" of this plugin in use
in a ***production*** web app (API)
please see: https://github.com/dwyl/time/tree/master/api/lib

+ **app.js** ***registering*** the **hapi-auth-jwt2 plugin**:
[app.js#L13](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/app.js#L13)
+ telling app.js where to find our **validateFunc**tion:
[app.js#L21](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/app.js#L21)
+ **validateFunc**tion (how we check the JWT is still valid):
[api/lib/auth_jwt_validate.js](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_validate.js) looks up the person's session in our ElasticSearch Database
if the [session record is ***found*** (valid) and ***not ended***](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_validate.js#L12) we allow the person to see the restricted content.
+ **Signing your JWTs**: in your app you need a method to *sign* the JWTs (and put them in a database
if that's how you are *verifying* your sessions) ours is:
[api/lib/auth_jwt_sign.js](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_sign.js#L18)

If you have ***any questions*** on this please post an issue/question on GitHub:
https://github.com/dwyl/hapi-auth-jwt2/issues
(*we are here to help get you started on your journey to **hapi**ness!*)

### Production-ready Example using Redis?

Redis is *perfect* for storing session data that needs to be checked
on every authenticated request.

If you are unfamiliar with Redis or anyone on your team needs a refresher,
please checkout: https://github.com/docdis/learn-redis

The ***code*** is at: https://github.com/dwyl/hapi-auth-jwt2-example
and with tests. please ask additional questions if unclear!

Having a more real-world example was *seconded* by [@manonthemat](https://github.com/manonthemat) see:
[hapi-auth-jwt2/issues/9](https://github.com/dwyl/hapi-auth-jwt2/issues/9)


## Documentation

- `key` - (***required***) the secret key used to check the signature of the token or a key lookup function with
Expand All @@ -190,6 +153,8 @@ signature `function(decoded, callback)` where:
- `issuer` - do not require the issuer to be valid
- `algorithms` - list of allowed algorithms
- `url tokens` - if you prefer to pass your token in via url, simply add a `token` url parameter to your reqest.
- `cookie token` - If you prefer to use cookies in your hapi.js app,
simply set the cookie `token=your.jsonwebtoken.here`

### verifyOptions let you define how to Verify the Tokens (*Optional*)

Expand Down Expand Up @@ -265,8 +230,54 @@ var url = "/path?token="+token;

There are _several_ options for generating secret keys.
The _easist_ way is to simply copy paste a _**strong random string**_ of alpha-numeric characters from https://www.grc.com/passwords.htm
(_if you want more a longer key simply refresh the page and copy-paste multiple random strings_)
(_if you want a longer key simply refresh the page and copy-paste multiple random strings_)

## Want to send/store your JWT in a Cookie?

[@benjaminlees](https://github.com/benjaminlees)
requested the ability to send tokens as cookies:
https://github.com/dwyl/hapi-auth-jwt2/issues/55
So we added the ability to *optionally* send/store your tokens in cookies
to simplify building your *web app*.

To enable cookie support in your application all you need to do is add
a few lines to your code:

### Cookie Options

Firstly set the options you want to apply to your cookie:

```js
var cookie_options = {
ttl: 365 * 24 * 60 * 60 * 1000, // expires a year from today
encoding: 'none', // we already used JWT to encode
isSecure: true, // warm & fuzzy feelings
isHttpOnly: true, // prevent client alteration
clearInvalid: false, // remove invalid cookies
strictHeader: true // don't allow violations of RFC 6265
}
```

### Set the Cookie on your `reply`

Then, in your authorisation handler

```js
reply({text: 'You have been authenticated!'})
.header("Authorization", token) // where token is the JWT
.state("token", token, cookie_options) // set the cookie with options
```

For a detailed example please see:
https://github.com/nelsonic/hapi-auth-jwt2-cookie-example

### Backgroun Reading

+ Wikipedia has a good intro (general): https://en.wikipedia.org/wiki/HTTP_cookie
+ Cookies Explained (by Nicholas C. Zakas - JavaScript über-master) http://www.nczonline.net/blog/2009/05/05/http-cookies-explained/
+ The Unofficial Cookie FAQ: http://www.cookiecentral.com/faq/
+ HTTP State Management Mechanism (long but complete spec):
http://tools.ietf.org/html/rfc6265

- - -

Expand All @@ -286,6 +297,47 @@ we *recommend* including it in your **package.json** ***explicitly*** as a **dep
> *If you have a question*, ***please post an issue/question on GitHub***:
https://github.com/dwyl/hapi-auth-jwt2/issues

<br />
<br />

### Real World Example ?

If you would like to see a "***real world example***" of this plugin in use
in a ***production*** web app (API)
please see: https://github.com/dwyl/time/tree/master/api/lib

+ **app.js** ***registering*** the **hapi-auth-jwt2 plugin**:
[app.js#L13](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/app.js#L13)
+ telling app.js where to find our **validateFunc**tion:
[app.js#L21](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/app.js#L21)
+ **validateFunc**tion (how we check the JWT is still valid):
[api/lib/auth_jwt_validate.js](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_validate.js) looks up the person's session in our ElasticSearch Database
if the [session record is ***found*** (valid) and ***not ended***](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_validate.js#L12) we allow the person to see the restricted content.
+ **Signing your JWTs**: in your app you need a method to *sign* the JWTs (and put them in a database
if that's how you are *verifying* your sessions) ours is:
[api/lib/auth_jwt_sign.js](https://github.com/dwyl/time/blob/0a5ec8711840528a4960c388825fb883fabddd76/api/lib/auth_jwt_sign.js#L18)

If you have ***any questions*** on this please post an issue/question on GitHub:
https://github.com/dwyl/hapi-auth-jwt2/issues
(*we are here to help get you started on your journey to **hapi**ness!*)

<br />

### Production-ready Example using Redis?

Redis is *perfect* for storing session data that needs to be checked
on every authenticated request.

If you are unfamiliar with Redis or anyone on your team needs a refresher,
please checkout: https://github.com/dwyl/learn-redis

The ***code*** is at: https://github.com/dwyl/hapi-auth-jwt2-example
and with tests. please ask additional questions if unclear!

Having a more real-world example was *seconded* by [@manonthemat](https://github.com/manonthemat) see:
[hapi-auth-jwt2/issues/9](https://github.com/dwyl/hapi-auth-jwt2/issues/9)


- - -

## Contributing [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/hapi-auth-jwt2/issues)
Expand Down Expand Up @@ -342,8 +394,10 @@ Think of our module as the "***new, simplified and actively maintained version**

## Useful Links

For more background on JWT see our post:
https://github.com/docdis/learn-json-web-tokens
+ For more background on jsonwebtokens (JWTs) see our detailed overview:
https://github.com/dwyl/learn-json-web-tokens
+ Securing Hapi Client Side Sessions:
https://blog.liftsecurity.io/2014/11/26/securing-hapi-client-side-sessions

### Hapi.js Auth

Expand All @@ -354,7 +408,8 @@ We borrowed code from the following:
+ https://github.com/hapijs/hapi-auth-cookie
+ https://github.com/hapijs/hapi-auth-hawk
+ https://github.com/ryanfitz/hapi-auth-jwt
(Ryan has made a good *starting point* - we tried to submit a [pull request](https://github.com/ryanfitz/hapi-auth-jwt/pull/27)
to improve it but got *ignored* ... an *authentication* plugin that [***ignores
security updates***](https://github.com/ryanfitz/hapi-auth-jwt/issues/26) in [dependencies](https://david-dm.org/ryanfitz/hapi-auth-jwt)
is a ***no-go*** for us; **security *matters***!)
(Ryan made a good *start* - however, when we tried to submit a [pull request](https://github.com/ryanfitz/hapi-auth-jwt/pull/27)
to improve (_security_) it was *ignored* for _weeks_ ... an *authentication* plugin that [***ignores security updates***](https://github.com/ryanfitz/hapi-auth-jwt/issues/26) in [dependencies](https://david-dm.org/ryanfitz/hapi-auth-jwt)
is a ***no-go*** for us; **security *matters***!) If you spot _any_
issue in ***hapi-auth-jwt2*** please create an issue: https://github.com/dwyl/hapi-auth-jwt2/issues
so we can get it _resolved_ ASAP!
26 changes: 12 additions & 14 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
var Boom = require('boom'); // error handling https://github.com/hapijs/boom
var Hoek = require('hoek'); // hapi utilities https://github.com/hapijs/hoek
var JWT = require('jsonwebtoken'); // https://github.com/docdis/learn-json-web-tokens
var pkg = require('../package.json');
var dir = __dirname.split('/')[__dirname.split('/').length-1];
var file = dir + __filename.replace(__dirname, '') + " -> ";
var pkg = require('../package.json');
var internals = {}; // Declare internals >> see: http://hapijs.com/styleguide

exports.register = function (server, options, next) {
server.auth.scheme('jwt', internals.implementation);
next();
};

exports.register.attributes = { // hapi requires attributes for a plugin.
pkg: require('../package.json') // See: http://hapijs.com/tutorials/plugins
exports.register.attributes = { // hapi requires attributes for a plugin.
pkg: pkg // See: http://hapijs.com/tutorials/plugins
};

internals.isFunction = function (functionToCheck) {
Expand All @@ -29,14 +27,19 @@ internals.implementation = function (server, options) {
var scheme = {
authenticate: function (request, reply) {
var auth;
// tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
if(request.query.token) {
if(request.query.token) { // tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
auth = request.query.token;
} // JWT tokens in cookie: https://github.com/dwyl/hapi-auth-jwt2/issues/55
else if (request.headers.cookie) {
var cookie = request.headers.cookie.replace(/token=/gi, '');
if(cookie.indexOf(';') > -1) { // cookie has options set
cookie = cookie.substring(0, cookie.indexOf(';'));
}
auth = cookie;
}
else {
auth = request.headers.authorization;
}

if (!auth && (request.auth.mode === 'optional' || request.auth.mode === 'try')) {
return reply.continue({ credentials: {} });
}
Expand All @@ -52,22 +55,17 @@ internals.implementation = function (server, options) {
}
else { // attempt to verify the token *asynchronously*
var keyFunc = (internals.isFunction(options.key)) ? options.key : function (decoded, callback) { callback(null, options.key); };

keyFunc(JWT.decode(token), function (err, key, extraInfo) {
if (err) {
return reply(Boom.wrap(err));
}

if (extraInfo) {
request.plugins[pkg.name] = { extraInfo: extraInfo };
}

var verifyOptions = options.verifyOptions || {};
JWT.verify(token, key, verifyOptions, function (err, decoded) {
if (err) {
// for 'try' mode we need to pass back the decoded token even if verification failed
if (err) { // for 'try' mode we need to pass back the decoded token even if verification failed
var credentials = JWT.decode(token);

if (err.name === 'TokenExpiredError') {
return reply(Boom.unauthorized('Token expired', 'Token'), null, { credentials: credentials });
}
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hapi-auth-jwt2",
"version": "4.6.0",
"version": "4.7.0",
"description": "Hapi.js Authentication Plugin/Scheme using JSON Web Tokens (JWT)",
"main": "lib/index.js",
"repository": {
Expand All @@ -27,21 +27,21 @@
"dependencies": {
"boom": "^2.8.0",
"hoek": "^2.14.0",
"jsonwebtoken": "^5.0.2"
"jsonwebtoken": "^5.0.4"
},
"peerDependencies": {
"hapi": ">=8.x.x"
},
"devDependencies": {
"aguid": "^1.0.3",
"hapi": "^8.6.1",
"codeclimate-test-reporter": "0.0.4",
"hapi": "^8.8.0",
"codeclimate-test-reporter": "^0.1.0",
"istanbul": "^0.3.17",
"jshint": "^2.8.0",
"pre-commit": "^1.0.10",
"redis": "^0.12.1",
"tap-spec": "^4.0.2",
"tape": "^4.0.0"
"tape": "^4.0.1"
},
"engines": {
"node": ">=0.10"
Expand Down
74 changes: 74 additions & 0 deletions test/cookies-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
var test = require('tape');
var JWT = require('jsonwebtoken');
var secret = 'NeverShareYourSecret';
var server = require('./server.js');
var cookie_options = '; Max-Age=31536000;' //' Expires=Mon, 18 Jul 2016 05:29:45 GMT; Secure; HttpOnly';

// var cookie_options = {
// ttl: 365 * 30 * 7 * 24 * 60 * 60 * 1000, // in the distant future ...
// encoding: 'none', // we already used JWT to encode
// isSecure: true, // warm & fuzzy feelings
// isHttpOnly: true, // prevent client alteration
// clearInvalid: false, // remove invalid cookies
// strictHeader: true // don't allow violations of RFC 6265
// }

test("Attempt to access restricted content using inVALID Cookie Token", function(t) {
var token = JWT.sign({ id:123,"name":"Charlie" }, 'badsecret');
var options = {
method: "POST",
url: "/privado",
headers: { cookie : "token=" + token}
};

server.inject(options, function(response) {
t.equal(response.statusCode, 401, "Invalid token should error!");
t.end();
});
});

test("Attempt to access restricted content with VALID Token but malformed Cookie", function(t) {
var token = JWT.sign({ id:123,"name":"Charlie" }, secret);
var options = {
method: "POST",
url: "/privado",
headers: { cookie : token }
};
// server.inject lets us similate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 400, "Valid Token but inVALID COOKIE should fial!");
t.end();
});
});

test("Access restricted content with VALID Token Cookie", function(t) {
var token = JWT.sign({ id:123,"name":"Charlie" }, secret);
var options = {
method: "POST",
url: "/privado",
headers: { cookie : "token=" + token }
};
// server.inject lets us similate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 200, "VALID COOKIE Token should succeed!");
t.end();
});
});

test("Access restricted content with VALID Token Cookie (With Options!)", function(t) {
var token = JWT.sign({ id:123,"name":"Charlie" }, secret);
var options = {
method: "POST",
url: "/privado",
headers: { cookie : "token=" + token + cookie_options }
};
// console.log(' - - - - - - - - - - - - - - - OPTIONS:')
// console.log(options);
// server.inject lets us similate an http request
server.inject(options, function(response) {
// console.log(' - - - - - - - - - - - - - - - response:')
// console.log(response);
t.equal(response.statusCode, 200, "VALID COOKIE Token (With Options!) should succeed!");
t.end();
});
});
Loading

0 comments on commit 227d932

Please sign in to comment.