Skip to content

Commit

Permalink
Merge pull request #446 from joshkel/extra-error-context
Browse files Browse the repository at this point in the history
Add exception and decoded token to error context
  • Loading branch information
nelsonic authored Dec 7, 2023
2 parents 38526f5 + c2ec9c0 commit 8e65d2b
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 24 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''.
Expand Down
12 changes: 10 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -153,4 +161,4 @@ declare namespace hapiAuthJwt2 {

declare const hapiAuthJwt2: Plugin<hapiAuthJwt2.RegisterOptions>;

export = hapiAuthJwt2;
export = hapiAuthJwt2;
43 changes: 25 additions & 18 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -143,7 +143,7 @@ internals.authenticate = async (token, options, request, h) => {
h,
'unauthorized',
'Invalid token format',
tokenType
{ scheme: tokenType, error: decodeErr }
),
payload: {
credentials: token,
Expand Down Expand Up @@ -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 },
};
Expand All @@ -195,7 +195,7 @@ internals.authenticate = async (token, options, request, h) => {
h,
'unauthorized',
errorMessage || 'Invalid credentials',
tokenType
{ scheme: tokenType, decoded }
),
payload: { credentials: decoded },
};
Expand All @@ -220,7 +220,8 @@ internals.authenticate = async (token, options, request, h) => {
request,
h,
'boomify',
validate_err
validate_err,
{ decoded }
),
payload: {
credentials: decoded,
Expand All @@ -244,7 +245,7 @@ internals.authenticate = async (token, options, request, h) => {
h,
'unauthorized',
'Invalid credentials',
tokenType
{ scheme: tokenType, decoded }
),
payload: { credentials: decoded },
};
Expand All @@ -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,
},
Expand All @@ -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);
Expand All @@ -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) => {
Expand Down
17 changes: 16 additions & 1 deletion test/error_func.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand Down
11 changes: 9 additions & 2 deletions test/error_func_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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');

Expand Down Expand Up @@ -63,4 +70,4 @@ const init = async() => {

init();

module.exports = server;
module.exports = { server, getLastErrorContext };

0 comments on commit 8e65d2b

Please sign in to comment.