UNPKG

koa-jwt

Version:

Koa middleware for validating JSON Web Tokens

789 lines (619 loc) 21.3 kB
'use strict'; const Koa = require('koa'); const request = require('supertest'); const jwt = require('jsonwebtoken'); const koajwt = require('../lib'); const expect = require('chai').expect; describe('failure tests', () => { it('should throw 401 if no authorization header', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh' })); request(app.listen()) .get('/') .expect(401) .expect('Authentication Error') .end(done); }); it('should throw 401 if no authorization header', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh', debug: true })); request(app.listen()) .get('/') .expect(401) .expect('Token not found') .end(done); }); it('should return 401 if authorization header is malformed', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh' })); request(app.listen()) .get('/') .set('Authorization', 'wrong') .expect(401) .expect('Bad Authorization header format. Format is "Authorization: Bearer <token>"') .end(done); }); it('should return 401 if authorization header does not start with Bearer', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh' })); request(app.listen()) .get('/') .set('Authorization', 'Bearskin Jacket') .expect(401) .expect('Bad Authorization header format. Format is "Authorization: Bearer <token>"') .end(done); }); it('should allow provided getToken function to throw', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh', getToken: ctx => ctx.throw(401, 'Bad Authorization') })); request(app.listen()) .get('/') .expect(401) .expect('Bad Authorization') .end(done); }); it('should throw if getToken function returns invalid jwt', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', getToken: () => jwt.sign({foo: 'bar'}, 'bad'), debug: true })); request(app.listen()) .get('/') .expect(401) .expect('invalid signature') .end(done); }); it('should throw if authorization header is not well-formatted jwt', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhh', debug: true })); request(app.listen()) .get('/') .set('Authorization', 'Bearer wrongjwt') .expect(401) .expect('jwt malformed') .end(done); }); it('should throw if authorization header is not valid jwt', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: 'different-shhhh', debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('invalid signature') .end(done); }); it('should throw if authorization header is not valid jwt according to any secret', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: ['different-shhhh', 'some-other-shhhhh'], debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('invalid signature') .end(done); }); it('should throw non-descriptive errors when debug is false', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: 'different-shhhh', debug: false })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('Authentication Error') .end(done); }); it('should throw if opts.cookies is set and the specified cookie is not well-formatted jwt', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, cookie: 'jwt', debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Cookie', `jwt=bad${token};`) .expect(401) .expect('invalid token') .end(done); }); it('should throw if audience is not expected', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', aud: 'expected-audience'}, secret); const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', audience: 'not-expected-audience', debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('jwt audience invalid. expected: not-expected-audience') .end(done); }); it('should throw if token is expired', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', exp: 1382412921 }, secret); const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('jwt expired') .end(done); }); it('should throw with original jsonwebtoken error as originalError property', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', exp: 1382412921 }, secret); const app = new Koa(); // Custom 401 handling app.use((ctx, next) => { return next().catch(err => { expect(err).to.have.property('originalError'); expect(err.originalError.message).to.equal('jwt expired'); throw err; }); }); app.use(koajwt({ secret: 'shhhhhh', debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('jwt expired') .end(done); }); it('should throw if token issuer is wrong', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', iss: 'http://foo' }, secret); const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', issuer: 'http://wrong', debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('jwt issuer invalid. expected: http://wrong') .end(done); }); it('should throw if secret neither provided by options or middleware', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', iss: 'http://foo' }, secret); const app = new Koa(); app.use(koajwt({debug: true})); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('Secret not provided') .end(done); }); it('should throw if secret both provided by options (right secret) and middleware (wrong secret)', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar', iss: 'http://foo' }, secret); const app = new Koa(); app.use(koajwt({secret: 'wrong secret', debug: true})); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('invalid signature') .end(done); }); it('should throw 401 if isRevoked throw error', done => { const isRevoked = (ctx, token, user) => Promise.reject(new Error('Token revocation check error')); const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, isRevoked, debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('Token revocation check error') .end(done); }); it('should throw 401 if revoked token', done => { const isRevoked = (ctx, token, user) => Promise.resolve(true); const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, isRevoked, debug: true })); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('Token revoked') .end(done); }); it('should throw if secret provider rejects', done => { const secret = 'shhhhhh'; const provider = ({alg, kid}) => Promise.reject(new Error('Not supported')); const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: provider, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('Not supported') .end(done); }); it('should throw if secret provider used but token invalid', done => { const provider = ({ alg, kid }) => Promise.resolve('a nice secret'); const app = new Koa(); app.use(koajwt({ secret: provider, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', 'Bearer dodgytoken') .expect(401) .expect('Invalid token') .end(done); }); it('should throw if secret provider returns a secret that does not match jwt', done => { const secret = 'shhhhhh'; const provider = ({alg, kid}) => Promise.resolve('not my secret'); const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: provider, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('invalid signature') .end(done); }); it('should throw if no secret provider returns a secret that matches jwt', done => { const secret = 'shhhhhh'; const provider = ({alg, kid}) => Promise.resolve(['not my secret', 'still not my secret']) const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: provider, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(401) .expect('invalid signature') .end(done); }); }); describe('passthrough tests', () => { it('should continue if `passthrough` is true', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', passthrough: true, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .expect(204) // No content .expect('') .end(done); }); it('should continue if `passthrough` is true with bad auth header format', done => { const app = new Koa(); app.use(koajwt({ secret: 'shhhhhh', passthrough: true, debug: true })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', 'liver and onions') .expect(204) // No content .expect('') .end(done); }); }); describe('success tests', () => { it('should work if authorization header is valid jwt', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if authorization header contains leading and/or trailing whitespace', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', ` Bearer ${token} `) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if authorization header is valid jwt according to one of the secrets', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: [secret, 'another secret'] })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if the provided getToken function returns a valid jwt', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, getToken: ctx => ctx.request.query.token })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get(`/?token=${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should use the first resolved token', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const invalidToken = jwt.sign({foo: 'bar'}, 'badSecret'); const app = new Koa(); app.use(koajwt({ secret: secret, cookie: 'jwt'})); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Cookie', `jwt=${token};`) .set('Authorization', `Bearer ${invalidToken}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if opts.cookies is set and the specified cookie contains valid jwt', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, cookie: 'jwt' })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Cookie', `jwt=${token};`) .expect(200) .expect(validUserResponse) .end(done); }); it('should use provided key for decoded data', done => { const validUserResponse = res => res.body.foo === 'bar' && 'Key param not used properly'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, key: 'jwtdata' })); app.use(ctx => { ctx.body = ctx.state.jwtdata; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if secret is provided by middleware', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use((ctx, next) => { ctx.state.secret = secret; return next(); }); app.use(koajwt()); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if secret is provided by secret provider function', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const provider = ({ alg, kid }) => Promise.resolve(secret); const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: provider })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if a valid secret is provided by one of the secret provider functions', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const provider = ({ alg, kid }) => Promise.resolve(['other-shhhh', secret]); const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: provider })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should not overwrite ctx.state.token on successful token verification if opts.tokenKey is undefined', done => { const validUserResponse = res => res.body.token === 'DONT_CLOBBER_ME' && 'ctx.state.token not clobbered'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use((ctx, next) => { ctx.state = { token: 'DONT_CLOBBER_ME' }; return next(); }); app.use(koajwt({ secret: secret, key: 'jwtdata' })); app.use(ctx => { ctx.body = { token: ctx.state.token }; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should populate the raw token to ctx.state, in key from opts.tokenKey', done => { const validUserResponse = res => res.body.token !== token && 'Token not passed through'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, key: 'jwtdata', tokenKey: 'testTokenKey' })); app.use(ctx => { ctx.body = { token: ctx.state.testTokenKey }; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should use middleware secret if both middleware and options provided', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use((ctx, next) => { ctx.state.secret = secret; return next(); }); app.use(koajwt({secret: 'wrong secret'})); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); }); describe('unless tests', () => { it('should pass if the route is excluded', done => { const validUserResponse = res => res.body.success === true && 'koa-jwt is getting fired.'; const secret = 'shhhhhh'; const app = new Koa(); app.use(koajwt({ secret: secret }).unless({ path: ['/public']})); app.use(ctx => { ctx.body = { success: true }; }); request(app.listen()) .get('/public') .set('Authorization', 'wrong') .expect(200) .expect(validUserResponse) .end(done); }); it('should fail if the route is not excluded', done => { const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret }).unless({ path: ['/public']})); app.use(ctx => { ctx.body = { success: true }; }); request(app.listen()) .get('/private') .set('Authorization', 'wrong') .expect(401) .expect('Bad Authorization header format. Format is "Authorization: Bearer <token>"') .end(done); }); it('should pass if the route is not excluded and the token is present', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Key param not used properly'; const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, key: 'jwtdata' }).unless({ path: ['/public']})); app.use(ctx => { ctx.body = ctx.state.jwtdata; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); it('should work if authorization header is valid jwt and is not revoked', done => { const validUserResponse = res => res.body.foo !== 'bar' && 'Wrong user'; const isRevoked = (token, ctx, user) => Promise.resolve(false); const secret = 'shhhhhh'; const token = jwt.sign({foo: 'bar'}, secret); const app = new Koa(); app.use(koajwt({ secret: secret, isRevoked })); app.use(ctx => { ctx.body = ctx.state.user; }); request(app.listen()) .get('/') .set('Authorization', `Bearer ${token}`) .expect(200) .expect(validUserResponse) .end(done); }); });