Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
woloski committed Jul 1, 2013
0 parents commit 07d163b
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.DS_Store
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
language: node_js
node_js:
- 0.8
- 0.10
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# node-jws [![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.png)](http://travis-ci.org/auth0/node-jsonwebtoken)


An implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html).

This was developed against `draft-ietf-oauth-json-web-token-08`. It makes use of [node-jws](https://github.com/brianloveswords/node-jws)

# Install

```bash
$ npm install jsonwebtoken
```

# Usage

## jwt.sign(payload, secretOrPrivateKey, options)

(Synchronous) Returns the JsonWebToken as string

`payload` could be an literal, buffer or string

`secretOrPrivateKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM
encoded private key for RSA and ECDSA.

`options`:

* `algorithm` (default: `HS256`)
* `expiresInMinutes`
* `audience`
* `subject`
* `issuer`

If `payload` is not a buffer or a string, it will be coerced into a string
using `JSON.stringify`.

If any `expiresInMinutes`, `audience`, `subject`, `issuer` are not provided, there is no default. The jwt generated won't include those properties in the payload.

Example

```js
// sign with default (HMAC SHA256)
var token = jwt.sign({ foo: 'bar' }, 'shhhhh');

// sign with RSA SHA256
var cert = fs.readFileSync('private.key'); // get private key
var token = jwt.sign({ foo: 'bar' }, cert, { algorithm: 'RS256'});
```

## jwt.verify(token, secretOrPublicKey, options, callback)

(Synchronous with callback) Returns the payload decoded if the signature (and optionally expiration, audience, issuer) are valid. If not, it will return the error.

`token` is the JsonWebToken string

`secretOrPublicKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM
encoded public key for RSA and ECDSA.

`options`

* `audience`: if you want to check audience (`aud`), provide a value here
* `issuer`: if you want to check issuer (`iss`), provide a value here

```js
// verify a token symmetric
jwt.verify(token, 'shhhhh', function(err, decoded) {
console.log(decoded.foo) // bar
});

// invalid token
jwt.verify(token, 'wrong-secret', function(err, decoded) {
// err
// decoded undefined
});

// verify a token asymmetric
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, function(err, decoded) {
console.log(decoded.foo) // bar
});

// verify audience
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo' }, function(err, decoded) {
// if audience mismatch, err == invalid audience
});

// verify issuer
var cert = fs.readFileSync('public.pem'); // get public key
jwt.verify(token, cert, { audience: 'urn:foo', issuer: 'urn:issuer' }, function(err, decoded) {
// if issuer mismatch, err == invalid issuer
});

```

## Algorithms supported

Array of supported algorithms. The following algorithms are currently supported.

alg Parameter Value | Digital Signature or MAC Algorithm
----------------|----------------------------
HS256 | HMAC using SHA-256 hash algorithm
HS384 | HMAC using SHA-384 hash algorithm
HS512 | HMAC using SHA-512 hash algorithm
RS256 | RSASSA using SHA-256 hash algorithm
RS384 | RSASSA using SHA-384 hash algorithm
RS512 | RSASSA using SHA-512 hash algorithm
ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm
ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm
ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm
none | No digital signature or MAC value included



# TODO

* X.509 certificate chain is not checked

# License

MIT

62 changes: 62 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
var jws = require('jws');
var moment = require('moment');

module.exports.sign = function(payload, secretOrPrivateKey, options) {
options = options || {};

var header = {typ: 'JWT', alg: options.algorithm || 'HS256'};
if (options.expiresInMinutes)
payload.exp = moment().add('minutes', options.expiresInMinutes).utc().unix();

if (options.audience)
payload.aud = options.audience;

if (options.issuer)
payload.iss = options.issuer;

if (options.subject)
payload.sub = options.subject;

payload.iat = moment().utc().unix();

var signed = jws.sign({header: header, payload: payload, secret: secretOrPrivateKey});

return signed;
};

module.exports.verify = function(jwtString, secretOrPublicKey, options, callback) {
if ((typeof options === 'function') && !callback) callback = options;
if (!options) options = {};

var valid;
try {
valid = jws.verify(jwtString, secretOrPublicKey);
}
catch (e) {
return callback(e);
}

if (!valid)
return callback(new Error('invalid signature'));

var jwt = jws.decode(jwtString);

if (jwt.payload.exp) {
if (moment().utc().unix() >= jwt.payload.exp)
return callback(new Error('jwt expired'));
}

if (jwt.payload.aud && options.audience) {
if (jwt.payload.aud !== options.audience)
return callback(new Error('jwt audience invalid. expected: ' + jwt.payload.aud));
}

if (jwt.payload.iss && options.issuer) {
if (jwt.payload.iss !== options.issuer)
return callback(new Error('jwt issuer invalid. expected: ' + jwt.payload.iss));
}

callback(null, jwt.payload);
};


27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "node-jsonwebtoken",
"version": "0.1.0",
"description": "JSON Web Token implementation (symmetric and asymmetric)",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "https://github.com/auth0/node-jsonwebtoken"
},
"keywords": [
"jwt"
],
"author": "auth0",
"license": "MIT",
"bugs": {
"url": "https://github.com/auth0/node-jsonwebtoken/issues"
},
"dependencies": {
"jws": "~0.2.2"
},
"devDependencies": {
"chai": "*"
}
}
19 changes: 19 additions & 0 deletions test/invalid_pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJjCCAg6gAwIBAgIJAMyz3mSPlaW4MA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
BAMUCyouYXV0aDAuY29tMB4XDTEzMDQxODE3MDE1MFoXDTI2MTIyNjE3MDE1MFow
FjEUMBIGA1UEAxQLKi5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDZq1Ua0/BGm+TaBFoftKWeYMWrQG9Fx3g7ikErxljmyOvlwqkiat3q
ixX+Dxw9TFb5gbBjNJ+L3nt4YefJgLsYvsHqkOUxWsB+HM/ulJRVnVrZm1tI3Nbg
xO1BQ7DrGfBpq2KCxtQCaQFRlQJw1+qS5LwrdIvihB7Kc142VElCFFHJ6+09eMUy
jy00Z5pfQr4Am6W6eEOS9ObDbNs4XgKOcWe5khWXj3UStou+VgbAg40XcYht2IbY
gMfKF+VUZOy3+e+aRTqPOBU3MAeb0tvCCPUQJbNAUHgSKVhAvNf8mRwttVsOLT70
anjjeCOd7RKS8fVKBwc2KtgNkghYdPY9AgMBAAGjdzB1MB0GA1UdDgQWBBSi4+X0
+MvCKDdd375mDhx/ZBbJ4DBGBgNVHSMEPzA9gBSi4+X0+MvCKDdd375mDhx/ZBbJ
4KEapBgwFjEUMBIGA1UEAxQLKi5hdXRoMC5jb22CCQDMs95kj5WluDAMBgNVHRME
BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBi0qPe0DzlPSufq+Gdk2Fwf1pGEtjA
D34IxxJ9SX6r1DS/NIP7IOLUnNU8cP8BQWl7i413v29jJsNV457pjdmqf8J7OE9O
eF5Yz1x91gY/27561Iga/TQeIVOlFQAgx66eLfUFFoAig3hz2srZo5TzYBixMJsS
fYMXHPiU7KoLUqYXvpSXIllstQCu51KCC6t9H7wZ92lTES1v76hFY4edQ30sftPo
kjAYWGEhMjPo/r4THcdSMqKXoRtCGEun4pTXid7MJcTgdGDrAJddLWi6SxKecEVB
MhMu4XfUCdxCwqQPjHeJ+zE49A1CUdBB2FN3BNLbmTTwEBgmuwyGRzhj
-----END CERTIFICATE-----
35 changes: 35 additions & 0 deletions test/jwt.hs.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var jwt = require('../index');

var expect = require('chai').expect;
var assert = require('chai').assert;

describe('HS256', function() {

describe('when signing a token', function() {
var secret = 'shhhhhh';

var token = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' });

it('should be syntactically valid', function() {
expect(token).to.be.a('string');
expect(token.split('.')).to.have.length(3);
});

it('should validate with secret', function(done) {
jwt.verify(token, secret, function(err, decoded) {
assert.ok(decoded.foo);
assert.equal('bar', decoded.foo);
done();
});
});

it('should throw with invalid secret', function(done) {
jwt.verify(token, 'invalid secret', function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
done();
});
});

});
});
115 changes: 115 additions & 0 deletions test/jwt.rs.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
var jwt = require('../index');
var fs = require('fs');
var path = require('path');

var expect = require('chai').expect;
var assert = require('chai').assert;

describe('RS256', function() {
var pub = fs.readFileSync(path.join(__dirname, 'pub.pem'));
var priv = fs.readFileSync(path.join(__dirname, 'priv.pem'));
var invalid_pub = fs.readFileSync(path.join(__dirname, 'invalid_pub.pem'));

describe('when signing a token', function() {
var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256' });

it('should be syntactically valid', function() {
expect(token).to.be.a('string');
expect(token.split('.')).to.have.length(3);
});

it('should validate with public key', function(done) {
jwt.verify(token, pub, function(err, decoded) {
assert.ok(decoded.foo);
assert.equal('bar', decoded.foo);
done();
});
});

it('should throw with invalid public key', function(done) {
jwt.verify(token, invalid_pub, function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
done();
});

});

});


describe('when signing a token with expiration', function() {
var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', expiresInMinutes: 10 });

it('should be valid expiration', function(done) {
jwt.verify(token, pub, function(err, decoded) {
assert.isNotNull(decoded);
assert.isNull(err);
done();
});
});

it('should be invalid', function(done) {
// expired token
token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', expiresInMinutes: -10 });

jwt.verify(token, pub, function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
done();
});
});

});

describe('when signing a token with audience', function() {
var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', audience: 'urn:foo' });

it('should check audience', function(done) {
jwt.verify(token, pub, function(err, decoded) {
assert.isNotNull(decoded);
assert.isNull(err);
done();
});
});

it('should throw when invalid audience', function(done) {
jwt.verify(token, pub, { audience: 'urn:wrong' }, function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
done();
});
});

});

describe('when signing a token with issuer', function() {
var token = jwt.sign({ foo: 'bar' }, priv, { algorithm: 'RS256', issuer: 'urn:foo' });

it('should check issuer', function() {
jwt.verify(token, pub, { issuer: 'urn:foo' }, function(err, decoded) {
assert.isNotNull(decoded);
assert.isNull(err);
});
});

it('should throw when invalid issuer', function() {
jwt.verify(token, pub, { issuer: 'urn:wrong' }, function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
});
});
});

describe('when verifying a malformed token', function() {
it('should throw', function(done) {
jwt.verify('fruit.fruit.fruit', pub, function(err, decoded) {
assert.isUndefined(decoded);
assert.isNotNull(err);
done();
});
});
});


});
Loading

0 comments on commit 07d163b

Please sign in to comment.