├── .gitignore ├── README.md ├── fixtures ├── badkey.pem ├── badkey.pub ├── key.pem └── key.pub ├── jwt-spec.js ├── jwt.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chronicled JWT 2 | 3 | A simplified wrapper around [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). 4 | 5 | Defaults: 6 | - Token expiry: 1 month 7 | - Algorithm: RSA (`'RS256'`) 8 | - Issuer: `'ch:server'` 9 | - Audience: `'ch:client'` 10 | - Subject: `'ch:login'` 11 | 12 | ## What/why 13 | 14 | JSON Web Tokens are a simple way to encode information into a bearer token to avoid database lookups when authenticating API requests. 15 | Basically, it's all the convenience of cookies but without the headaches that come with trying to use cookies for a public API (e.g. CORS). 16 | Read more about JWTs [here](http://jwt.io/). 17 | 18 | ## Usage 19 | 20 | First, we initialize a JWT processor. 21 | This takes the contents of our RSA private key file as a string (or Buffer). 22 | 23 | ```js 24 | var jwt = new JWT(privateKey); 25 | ``` 26 | 27 | Let's generate a token. This is a plain Javascript object that should contain the user, their permissions, etc. 28 | 29 | ```js 30 | var payload = { 31 | user: { 32 | nickname: 'duncan', 33 | name: 'Duncan Smith', 34 | email: 'duncan@chronicled.com', 35 | roles: ['admin', 'user'] 36 | } 37 | }; 38 | 39 | var token = jwt.sign(payload); 40 | ``` 41 | 42 | Now that we have the token, we send it back to the client so they can use it to authenticate all requests from here on out. 43 | 44 | Later on (most likely in a middleware), we'll want to verify the token (to make sure it hasn't been tampered with), and decode it (so that we can use the inforation contained within). We'll need to pass in the public key information as well (again, as a string or Buffer). 45 | 46 | ```js 47 | var verifiedToken = jwt.verify(token, publicKey); 48 | 49 | if (verifiedToken.valid) { 50 | console.log(verifiedToken.user); // => {nickame: 'duncan', name: 'Duncan Smith', email: 'duncan@chronicled.com', roles: ['admin', 'user']} 51 | } 52 | ``` 53 | 54 | If the token is invalid (malformed, expired, etc), we can check the `reason` property to find out why: 55 | 56 | ```js 57 | console.log(invalidToken.reason); // => 'Token is expired.' 58 | ``` 59 | 60 | **Protip: to generate a public/private key pair:** 61 | 62 | ```sh 63 | openssl genrsa -out key.pem 2048 # Generate a private key 64 | openssl rsa -in key.pem -pubout >> key.pub # Generate a public key from the private key 65 | ``` 66 | 67 | ## Hacking 68 | 69 | Tests are written with Mocha and Should.js. 70 | 71 | Run tests with `npm test`. 72 | 73 | Run tests continuously with `npm run tests:watch`. 74 | 75 | ## TODO 76 | 77 | Make isomorphic. Currently only works in Node.js due to dependency on `jsonwebtoken` (above). 78 | The main challenge in making it isomorphic is the reliance on native crypto libraries. 79 | -------------------------------------------------------------------------------- /fixtures/badkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA4prjy5xQCFdjDPSk9s5sKKRZL2YZGUqeVujGxaPAx/2zkARH 3 | rrydX/2eHM7ER1fnrO8FwNas9Vvroqdki+rWOtqUSAZW2noG8Hh9POJ5/3Ru2+W7 4 | oauvfhovbTMunImC4yWNo8WuRD8AIz9HdV9nMrzEfLSPrJBDJQHJFL2egcDTmqaK 5 | G+4lYlVHXbgTGLoMcA1RCBUBqeSqHQ8L7V8bC+Jdaf+tPEZqiVyJ8TthQ5qkebMS 6 | Z/F/P84tdLU9THaLv3HLX2NGnQb8vygOUBxCeiX657phX1yMWLz5mje/qwEqa89d 7 | PFWxYgBLIWBlMsUSnyxA+FIwHD9h9C1nruXBbwIDAQABAoIBAQC8KooFxSLgClCB 8 | rZReLUK67N3x8gHdcozQ9jI4Y6Xta8nzSNqNSqoTz3G/0iJCa8hBXamVW637f/zG 9 | 7+EaHkMOU/rVZA4zFSk1/ZdhO166tWBo0PdhZEMxn2TKdLm72qQJImHfIIN4wCfJ 10 | HP6lhDcZdo5iTz0OuixHSphC1MIOUxQE0dAEjii/Cttwn7dRFsn2+jvJoqW/S41t 11 | TtXNggVf7YTXTYz1UL7UZ+YuJmBj/gtOZLg37HpAKL965knrEqh+dCfl9AiE1vTo 12 | JSh57kwHeUOoLGW92UCt+WvfodG7vj/dS4GCmMsNV5iB1T9M7T8idMOX68NfPXnj 13 | u4W9FOjBAoGBAPtho2rHZHOmlnZZc7vGmmNYFUSsYxwwso/kh9c3kcO9lVcgj+RU 14 | xGfRAFxLniBUBNbDIEJm1qAWZjFdvDi7td83FFBsXI+rYMMUWPh4sPgtQIsmOeC1 15 | 1ysUTLllqDdW7C4FlW84kgPIAfKFHwMtyhxBevi+0ybObQRlSj2OR2tXAoGBAObE 16 | t4g4wRpivqKsfc6rt+3SiOKy5gCiriNclFzmnPzTkLkdtuQ7O9fBpIQZgM9PXnxs 17 | YSSoP+lR2QlgPH2H8ZlkFWufZ9DGaUUheb/CgDSAnfTR91utTtGWWyrhkkrKcERk 18 | 0Wi6DIJtGnyxpEJD/pzM4V4kiZ1WxkLVN5tJpSOpAoGAVTvE2fajc6cfSx+HYzqy 19 | rE54X6GHtU45rRpSiMF5tgG4+iK7RzMKqRyvX5vwEIwMW/krHfiaews9OS3MHPxg 20 | bT/SrnYfaEM2es6SYCUj8/H7+XJLm4psW1n1rcLvJ1xcljokceKfd5LAPkeuvTgw 21 | WCZHmMGy/GxvgFcLthVg198CgYB4aD3m6s3+yXT0hhHiiwCeK1LXDkcqH7fCpaSX 22 | 0JAq7uy9Wf66mRmBWv1PG8t039HKE/af3NX0FIus87S8PvlVcr4meHb/nPcCZhQT 23 | dRXVzyIbwo5RHF1ayBvrhOUC9xua5AvLm/+48dp3I200UiwAfno8182h9cvexUeN 24 | U+DBeQKBgB/XX1XwMYHhA5qZJW5gVUt4xTBvsI/WbgXbmw+TOT59Bzhpvbu2Ydkb 25 | cMMDmO0hxIiQC0TlO+thb3lF+FquyX59CHichpN5trKuBTaZUDW9XaFWlh9LRdk0 26 | voiH2qRvGoEyjAbEmg+D1OI+Ql3TWUGBjZcVk0diuw1vdwnBMlDz 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /fixtures/badkey.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4prjy5xQCFdjDPSk9s5s 3 | KKRZL2YZGUqeVujGxaPAx/2zkARHrrydX/2eHM7ER1fnrO8FwNas9Vvroqdki+rW 4 | OtqUSAZW2noG8Hh9POJ5/3Ru2+W7oauvfhovbTMunImC4yWNo8WuRD8AIz9HdV9n 5 | MrzEfLSPrJBDJQHJFL2egcDTmqaKG+4lYlVHXbgTGLoMcA1RCBUBqeSqHQ8L7V8b 6 | C+Jdaf+tPEZqiVyJ8TthQ5qkebMSZ/F/P84tdLU9THaLv3HLX2NGnQb8vygOUBxC 7 | eiX657phX1yMWLz5mje/qwEqa89dPFWxYgBLIWBlMsUSnyxA+FIwHD9h9C1nruXB 8 | bwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAo7D0N18WtF8SHxXipfOQWCfLnp4sl4wiScCX2W6wevFepZd8 3 | fxDvrLdbN5YVpcQDqIGIHIjKZfeGHe8Teu6Rz/PpfV7Tvwzq5P6Om3vs/z4Yx33G 4 | NybG/0BhcQnbM3f3L8Clcbl8WpwsotoUx/0sEzNXygiR8V0Dl73Rs6/Y3BtQMe4u 5 | zadvriPTaLLHISXO0kSFex/13Sia7zVAbNSmLeivM2PXsYLRdgWN0dmh4o6UQbX9 6 | 2AsQA85Z7678xSt1qx2eReu5dcNcVKLPT8feqhqsVB+JjIpyfLc4UYTJzcrs19/s 7 | V6OI5dnDenVB4ggMAeNyrnVZ7gdQA8a59HzKtQIDAQABAoIBAQCXM+iSkUJOwJbj 8 | ofqnIlIOLExZK3CzWB2AFOfvT+Cy3+x2d3TnhsRtvT0EHp3GbkaWy3foK68R+0+m 9 | rOVIo8CK35qpVQOxMYR0nHMsSo/Dwh9vfs2uuiINO/IGUX1qkIEDWic44oZsyGAu 10 | 2dViWojw/3czRzFQS4P7Ebr4CRH/Vb1FPRrDAe240kHJQXIUUMGXR3/xo2hGsjGg 11 | 3yo4U9rQ5WJmxdBI5M7kVs019P9SQwzKztdffCsgfw1nycgqgCcs7lxNox1WUkTS 12 | JoDUJYOE+TT1jAPEnSvNJmJBCr/XwnxZfHLuKiP93DFO7Dz+mZj5ajCT5NGRgnbK 13 | RWqY8MVhAoGBANAOJ1p9gu5+/sLu0eCgfFAMHFYuNEXzhrg3W0lPoiDpHjfxF335 14 | 7YyKqh8Tw7LWZ55l7Ry7I0e7f6CV5oyLPXI2hCAVUbLtjbQqErw1mRpfQPv0TypN 15 | UYdUfy/+xWk3OExFtIcKtug08x4WtjkOV63G6z2c//snXjxr4SjdFEmJAoGBAMlp 16 | n1uIhcOt3ZuBOitaXdm6Mo6fJxxit+dZM+/vh68kvnnUVdo08AQvGZMqlnwFwsie 17 | hfl084tZz1GPK8cbLj8TxSIjDhO8oKH6TEP+Pc5MyLklWK62Vwe+GIwYfLO2P9w+ 18 | c9M9HMsUqHO9E+UAREKYT8PYb3b/T0A89IMQHKjNAoGAafJbLeRuMT7wQpnUvHtc 19 | 8nJIV8ZtjhWFy/c8gCeSGwo6/ifCW79SrVfLa8nnxQETgQDMbu4I+DVNt5u97GHX 20 | Z3rFa4UPtnrrxwwJwFfW4CwTbnopehQnaS+pGOq24m5hN83jMUVOgQ69otvT7VgV 21 | ZAPAtP5nCCEzBre0z9dMxmkCgYBRqBrNd1oT7UdtJkU5Elf+T7jzjZ7DqVo51zZx 22 | TkFKRrFanzV3VAMDDz1lJtz/xy4jO/HBIyMiGfUtccgj3ucu94ryvheU9OKDCQEm 23 | h8ry+rpeJcAAhThfkThOhBb6nSbi2gcHWz1zMBrjYYLRUvxZq3qbQgJe0j+FIbFX 24 | +Y5E+QKBgHBjn13qdWiyi38jES7e2mLTnj8345qqg2dCiQcU2AmK555X12jgU7rJ 25 | UDzSfmhWB4lsEnXEolavKYuN0QoUS5HI3cTqJTI+K6YyD8LdgsI1Zxq3JC6p20So 26 | AYf06zDFI32mQiMlNAsCZrfJrzmEp80cNVDUGb/0S0eYFhVX8+Ut 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /fixtures/key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7D0N18WtF8SHxXipfOQ 3 | WCfLnp4sl4wiScCX2W6wevFepZd8fxDvrLdbN5YVpcQDqIGIHIjKZfeGHe8Teu6R 4 | z/PpfV7Tvwzq5P6Om3vs/z4Yx33GNybG/0BhcQnbM3f3L8Clcbl8WpwsotoUx/0s 5 | EzNXygiR8V0Dl73Rs6/Y3BtQMe4uzadvriPTaLLHISXO0kSFex/13Sia7zVAbNSm 6 | LeivM2PXsYLRdgWN0dmh4o6UQbX92AsQA85Z7678xSt1qx2eReu5dcNcVKLPT8fe 7 | qhqsVB+JjIpyfLc4UYTJzcrs19/sV6OI5dnDenVB4ggMAeNyrnVZ7gdQA8a59HzK 8 | tQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /jwt-spec.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var fs = require('fs'); 3 | var JWT = require('./jwt'); 4 | var privateKey = fs.readFileSync('./fixtures/key.pem'); 5 | var publicKey = fs.readFileSync('./fixtures/key.pub'); 6 | var wrongPublicKey = fs.readFileSync('./fixtures/badkey.pub'); 7 | 8 | describe('The JSON Web Token helper', function () { 9 | 10 | describe('comes with reasonable defaults. It', function () { 11 | var jwt = new JWT(privateKey); 12 | 13 | it('has a default algorithm of "RS256"', function () { 14 | jwt.algorithm.should.equal('RS256'); 15 | }); 16 | 17 | it('has a default expiry time of 1 month', function () { 18 | jwt.expiresInSeconds.should.equal(2592000); 19 | }); 20 | 21 | it('has a default issuer of "ch:server"', function () { 22 | jwt.issuer.should.equal('ch:server'); 23 | }); 24 | 25 | it('has a default subject of "ch:login"', function () { 26 | jwt.subject.should.equal('ch:login'); 27 | }); 28 | 29 | it('has a default audience of "ch:client"', function () { 30 | jwt.audience.should.equal('ch:client'); 31 | }); 32 | }); 33 | 34 | describe('has a constructor, which', function () { 35 | throwsIf('the given secret is not a valid RSA private key', function () { 36 | var jwt = new JWT('badsecret'); 37 | }); 38 | 39 | it('warns if no private key was given', function () { 40 | var log = console.log; 41 | var called = false; 42 | var spy = new Spy(console, 'log'); 43 | var jwt = new JWT(); 44 | spy.called.should.be.true; 45 | }) 46 | }); 47 | 48 | describe('has a `sign` method, which', function () { 49 | var jwt = new JWT(privateKey); 50 | var jwtNoKey = new JWT(); 51 | var payload = {my: 'payload'}; 52 | var badPayload = {iss: 'whatever'}; 53 | 54 | it('returns a token (String)', function () { 55 | var token = jwt.sign(payload); 56 | token.should.be.a.String; 57 | }); 58 | 59 | throwsIf('no private key was given', function () { 60 | jwtNoKey.sign(payload); 61 | }); 62 | 63 | throwsIf('no payload was given', function () { 64 | jwt.sign(); 65 | }); 66 | 67 | throwsIf('the payload contains reserved keys', function () { 68 | jwt.sign(badPayload); 69 | }); 70 | }); 71 | 72 | 73 | describe('has a `verify` method, which', function () { 74 | var jwt = new JWT(privateKey); 75 | var payload = {my: 'payload'}; 76 | var token = jwt.sign(payload); 77 | var goodResult = jwt.verify(token, publicKey); 78 | var badResult = jwt.verify(token, wrongPublicKey); 79 | 80 | throwsIf('no token is given', function () { 81 | jwt.verify(undefined, publicKey); 82 | }); 83 | 84 | throwsIf('no public key is given', function () { 85 | jwt.verify(token, undefined); 86 | }); 87 | 88 | throwsIf('the public key is invalid', function () { 89 | jwt.verify(token, 'totallyNotLegit'); 90 | }); 91 | 92 | describe('returns a result, which', function () { 93 | it('indicates a valid token', function () { 94 | goodResult.valid.should.be.true; 95 | }); 96 | 97 | it('indicates an invalid token', function () { 98 | badResult.valid.should.be.false; 99 | }); 100 | 101 | it('indicates why an invalid token was invalid', function () { 102 | // This might be because the token is malformed, expired, etc 103 | badResult.reason.should.be.a.String; 104 | }); 105 | 106 | it('contains the JWT token metadata', function () { 107 | goodResult.iss.should.be.a.String; 108 | goodResult.aud.should.be.a.String; 109 | goodResult.sub.should.be.a.String; 110 | goodResult.exp.should.be.a.Number; 111 | goodResult.iat.should.be.a.Number; 112 | }); 113 | 114 | it('contains the payload', function () { 115 | for (var key in payload) { 116 | goodResult[key].should.equal(payload[key]); 117 | } 118 | }); 119 | }); 120 | }); 121 | 122 | describe('has a method called `decode`, which', function () { 123 | var jwt = new JWT(privateKey); 124 | var token = jwt.sign({my: 'payload'}); 125 | 126 | throwsIf('the token is not provided', function () { 127 | jwt.decode(undefined); 128 | }); 129 | 130 | throwsIf('the token is not a string', function () { 131 | jwt.decode(24); 132 | }); 133 | 134 | throwsIf('the token is malformed', function () { 135 | jwt.decode('totallyNotLegit'); 136 | }); 137 | 138 | it('returns the token data as an object', function () { 139 | var data = jwt.decode(token); 140 | data.my.should.equal('payload'); 141 | data.iss.should.be.a.String; 142 | }); 143 | }); 144 | }); 145 | 146 | function throwsIf(desc, fn) { 147 | it('throws if ' + desc, function () { 148 | should.throws(fn, Error); 149 | }); 150 | } 151 | 152 | function Spy (object, methodName) { 153 | var old = object[methodName]; 154 | this.called = false; 155 | var _this = this; 156 | 157 | object[methodName] = function () { 158 | _this.called = true; 159 | object[methodName] = old; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /jwt.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function JWT (secret, options) { 4 | this.jwt = require('jsonwebtoken'); 5 | 6 | var defaultSigningOptions = { 7 | algorithm: 'RS256', 8 | expiresInSeconds: 2592000, // 1 month 9 | issuer: 'ch:server', 10 | subject: 'ch:login', 11 | audience: 'ch:client' 12 | }; 13 | 14 | var options = options || {}; 15 | 16 | this._signingOptions = _.extend({}, defaultSigningOptions, options); 17 | this.secret = secret; 18 | _.extend(this, this._signingOptions); 19 | 20 | if (!this.secret) { 21 | this._noKey = true; 22 | } 23 | else { 24 | if (!isValidPrivateKey(this.secret)) { 25 | throw new Error('Invalid private key given: "' + this.secret + '".'); 26 | } 27 | } 28 | } 29 | 30 | JWT.prototype = { 31 | sign: function (payload) { 32 | if (this._noKey) { 33 | throw new Error('No private key given. Please provide a valid RSA private key.') 34 | return false; 35 | } 36 | 37 | var reservedKeys = ['iss', 'aud', 'sub', 'exp', 'iat']; 38 | var keys = includedKeys(payload, reservedKeys); 39 | 40 | if (keys.length > 0) { 41 | throw new Error('Payload includes one or more reserved keys: ' + keys); 42 | } 43 | 44 | return this.jwt.sign(_.extend({}, payload), this.secret, this._signingOptions); 45 | }, 46 | 47 | verify: function (token, cert) { 48 | var result; 49 | 50 | if (!token) { 51 | throw new Error('No token given.'); 52 | } 53 | 54 | if (!cert) { 55 | throw new Error('No public key given.'); 56 | } 57 | 58 | if (!isValidPublicKey(cert)) { 59 | throw new Error('Invalid public key: "' + cert + '"'); 60 | } 61 | 62 | try { 63 | // We wrap this in a try-catch because we only want 64 | // to raise an exception if there is a programming 65 | // error (as opposed to a missing piece of data). 66 | result = this.jwt.verify(token, cert, this._signingOptions); 67 | result.valid = true; 68 | } 69 | catch (err) { 70 | result = {valid: false, reason: err.message}; 71 | } 72 | 73 | return result; 74 | }, 75 | 76 | decode: function (token) { 77 | var bits, decodedBits, tokenData; 78 | 79 | if (!token) { 80 | throw new Error('No token provided.'); 81 | } 82 | 83 | if (!token.split) { 84 | throw new Error('Invalid token provided: ' + token); 85 | } 86 | 87 | bits = token.split('.'); 88 | 89 | if (bits.length !== 3) { 90 | throw new Error('Malformed token provided: ' + token); 91 | } 92 | 93 | decodedBits = bits.map(decodeBit); 94 | tokenData = decodedBits[1]; 95 | 96 | return JSON.parse(tokenData); 97 | } 98 | }; 99 | 100 | module.exports = JWT; 101 | 102 | function warnPrivateKey () { 103 | console.log('Warning: no private key given. You will not be able to sign payloads with this instance. However, you can still verify tokens.'); 104 | } 105 | 106 | function isValidPrivateKey (secret) { 107 | return secret.toString('utf8').indexOf('-----BEGIN RSA PRIVATE KEY-----') === 0; 108 | } 109 | 110 | function isValidPublicKey (cert) { 111 | var certStr = cert.toString('utf8'); 112 | return certStr.indexOf('-----BEGIN PUBLIC KEY-----') === 0 || certStr.indexOf('-----BEGIN CERTIFICATE-----') === 0; 113 | } 114 | 115 | function includedKeys(obj, keys) { 116 | return keys.filter(function (key) { 117 | return obj.hasOwnProperty(key); 118 | }); 119 | } 120 | 121 | function decodeBit (encoded) { 122 | return new Buffer(encoded, 'base64').toString(); 123 | } 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chronicled/jwt", 3 | "version": "1.0.3", 4 | "description": "A simplified wrapper around [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken).", 5 | "main": "jwt.js", 6 | "scripts": { 7 | "test": "mocha jwt-spec.js --timeout 5000", 8 | "test:watch": "mocha --timeout 5000 --watch *.js jwt-spec.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "jsonwebtoken": "^5.0.0", 14 | "lodash": "^3.7.0", 15 | "should": "^6.0.1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/chronicled/jwt.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/chronicled/jwt/issues" 23 | }, 24 | "homepage": "https://github.com/chronicled/jwt" 25 | } 26 | --------------------------------------------------------------------------------