Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added parameter to irgnore url token #146

Merged
merged 12 commits into from
Apr 30, 2016
62 changes: 31 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ 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
If you are totally new to JWTs, we wrote an introductory post explaining
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
If you (or anyone on your team) are unfamiliar with **Hapi.js** we have a
quick guide for that too: https://github.com/dwyl/learn-hapi

## Usage
Expand Down Expand Up @@ -172,7 +172,7 @@ signature `function(decoded, callback)` where:
- `validateFunc` - (***required***) the function which is run once the Token has been decoded with
signature `function(decoded, request, callback)` where:
- `decoded` - (***required***) is the ***decoded*** and ***verified*** JWT received from the client in **request.headers.authorization**
- `request` - (***required***) is the original ***request*** received from the client
- `request` - (***required***) is the original ***request*** received from the client
- `callback` - (***required***) a callback function with the signature `function(err, isValid, credentials)` where:
- `err` - an internal error.
- `valid` - `true` if the JWT was valid, otherwise `false`.
Expand All @@ -189,10 +189,10 @@ signature `function(decoded, callback)` where:
- `responseFunc` - (***optional***) function called to decorate the response with authentication headers before the response headers or payload is written where:
- `request` - the request object.
- `reply(err, response)`- is called if an error occurred
- `urlKey` - (***optional*** *defaults to* `'token'`) - if you prefer to pass your token via url, simply add a `token` url parameter to your request or use a custom parameter by setting `urlKey`
- `cookieKey` - (***optional*** *defaults to* `'token'`) if you prefer to set your own cookie key or your project has a cookie called `'token'` for another purpose, you can set a custom key for your cookie by setting `options.cookieKey='yourkeyhere'`.
- `headerKey` - (***optional*** *defaults to* `'authorization'`) - if you want to set a custom key for your header token use the `headerKey` option.
- `tokenType` - (***optional*** *defaults to noe*) allow custom token type, e.g. `Authorization: <tokenType> 12345678`.
- `urlKey` - (***optional*** *defaults to* `'token'`) - if you prefer to pass your token via url, simply add a `token` url parameter to your request or use a custom parameter by setting `urlKey`. To disable the url parameter set urlKey to `false` or ''.
- `cookieKey` - (***optional*** *defaults to* `'token'`) - if you prefer to set your own cookie key or your project has a cookie called `'token'` for another purpose, you can set a custom key for your cookie by setting `options.cookieKey='yourkeyhere'`. To disable cookies set cookieKey to `false` or ''.
- `headerKey` - (***optional*** *defaults to* `'authorization'`) - if you want to set a custom key for your header token use the `headerKey` option. To disable header token set headerKey to `false` or ''.
- `tokenType` - (***optional*** *defaults to none*) - allow custom token type, e.g. `Authorization: <tokenType> 12345678`.
- `complete` - (***optional*** *defaults to* `false`) - set to `true` to receive the complete token (`decoded.header`, `decoded.payload` and `decoded.signature`) as `decoded` argument to key lookup and `verifyFunc` callbacks (*not `validateFunc`*)


Expand Down Expand Up @@ -288,20 +288,20 @@ var url = "/path?token="+token;
```

> What if I want to *disable* the ability to pass JWTs in via the URL?
(*asked by* @bitcloud in [issue #146](https://github.com/dwyl/hapi-auth-jwt2/pull/146))
(*asked by* @bitcloud in [issue #146](https://github.com/dwyl/hapi-auth-jwt2/pull/146))
> simply set your `urlKey` to something *impossible* to guess see:
[*example*](https://github.com/dwyl/hapi-auth-jwt2/pull/146#issuecomment-205481751)

## Generating Your Secret Key

@skota asked "***How to generate secret key***?" in: [dwyl/hapi-auth-jwt2/issues/**48**](https://github.com/dwyl/hapi-auth-jwt2/issues/48)

There are _several_ options for generating secret keys.
There are _several_ options for generating secret keys.
The _easist_ way is to run node's crypto hash in your terminal:
```js
node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"
```
and copy the resulting base64 key and use it as your JWT secret.
and copy the resulting base64 key and use it as your JWT secret.
If you are *curious* how strong that key is watch: https://youtu.be/koJQQWHI-ZA


Expand All @@ -316,15 +316,15 @@ within the request lifecycle with the `request.auth.token` property.

*Note* that this is the ***encoded token***,
and it's only useful if you want to use to make
request to other servers using the user's token.
request to other servers using the user's token.

The *decoded* version of the token, accessible via `request.auth.credentials`

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

[@benjaminlees](https://github.com/benjaminlees)
requested the ability to send/receive tokens as cookies:
[dwyl/hapi-auth-jwt2/issues/**55**](https://github.com/dwyl/hapi-auth-jwt2/issues/55)
[dwyl/hapi-auth-jwt2/issues/**55**](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*.

Expand Down Expand Up @@ -371,29 +371,29 @@ http://tools.ietf.org/html/rfc6265

## Frequently Asked Questions (FAQ)

1. Do I need to include **jsonwebtoken** in my project? asked in [hapi-auth-jwt2/issues/32](https://github.com/dwyl/hapi-auth-jwt2/issues/32)
1. Do I need to include **jsonwebtoken** in my project? asked in [hapi-auth-jwt2/issues/32](https://github.com/dwyl/hapi-auth-jwt2/issues/32)
**Q**: Must I include the **jsonwebtoken** package in my project
[given that **hapi-auth-jwt2** plugin already includes it] ?
[given that **hapi-auth-jwt2** plugin already includes it] ?
**A**: Yes, you need to *manually* install the **jsonwebtoken**
node module from NPM with `npm install jsonwebtoken --save` if you want to ***sign*** JWTs in your app.
node module from NPM with `npm install jsonwebtoken --save` if you want to ***sign*** JWTs in your app.
Even though **hapi-auth-jwt2** includes it
as a **dependency** your app does not know where to find it in the **node_modules** tree for your project.
as a **dependency** your app does not know where to find it in the **node_modules** tree for your project.
Unless you include it via ***relative path*** e.g:
`var JWT = require('./node_modules/hapi-auth-jwt2/node_modules/jsonwebtoken');`
`var JWT = require('./node_modules/hapi-auth-jwt2/node_modules/jsonwebtoken');`
we *recommend* including it in your **package.json** ***explicitly*** as a **dependency** for your project.

2. Can we supply a ***Custom Verification*** function instead of using the **JWT.verify** method?
2. Can we supply a ***Custom Verification*** function instead of using the **JWT.verify** method?
asked by *both* [Marcus Stong](https://github.com/stongo) & [Kevin Stewart](https://github.com/kdstew)
in [issue #120](https://github.com/dwyl/hapi-auth-jwt2/issues/120) and [issue #130](https://github.com/dwyl/hapi-auth-jwt2/issues/130) respectively.
**Q**: Does this module support custom verification function or disabling verification?
**A**: Yes, it *does now*! (*see: "Advanced Usage" below*) the inclusion of a `verifyFunc`
in [issue #120](https://github.com/dwyl/hapi-auth-jwt2/issues/120) and [issue #130](https://github.com/dwyl/hapi-auth-jwt2/issues/130) respectively.
**Q**: Does this module support custom verification function or disabling verification?
**A**: Yes, it *does now*! (*see: "Advanced Usage" below*) the inclusion of a `verifyFunc`
gives you *complete control* over the verification of the incoming JWT.


## *Advanced/Alternative* Usage => Bring Your Own `verifyFunc`

While *most* people using `hapi-auth-jwt2` will opt for the *simpler* use case
(*using a* ***Validation Function*** *`validateFunc` - see: Basic Usage example above -
(*using a* ***Validation Function*** *`validateFunc` - see: Basic Usage example above -
which validates the JWT payload after it has been verified...*)
others may need more control over the `verify` step.

Expand All @@ -407,19 +407,19 @@ while you are initializing the plugin.
- `verifyFunc` - (***optional***) the function which is run once the Token has been decoded
(*instead of a `validateFunc`*) with signature `function(decoded, request, callback)` where:
- `decoded` - (***required***) is the ***decoded*** and ***verified*** JWT received from the client in **request.headers.authorization**
- `request` - (***required***) is the original ***request*** received from the client
- `request` - (***required***) is the original ***request*** received from the client
- `callback` - (***required***) a callback function with the signature `function(err, isValid, credentials)` where:
- `err` - an internal error.
- `valid` - `true` if the JWT was valid, otherwise `false`.
- `credentials` - (***optional***) alternative credentials to be set instead of `decoded`.

The advantage of this approach is that it allows people to write a
custom verification function or to bypass the `JWT.verify` *completely*.
custom verification function or to bypass the `JWT.verify` *completely*.
For more detail, see: use-case discussion in https://github.com/dwyl/hapi-auth-jwt2/issues/120


> ***Note***: *nobody has requested the ability to use* ***both*** *a*
`validateFunc` ***and*** `verifyFunc`.
`validateFunc` ***and*** `verifyFunc`.
This should not be *necessary*
because with a `verifyFunc` you can incorporate your own custom-logic.

Expand All @@ -436,7 +436,7 @@ See the release notes for more details:
However in the interest of
security/performance we *recommend* using the [*latest version*](https://github.com/hapijs/hapi/) of Hapi.

> *If you have a question, or need help getting started* ***please post an issue/question on
> *If you have a question, or need help getting started* ***please post an issue/question on
GitHub***: https://github.com/dwyl/hapi-auth-jwt2/issues *or*
[![Join the chat at https://gitter.im/dwyl/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dwyl/chat/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

Expand Down Expand Up @@ -482,7 +482,7 @@ if the [session record is ***found*** (valid) and ***not ended***](https://githu
[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
https://github.com/dwyl/hapi-auth-jwt2/issues
(*we are here to help get you started on your journey to **hapi**ness!*)

<br />
Expand All @@ -491,7 +491,7 @@ https://github.com/dwyl/hapi-auth-jwt2/issues

## Contributing [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/hapi-auth-jwt2/issues)

If you spot an area for improvement, please raise an issue: https://github.com/dwyl/hapi-auth-jwt2/issues
If you spot an area for improvement, please raise an issue: https://github.com/dwyl/hapi-auth-jwt2/issues
*Someone* in the dwyl team is *always* online so we will usually answer within a few hours.

### Running the tests requires environment variables
Expand All @@ -512,15 +512,15 @@ export REDISCLOUD_URL='redis://rediscloud:[email protected]
## Motivation

While making [***Time***](https://github.com/dwyl/time) we want to ensure
our app (and API) is as ***simple*** as *possible* to use.
our app (and API) is as ***simple*** as *possible* to use.
This lead us to using JSON Web Tokens for ***Stateless*** Authentication.

We did a *extensive* [research](https://www.npmjs.com/search?q=hapi+auth+jwt)
into *existing* modules that *might* solve our problem; there are *many* on NPM:
![npm search for hapi+jwt](http://i.imgur.com/xIj3Xpa.png)

but they were invariably ***too complicated***, poorly documented and
had *useless* (non-real-world) "examples"!
had *useless* (non-real-world) "examples"!

Also, none of the *existing* modules exposed the **request** object
to the **validateFunc** which we thought might be handy.
Expand All @@ -532,7 +532,7 @@ So we decided to write our own module addressing all these issues.

## Why hapi-auth-jwt2 ?

The name we wanted was taken.
The name we wanted was taken.
Think of our module as the "***new, simplified and actively maintained version***"

## Useful Links
Expand Down
13 changes: 6 additions & 7 deletions lib/extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ var Cookie = require('cookie'); // highly popular decoupled cookie parser
*/

module.exports = function (request, options) {

// The key holding token value in url or cookie defaults to token
var urlKey = typeof options.urlKey === 'string' ? options.urlKey : 'token';
var cookieKey = typeof options.cookieKey === 'string' ? options.cookieKey : 'token';
var headerKey = typeof options.headerKey === 'string' ? options.headerKey : 'authorization';
var urlKey = options.urlKey === false || typeof options.urlKey === 'string' ? options.urlKey : 'token';
var cookieKey = options.cookieKey === false || typeof options.cookieKey === 'string' ? options.cookieKey : 'token';
var headerKey = options.headerKey === false || typeof options.headerKey === 'string' ? options.headerKey : 'authorization';
var auth;

if(request.query[urlKey]) { // tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
if(urlKey && request.query[urlKey]) { // tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
auth = request.query[urlKey];
} // JWT tokens in cookie: https://github.com/dwyl/hapi-auth-jwt2/issues/55
else if (request.headers[headerKey]) {
else if (headerKey && request.headers[headerKey]) {
if (typeof options.tokenType === 'string') {
var token = request.headers[headerKey].match(new RegExp(options.tokenType + '\\s+([^$]+)', 'i'));
auth = token === null ? null : token[1];
} else {
auth = request.headers[headerKey];
}
}
else if (request.headers.cookie) {
else if (cookieKey && request.headers.cookie) {
auth = Cookie.parse(request.headers.cookie)[cookieKey];
}

Expand Down
32 changes: 32 additions & 0 deletions test/basic_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,42 @@ server.register(require('../'), function () {
verifyOptions: { algorithms: [ 'HS256' ] } // only allow HS256 algorithm
});

server.auth.strategy('jwt-nourl', 'jwt', {
key: secret,
validateFunc: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // only allow HS256 algorithm
urlKey: false
});

server.auth.strategy('jwt-nocookie', 'jwt', {
key: secret,
validateFunc: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // only allow HS256 algorithm
cookieKey: false
});

server.auth.strategy('jwt-nourl2', 'jwt', {
key: secret,
validateFunc: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // only allow HS256 algorithm
urlKey: ''
});

server.auth.strategy('jwt-nocookie2', 'jwt', {
key: secret,
validateFunc: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // only allow HS256 algorithm
cookieKey: ''
});

server.route([
{ method: 'GET', path: '/', handler: home, config: { auth: false } },
{ method: 'GET', path: '/token', handler: sendToken, config: { auth: 'jwt' } },
{ method: 'POST', path: '/privado', handler: privado, config: { auth: 'jwt' } },
{ method: 'POST', path: '/privadonourl', handler: privado, config: { auth: 'jwt-nourl' } },
{ method: 'POST', path: '/privadonocookie', handler: privado, config: { auth: 'jwt-nocookie' } },
{ method: 'POST', path: '/privadonourl2', handler: privado, config: { auth: 'jwt-nourl2' } },
{ method: 'POST', path: '/privadonocookie2', handler: privado, config: { auth: 'jwt-nocookie2' } },
{ method: 'POST', path: '/required', handler: privado, config: { auth: { mode: 'required', strategy: 'jwt' } } },
{ method: 'POST', path: '/optional', handler: privado, config: { auth: { mode: 'optional', strategy: 'jwt' } } },
{ method: 'POST', path: '/try', handler: privado, config: { auth: { mode: 'try', strategy: 'jwt' } } }
Expand Down
30 changes: 30 additions & 0 deletions test/cookies_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,33 @@ test("Valid Google Analytics cookie should be ignored (BAD Header Token)", funct
t.end();
});
});

test("Attempt to access restricted content with cookieKey=false", function(t) {
var token = JWT.sign({ id: 123, "name": "Charlie" }, secret);
var options = {
method: "POST",
url: "/privadonocookie",
headers: { cookie: "token=" + token }
};
// server.inject lets us simulate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 401, "Disabled cookie auth shouldn't accept valid token!");
t.end();
});
});


test("Attempt to access restricted content with cookieKey=''", function(t) {
var token = JWT.sign({ id: 123, "name": "Charlie" }, secret);
var options = {
method: "POST",
url: "/privadonocookie2",
headers: { cookie: "=" + token }
};
// server.inject lets us simulate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 400, "Disabled cookie auth shouldn't accept valid token!");
t.end();
});
});

30 changes: 30 additions & 0 deletions test/url_token_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,33 @@ test("Access restricted content (with VALID Token)", function(t) {
t.end();
});
});

test("Using route with urlKey=false should be denied", function(t) {
// use the token as the 'authorization' header in requests
var token = JWT.sign({ id: 123, "name": "Charlie" }, secret);
token = "?token=" + token;
var options = {
method: "POST",
url: "/privadonourl" + token
};
// server.inject lets us simulate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 401, "No urlKey configured so URL-Tokens should be denied");
t.end();
});
});

test("Using route with urlKey='' should be denied", function(t) {
// use the token as the 'authorization' header in requests
var token = JWT.sign({ id: 123, "name": "Charlie" }, secret);
token = "?=" + token;
var options = {
method: "POST",
url: "/privadonourl2" + token
};
// server.inject lets us simulate an http request
server.inject(options, function(response) {
t.equal(response.statusCode, 401, "No urlKey configured so URL-Tokens should be denied");
t.end();
});
});