diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28f1ba7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..663b27a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - 0.8 + - 0.10 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8717fc --- /dev/null +++ b/README.md @@ -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 + diff --git a/index.js b/index.js new file mode 100644 index 0000000..22365c8 --- /dev/null +++ b/index.js @@ -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); +}; + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0e1eaed --- /dev/null +++ b/package.json @@ -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": "*" + } +} diff --git a/test/invalid_pub.pem b/test/invalid_pub.pem new file mode 100644 index 0000000..2482abb --- /dev/null +++ b/test/invalid_pub.pem @@ -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----- diff --git a/test/jwt.hs.tests.js b/test/jwt.hs.tests.js new file mode 100644 index 0000000..1d23365 --- /dev/null +++ b/test/jwt.hs.tests.js @@ -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(); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/jwt.rs.tests.js b/test/jwt.rs.tests.js new file mode 100644 index 0000000..50f4b71 --- /dev/null +++ b/test/jwt.rs.tests.js @@ -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(); + }); + }); + }); + + +}); \ No newline at end of file diff --git a/test/priv.pem b/test/priv.pem new file mode 100644 index 0000000..7be6d5a --- /dev/null +++ b/test/priv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN ++H7GHp3/QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXi +c78kOugMY1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6Gb +RKzyTKcB58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRX +kdDSHty6lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1K +kyHFqWpxaJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABAoIBAQCYKw05YSNhXVPk +eHLeW/pXuwR3OkCexPrakOmwMC0s2vIF7mChN0d6hvhVlUp68X7V8SnS2JxAGo8v +iHY+Et3DdwZ3cxnzwh+BEhzgDfoIOmkoGppZPyX/K6klWtbGUrTtSISOWXbvEXQU +G0qGAvDOzIGTsdMDX7slnU70Ac23JybPY5qBSiE+ky8U4dm2fUHMroWub4QP5vA/ +nqyWqX2FB/MEAbcujaknDQrFCtbmtUYlBbJCKGd9V3cGEqp6H7oH+ah2ofMc91gJ +mCHk3YyWZB/bcVXH3CA+s1ywvCOVDBZ3Nw7Pt9zIcv6Rl9UKIy+Nx0QjXxR90Hla +Tr0GHIShAoGBAPsD7uXm+0ksnGyKRYgvlVad8Z8FUFT6bf4B+vboDbx40FO8O/5V +PraBPC5z8YRSBOQ/WfccPQzakkA28F2pXlRpXu5JcErVWnyyUiKpX5sw6iPenQR2 +JO9hY/GFbKiwUhVHpvWMcXFqFLSQu2A86jPnFFEfG48ZT4IhTzINKJVZAoGBAMKc +B3YGfVfY9qiRFXzYRdSRLg5c8p/HzuWwXc9vfJ4kQTDkPXe/+nqD67rzeT54uVec +jKoIrsCu4BfEaoyvOT+1KmUfdEpBgYZuuEC4CZf7dgKbXOpPVvZDMyJ/e7HyqTpw +mvIYJLPm2fNAcAsnbrNX5mhLwwzEIltbplUUeRdrAoGBAKhZgPYsLkhrZRXevreR +wkTvdUfD1pbHxtFfHqROCjhnhsFCM7JmFcNtdaFqHYczQxiZ7IqxI7jlNsVek2Md +3qgaa5LBKlDmOuP67N9WXUrGSaJ5ATIm0qrB1Lf9VlzktIiVH8L7yHHaRby8fQ8U +i7b3ukaV6HPW895A3M6iyJ8xAoGAInp4S+3MaTL0SFsj/nFmtcle6oaHKc3BlyoP +BMBQyMfNkPbu+PdXTjtvGTknouzKkX4X4cwWAec5ppxS8EffEa1sLGxNMxa19vZI +yJaShI21k7Ko3I5f7tNrDNKfPKCsYMEwgnHKluDwfktNTnyW/Uk2dgXuMaXSHHN5 +XZt59K8CgYArGVOWK7LUmf3dkTIs3tXBm4/IMtUZmWmcP9C8Xe/Dg/IdQhK5CIx4 +VXl8rgZNeX/5/4nJ8Q3LrdLau1Iz620trNRGU6sGMs3x4WQbSq93RRbFzfG1oK74 +IOo5yIBxImQOSk5jz31gF9RJb15SDBIxonuWv8qAERyUfvrmEwR0kg== +-----END RSA PRIVATE KEY----- diff --git a/test/pub.pem b/test/pub.pem new file mode 100644 index 0000000..dd95d34 --- /dev/null +++ b/test/pub.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIJAMKR/NsyfcazMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIxMTEyMjM0MzQxWhcNMTYxMjIxMjM0MzQxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvtH4wKLYlIXZlfYQFJtXZVC3fD8XMarzwvb/fHUyJ6NvNStN+H7GHp3/ +QhZbSaRyqK5hu5xXtFLgnI0QG8oE1NlXbczjH45LeHWhPIdc2uHSpzXic78kOugM +Y1vng4J10PF6+T2FNaiv0iXeIQq9xbwwPYpflViQyJnzGCIZ7VGan6GbRKzyTKcB +58yx24pJq+CviLXEY52TIW1l5imcjGvLtlCp1za9qBZa4XGoVqHi1kRXkdDSHty6 +lZWj3KxoRvTbiaBCH+75U7rifS6fR9lqjWE57bCGoz7+BBu9YmPKtI1KkyHFqWpx +aJc/AKf9xgg+UumeqVcirUmAsHJrMwIDAQABo4GnMIGkMB0GA1UdDgQWBBTs83nk +LtoXFlmBUts3EIxcVvkvcjB1BgNVHSMEbjBsgBTs83nkLtoXFlmBUts3EIxcVvkv +cqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV +BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAMKR/NsyfcazMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBABw7w/5k4d5dVDgd/OOOmXdaaCIKvt7d +3ntlv1SSvAoKT8d8lt97Dm5RrmefBI13I2yivZg5bfTge4+vAV6VdLFdWeFp1b/F +OZkYUv6A8o5HW0OWQYVX26zIqBcG2Qrm3reiSl5BLvpj1WSpCsYvs5kaO4vFpMak +/ICgdZD+rxwxf8Vb/6fntKywWSLgwKH3mJ+Z0kRlpq1g1oieiOm1/gpZ35s0Yuor +XZba9ptfLCYSggg/qc3d3d0tbHplKYkwFm7f5ORGHDSD5SJm+gI7RPE+4bO8q79R +PAfbG1UGuJ0b/oigagciHhJp851SQRYf3JuNSc17BnK2L5IEtzjqr+Q= +-----END CERTIFICATE-----