diff --git a/package.json b/package.json index f2342a7..adbefbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "giftbit-cassava-routes", - "version": "2.0.0", + "version": "3.0.0", "description": "Private Giftbit routes for use with Cassava.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -31,14 +31,14 @@ "@types/cookie": "^0.3.0", "@types/jsonwebtoken": "^7.2.1", "@types/mocha": "^2.2.41", - "@types/node": "^8.0.6", - "aws-sdk": "^2.80.0", + "@types/node": "^8.0.8", + "aws-sdk": "^2.81.0", "chai": "^4.0.2", "mocha": "^3.4.2", "rimraf": "^2.6.1", "ts-node": "^3.1.0", - "tslint": "^5.4.3", - "typescript": "2.3.4" + "tslint": "^5.5.0", + "typescript": "^2.4.1" }, "dependencies": { "jsonwebtoken": "^7.4.1" diff --git a/src/jwtauth/JwtAuthorizationRoute.test.ts b/src/jwtauth/JwtAuthorizationRoute.test.ts index 31d1b02..36274b6 100644 --- a/src/jwtauth/JwtAuthorizationRoute.test.ts +++ b/src/jwtauth/JwtAuthorizationRoute.test.ts @@ -99,6 +99,60 @@ describe("JwtAuthorizationRoute", () => { chai.assert.isTrue(secondHandlerCalled); }); + it("verifies a JWT with date strings", async() => { + // The spec calls for timestamps but we were issuing JWTs with date strings for a while. + // We'll still accept these technically-wrong JWTs. + + const router = new cassava.Router(); + const jwtAuthorizationRoute = new JwtAuthorizationRoute(Promise.resolve({secretkey:"secret"})); + jwtAuthorizationRoute.logErrors = false; + router.route(jwtAuthorizationRoute); + router.route({ + matches: () => true, + handle: async evt => ({body: {}}) + }); + + const resp = await cassava.testing.testRouter(router, cassava.testing.createTestProxyEvent("/foo/bar", "GET", { + headers: { + Authorization: "Bearer eyJ2ZXIiOjMsInZhdiI6MSwiYWxnIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.eyJnIjp7Imd1aSI6InVzZXItZjJmZTU3ZTg2ZjQyNDc5ZTg5YzYwMzRmZTg0NGJmM2UtVEVTVCIsImdtaSI6InVzZXItZjJmZTU3ZTg2ZjQyNDc5ZTg5YzYwMzRmZTg0NGJmM2UtVEVTVCJ9LCJpYXQiOiIyMDE3LTA3LTA1VDIyOjQ5OjU5LjcxMiswMDAwIiwiZXhwIjoiMjIxNy0wNy0wNVQyMzo0OTo1OS43MTIrMDAwMCIsImp0aSI6ImJhZGdlLTMzZTQ1N2E2NDQ0ZTQ2YjY5MzU3YjkyMDMyM2ZjYWY2IiwicGFyZW50SnRpIjoiYmFkZ2UtYmMyM2IyYmQxMmIwNDJiYTk1ZTQyMzJiODBhZTNhYWIiLCJzY29wZXMiOlsiQyIsIkNFQyIsIkNFUiIsImxpZ2h0cmFpbFYxOmNhcmRTZWFyY2giXSwicm9sZXMiOltdfQ.ydBnIZXP_i7dhsIAQ-ajWltPX1uweLcMixkfaBEDCK4" + } + })); + + chai.assert.equal(resp.statusCode, 200, JSON.stringify(resp)); + }); + + it("rejects an Authorization header missing 'Bearer '", async() => { + const router = new cassava.Router(); + const jwtAuthorizationRoute = new JwtAuthorizationRoute(Promise.resolve({secretkey:"secret"})); + jwtAuthorizationRoute.logErrors = false; + router.route(jwtAuthorizationRoute); + + const resp = await cassava.testing.testRouter(router, cassava.testing.createTestProxyEvent("/foo/bar", "GET", { + headers: { + Authorization: "eyJ2ZXIiOjEsInZhdiI6MSwiYWxnIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.eyJnIjp7Imd1aSI6InVzZXItNzA1MjIxMGJjYjk0NDQ4YjgyNWZmYTY4NTA4ZDI5YWQtVEVTVCIsImdtaSI6InVzZXItNzA1MjIxMGJjYjk0NDQ4YjgyNWZmYTY4NTA4ZDI5YWQifSwiaWF0IjoiMjAxNi0xMi0xMlQyMDoxMTo0MC45OTcrMDAwMCIsInNjb3BlcyI6WyJDIiwiVCIsIlIiLCJDRUMiLCJDRVIiLCJVQSIsIkYiXX0.uZxYrUPqwJk5oTTtDWaPOYzhRSt5dzRS4OZGYP8u2Po" + } + })); + + chai.assert.isObject(resp); + chai.assert.equal(resp.statusCode, 401, JSON.stringify(resp)); + }); + + it("rejects a JWT that is not base64", async() => { + const router = new cassava.Router(); + const jwtAuthorizationRoute = new JwtAuthorizationRoute(Promise.resolve({secretkey:"secret"})); + jwtAuthorizationRoute.logErrors = false; + router.route(jwtAuthorizationRoute); + + const resp = await cassava.testing.testRouter(router, cassava.testing.createTestProxyEvent("/foo/bar", "GET", { + headers: { + Authorization: "Bearer lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" + } + })); + + chai.assert.isObject(resp); + chai.assert.equal(resp.statusCode, 401, JSON.stringify(resp)); + }); + it("rejects an expired JWT", async() => { const router = new cassava.Router(); const jwtAuthorizationRoute = new JwtAuthorizationRoute(Promise.resolve({secretkey:"secret"})); @@ -214,4 +268,20 @@ describe("JwtAuthorizationRoute", () => { chai.assert.isObject(resp); chai.assert.equal(resp.statusCode, 401, JSON.stringify(resp)); }); + + it("rejects a JWT with alg:none", async() => { + const router = new cassava.Router(); + const jwtAuthorizationRoute = new JwtAuthorizationRoute(Promise.resolve({secretkey:"secret"})); + jwtAuthorizationRoute.logErrors = false; + router.route(jwtAuthorizationRoute); + + const resp = await cassava.testing.testRouter(router, cassava.testing.createTestProxyEvent("/foo/bar", "GET", { + headers: { + Authorization: "Bearer eyJ2ZXIiOjMsInZhdiI6MSwiYWxnIjoibm9uZSIsInR5cCI6IkpXVCJ9.eyJnIjp7Imd1aSI6InVzZXItZjJmZTU3ZTg2ZjQyNDc5ZTg5YzYwMzRmZTg0NGJmM2UtVEVTVCIsImdtaSI6InVzZXItZjJmZTU3ZTg2ZjQyNDc5ZTg5YzYwMzRmZTg0NGJmM2UtVEVTVCJ9LCJqdGkiOiJiYWRnZS0zM2U0NTdhNjQ0NGU0NmI2OTM1N2I5MjAzMjNmY2FmNiIsInNjb3BlcyI6WyJDIiwiQ0VDIiwiQ0VSIl0sInJvbGVzIjpbXX0.tFWA2jK8E0QVaG45h1BeARQQZxNJHVhIDh4-fs2qxhg" + } + })); + + chai.assert.isObject(resp); + chai.assert.equal(resp.statusCode, 401, JSON.stringify(resp)); + }); }); diff --git a/src/jwtauth/JwtAuthorizationRoute.ts b/src/jwtauth/JwtAuthorizationRoute.ts index 2fa45ae..ab8549a 100644 --- a/src/jwtauth/JwtAuthorizationRoute.ts +++ b/src/jwtauth/JwtAuthorizationRoute.ts @@ -14,8 +14,7 @@ export class JwtAuthorizationRoute implements cassava.routes.Route { constructor( private readonly authConfigPromise: Promise, - private readonly rolesConfigPromise?: Promise, - private readonly jwtOptions?: jwt.VerifyOptions) {} + private readonly rolesConfigPromise?: Promise) {} async handle(evt: cassava.RouterEvent): Promise { try { @@ -24,8 +23,10 @@ export class JwtAuthorizationRoute implements cassava.routes.Route { throw new Error("Secret is null. Check that the source of the secret can be accessed."); } + // Expiration time is checked manually because we issued JWTs with date string expirations, + // which is against the spec and the library rightly rejects those. const token = this.getToken(evt); - const payload = jwt.verify(token, secret.secretkey, this.jwtOptions); + const payload = jwt.verify(token, secret.secretkey, {ignoreExpiration: true, algorithms: ["HS256"]}); const auth = new AuthorizationBadge(payload, this.rolesConfigPromise ? await this.rolesConfigPromise : null); if (auth.expirationTime && auth.expirationTime.getTime() < Date.now()) { throw new Error(`jwt expired at ${auth.expirationTime} (and it is currently ${new Date()})`);