From c2ec9c03bbdd8ab0cb111c6398a5d038b1036cf4 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Mon, 4 Dec 2023 17:27:33 -0500 Subject: [PATCH] Add exception and decoded token to error context Fixes #442 --- README.md | 4 +++- lib/index.d.ts | 12 +++++++++-- lib/index.js | 43 +++++++++++++++++++++++---------------- test/error_func.test.js | 17 +++++++++++++++- test/error_func_server.js | 11 ++++++++-- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 804592c..6d04b33 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,9 @@ signature `async function(decoded)` where: - `errorContext.message` - ***required*** the `message` passed into the `Boom` method call - `errorContext.schema` - the `schema` passed into the `Boom` method call - `errorContext.attributes` - the `attributes` passed into the `Boom` method call - - The function is expected to return the modified `errorContext` with all above fields defined. + - `errorContext.error` - the exception thrown (optional, if available) + - `errorContext.token` - the JWT provided, in string form (optional, if available) + - The function is expected to return the modified `errorContext` with all above non-optional fields defined. - `request` - the request object. - `h`- the response toolkit. - `urlKey` - (***optional*** *defaults to* `'token'`) - if you prefer to pass your token via url, simply add a `token` url parameter to your request or use a custom parameter by setting `urlKey`. To disable the url parameter set urlKey to `false` or ''. diff --git a/lib/index.d.ts b/lib/index.d.ts index 9aabc29..cc9cc13 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,5 +1,5 @@ import { Request, ResponseObject, Plugin, ResponseToolkit } from '@hapi/hapi'; -import { VerifyOptions } from 'jsonwebtoken'; +import { Jwt, VerifyOptions } from 'jsonwebtoken'; declare module '@hapi/hapi' { interface ServerAuth { @@ -30,6 +30,14 @@ declare namespace hapiAuthJwt2 { attributes?: { [key: string]: string; }; + /** + * the exception thrown (e.g., by `jsonwebtoken.verify`) + */ + error?: Error; + /** + * the decoded (possibly invalid) JWT received from the client + */ + decoded?: Jwt; } interface ValidationResult { @@ -153,4 +161,4 @@ declare namespace hapiAuthJwt2 { declare const hapiAuthJwt2: Plugin; -export = hapiAuthJwt2; \ No newline at end of file +export = hapiAuthJwt2; diff --git a/lib/index.js b/lib/index.js index 1698fca..d6b55aa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -94,8 +94,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', 'token is null', - tokenType, - null, //attributes + { scheme: tokenType }, true //flag missing token to HAPI auth framework to allow subsequent auth strategies ), payload: { @@ -113,8 +112,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', 'Invalid token format', - tokenType, - null, //attributes + { scheme: tokenType }, true //flag missing token to HAPI auth framework to allow subsequent auth strategies ), payload: { @@ -125,6 +123,7 @@ internals.authenticate = async (token, options, request, h) => { // verification is done later, but we want to avoid decoding if malformed request.auth.token = token; // keep encoded JWT available in the request // otherwise use the same key (String) to validate all JWTs + let decodeErr; try { decoded = JWT.decode(token, { complete: options.complete || false }); } catch (e) { @@ -133,6 +132,7 @@ internals.authenticate = async (token, options, request, h) => { // returning null, so here we just fall through to the following // block that tests if decoded is not set, so that we can handle // both failure types at once + decodeErr = e; } if (!decoded) { @@ -143,7 +143,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', 'Invalid token format', - tokenType + { scheme: tokenType, error: decodeErr } ), payload: { credentials: token, @@ -174,7 +174,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', err_message, - tokenType + { scheme: tokenType, decoded, error: verify_err } ), payload: { credentials: token }, }; @@ -195,7 +195,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', errorMessage || 'Invalid credentials', - tokenType + { scheme: tokenType, decoded } ), payload: { credentials: decoded }, }; @@ -220,7 +220,8 @@ internals.authenticate = async (token, options, request, h) => { request, h, 'boomify', - validate_err + validate_err, + { decoded } ), payload: { credentials: decoded, @@ -244,7 +245,7 @@ internals.authenticate = async (token, options, request, h) => { h, 'unauthorized', 'Invalid credentials', - tokenType + { scheme: tokenType, decoded } ), payload: { credentials: decoded }, }; @@ -261,7 +262,14 @@ internals.authenticate = async (token, options, request, h) => { }; } catch (verify_error) { return { - error: internals.raiseError(options, request, h, 'boomify', verify_error), + error: internals.raiseError( + options, + request, + h, + 'boomify', + verify_error, + { decoded } + ), payload: { credentials: decoded, }, @@ -275,17 +283,16 @@ internals.raiseError = function raiseError( request, h, errorType, - message, - scheme, - attributes, + errorOrMessage, + extraContext, isMissingToken ) { let errorContext = { errorType: errorType, - message: message, - scheme: scheme, - attributes: attributes, + message: errorOrMessage.toString(), + error: typeof errorOrMessage === 'object' ? errorOrMessage : undefined, }; + Object.assign(errorContext, extraContext); if (internals.isFunction(options.errorFunc)) { errorContext = options.errorFunc(errorContext, request, h); @@ -310,11 +317,11 @@ internals.raiseError = function raiseError( /** * implementation is the "main" interface to the plugin and contains all the - * "implementation details" (methods) such as authenicate, response & raiseError + * "implementation details" (methods) such as authenticate, response & raiseError * @param {Object} server - the Hapi.js server object we are attaching the * the hapi-auth-jwt2 plugin to. * @param {Object} options - any configuration options passed in. - * @returns {Function} authenicate - we return the authenticate method after + * @returns {Function} authenticate - we return the authenticate method after * registering the plugin as that's the method that gets called for each route. */ internals.implementation = (server, options) => { diff --git a/test/error_func.test.js b/test/error_func.test.js index 32ea588..c1934c7 100644 --- a/test/error_func.test.js +++ b/test/error_func.test.js @@ -2,7 +2,13 @@ const test = require('tape'); const JWT = require('jsonwebtoken'); // const secret = 'NeverShareYourSecret'; -const server = require('./error_func_server'); // test server which in turn loads our module +const { server, getLastErrorContext } = require('./error_func_server'); // test server which in turn loads our module + +function getPayloadFromDecoded(decoded) { + const payload = Object.assign({}, decoded); + delete payload.iat; + return payload; +} test("Access a route that has no auth strategy", async function(t) { const options = { @@ -25,8 +31,11 @@ test("customVerify simulate error condition", async function(t) { }; // server.inject lets us simulate an http request const response = await server.inject(options); + const errorContext = getLastErrorContext(); t.equal(response.statusCode, 500, "customVerify force error"); t.equal(response.result.message, "An internal server error occurred", "GET /required with forced error that has not been customised"); + t.deepEqual(getPayloadFromDecoded(errorContext.decoded), payload); + t.equal(errorContext.error.message, 'customVerify fails!'); t.end(); }); @@ -40,8 +49,11 @@ test("customVerify simulate error condition but is safely not customised", async }; // server.inject lets us simulate an http request const response = await server.inject(options); + const errorContext = getLastErrorContext(); t.equal(response.statusCode, 500, "customVerify force error"); t.equal(response.result.message, "An internal server error occurred", "GET /required with forced error that has not been customised"); + t.deepEqual(getPayloadFromDecoded(errorContext.decoded), payload); + t.equal(errorContext.error.message, 'ignore'); t.end(); }); @@ -55,9 +67,12 @@ test("customVerify with fail condition", async function(t) { }; // server.inject lets us simulate an http request const response = await server.inject(options); + const errorContext = getLastErrorContext(); t.equal(response.statusCode, 401, "GET /required with customVerify rejected"); t.equal(response.result.message, "Invalid credentials mate", "GET /required with customVerify rejected and customised error message"); t.deepEqual(response.headers['set-cookie'], [ 'customError=setInCustomErrorFn; Secure; HttpOnly; SameSite=Strict' ], 'Valid request should have access to the response toolkit object'); + t.deepEqual(getPayloadFromDecoded(errorContext.decoded), payload); + t.equal(errorContext.error, undefined); t.end(); }); diff --git a/test/error_func_server.js b/test/error_func_server.js index f816051..dd5f747 100644 --- a/test/error_func_server.js +++ b/test/error_func_server.js @@ -15,10 +15,16 @@ const privado = function(req, reply) { return req.auth.credentials; }; +let lastErrorContext = undefined; +function getLastErrorContext() { + return lastErrorContext; +} + // defining our own validate function lets us do something // useful/custom with the decodedToken before reply(ing) const customVerify = function (decoded, request) { - if(decoded.error) { + lastErrorContext = undefined; + if (decoded.error) { throw new Error('customVerify fails!'); } else if (decoded.custom_error) { @@ -34,6 +40,7 @@ const customVerify = function (decoded, request) { const customErrorFunc = function (errorContext, req, h) { const result = errorContext; + lastErrorContext = errorContext; h.response().state('customError', 'setInCustomErrorFn'); @@ -63,4 +70,4 @@ const init = async() => { init(); -module.exports = server; +module.exports = { server, getLastErrorContext };