Skip to content

Commit f01288f

Browse files
committed
AUT-4272: render error pages upon CSRF error
Note that if the session is invalid (e.g., if it has expired) then we wish to render the standard "session expired" page. Previously in this case we would render the CSRF error text which is not user friendly. This commit renders the "session expired" page if a CSRF error has occurred, but this is due to an invalid session. If the session is still active, then we render the "internal server error" page. The user can use the browsers back button to return to the form on the previous page, the token in the "hidden" input will be reset to a valid value automatically, allowing the user to resubmit the form successfully.
1 parent e966bb6 commit f01288f

6 files changed

Lines changed: 94 additions & 15 deletions

File tree

src/app.constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export const ERROR_MESSAGES = {
139139
PAGE_NOT_FOUND: "Request page not found",
140140
};
141141

142+
export const CSRF_MISSING_CODE = "EBADCSRFTOKEN";
143+
142144
export const ERROR_LOG_LEVEL = {
143145
ERROR: "Error",
144146
INFO: "Info",
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import type { NextFunction, Request, Response } from "express";
2+
import { sessionIsValid } from "../middleware/session-middleware.js";
3+
import { CSRF_MISSING_CODE, HTTP_STATUS_CODES } from "../app.constants.js";
24

35
export function csrfMissingHandler(
46
err: any,
57
req: Request,
68
res: Response,
79
next: NextFunction
810
): void {
9-
const CSRF_MISSING_CODE = "EBADCSRFTOKEN";
1011
if (err.code === CSRF_MISSING_CODE) {
11-
// CSRF token missing or invalid
12-
res.status(403).send("Forbidden: Invalid or missing CSRF token");
13-
} else {
14-
next(err);
12+
if (sessionIsValid(req)) {
13+
req.log.error(
14+
`Invalid or missing CSRF token, but session is valid. Returning ${HTTP_STATUS_CODES.FORBIDDEN} status.`
15+
);
16+
17+
res.status(HTTP_STATUS_CODES.FORBIDDEN);
18+
} else {
19+
req.log.warn(
20+
`Session has expired - unable to validate CSRF token if present in request body. Returning ${HTTP_STATUS_CODES.UNAUTHORIZED} status.`
21+
);
22+
23+
res.status(HTTP_STATUS_CODES.UNAUTHORIZED);
24+
}
1525
}
26+
27+
next(err);
1628
}

src/handlers/internal-server-error-handler.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { NextFunction, Request, Response } from "express";
22
import { getAccountManagementUrl } from "../config.js";
3-
import { ERROR_MESSAGES, HTTP_STATUS_CODES } from "../app.constants.js";
3+
import {
4+
CSRF_MISSING_CODE,
5+
ERROR_MESSAGES,
6+
HTTP_STATUS_CODES,
7+
} from "../app.constants.js";
8+
49
export function serverErrorHandler(
510
err: any,
611
req: Request,
@@ -11,6 +16,13 @@ export function serverErrorHandler(
1116
return next(err);
1217
}
1318

19+
if (
20+
res.statusCode == HTTP_STATUS_CODES.FORBIDDEN &&
21+
err.code === CSRF_MISSING_CODE
22+
) {
23+
return res.render("common/errors/500.njk");
24+
}
25+
1426
if (
1527
(res.statusCode == HTTP_STATUS_CODES.UNAUTHORIZED &&
1628
err.message ===
Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import { expect } from "chai";
22
import type { NextFunction, Request, Response } from "express";
33
import sinon from "sinon";
4-
import { HTTP_STATUS_CODES } from "../../app.constants.js";
4+
import { CSRF_MISSING_CODE, HTTP_STATUS_CODES } from "../../app.constants.js";
55
import { csrfMissingHandler } from "../csrf-missing-handler.js";
6+
import { createMockRequest } from "../../../test/helpers/mock-request-helper.js";
67
describe("csrfMissingHandler", () => {
78
let req: Request;
89
let res: Response;
910
let next: NextFunction;
1011

1112
beforeEach(() => {
12-
req = {} as Request;
13+
req = createMockRequest("/", {
14+
session: {
15+
id: "session-id",
16+
sessionRestored: true,
17+
},
18+
cookies: {
19+
gs: "gs-cookie",
20+
aps: "aps-cookie",
21+
},
22+
});
1323
res = {
1424
headersSent: false,
1525
statusCode: 200,
@@ -23,17 +33,30 @@ describe("csrfMissingHandler", () => {
2333
next = sinon.spy();
2434
});
2535

26-
it("should return unauthorized if the error is a csrf missing error", () => {
36+
it("should return forbidden if the error is a csrf missing error and the session is valid", () => {
2737
const err = new Error("Invalid CSRF token") as any;
28-
err.code = "EBADCSRFTOKEN";
38+
err.code = CSRF_MISSING_CODE;
2939

3040
csrfMissingHandler(err, req, res, next);
3141

3242
expect(res.statusCode).to.equal(HTTP_STATUS_CODES.FORBIDDEN);
33-
expect(res.send).to.have.been.calledOnceWith(
34-
"Forbidden: Invalid or missing CSRF token"
35-
);
36-
expect(next).not.to.have.been.called;
43+
expect(req.log.error).to.have.been.calledOnce;
44+
45+
expect(next).to.have.been.called;
46+
});
47+
48+
it("should return unauthorized if the error is a csrf missing error and the session is invalid", () => {
49+
delete req.session;
50+
51+
const err = new Error("Invalid CSRF token") as any;
52+
err.code = CSRF_MISSING_CODE;
53+
54+
csrfMissingHandler(err, req, res, next);
55+
56+
expect(res.statusCode).to.equal(HTTP_STATUS_CODES.UNAUTHORIZED);
57+
expect(req.log.warn).to.have.been.calledOnce;
58+
59+
expect(next).to.have.been.called;
3760
});
3861

3962
it("should call next for any other error", () => {
@@ -42,6 +65,9 @@ describe("csrfMissingHandler", () => {
4265

4366
csrfMissingHandler(err, req, res, next);
4467

68+
expect(req.log.error).not.to.have.been.called;
69+
expect(req.log.warn).not.to.have.been.called;
70+
4571
expect(next).to.have.been.called;
4672
});
4773
});

src/handlers/tests/internal-server-error-handler.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { expect } from "chai";
22
import type { NextFunction, Request, Response } from "express";
33
import sinon from "sinon";
4-
import { ERROR_MESSAGES, HTTP_STATUS_CODES } from "../../app.constants.js";
4+
import {
5+
CSRF_MISSING_CODE,
6+
ERROR_MESSAGES,
7+
HTTP_STATUS_CODES,
8+
} from "../../app.constants.js";
59
import { serverErrorHandler } from "../internal-server-error-handler.js";
610
describe("serverErrorHandler", () => {
711
let req: Request;
@@ -34,6 +38,21 @@ describe("serverErrorHandler", () => {
3438
expect(res.statusCode).to.equal(200);
3539
});
3640

41+
it("should render 500 template if error is forbidden and the error is a csrf missing error", () => {
42+
const err = new Error("Invalid CSRF token") as any;
43+
err.code = CSRF_MISSING_CODE;
44+
45+
res.statusCode = HTTP_STATUS_CODES.FORBIDDEN;
46+
47+
const renderSpy = sinon.spy(res, "render");
48+
const expectedTemplate = "common/errors/500.njk";
49+
50+
serverErrorHandler(err, req, res, next);
51+
52+
expect(renderSpy).to.have.been.calledOnceWith(expectedTemplate);
53+
expect(res.statusCode).to.equal(HTTP_STATUS_CODES.FORBIDDEN);
54+
});
55+
3756
it("should render mid-journey-direct-navigation template if session is invalid and not a GOV.UK request", () => {
3857
res.statusCode = HTTP_STATUS_CODES.UNAUTHORIZED;
3958
const err = new Error(

test/helpers/mock-request-helper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { mockRequest } from "mock-req-res";
44

55
export interface MockRequestOptions {
66
headers?: any;
7+
session?: any;
8+
cookies?: any;
79
}
810

911
export function createMockRequest(
@@ -33,5 +35,11 @@ export function createMockRequest(
3335
if (request.headers["x-forwarded-for"]) {
3436
request.ip = options?.headers["x-forwarded-for"];
3537
}
38+
if (options?.session) {
39+
request.session = options.session;
40+
}
41+
if (options?.cookies) {
42+
request.cookies = options.cookies;
43+
}
3644
return request;
3745
}

0 commit comments

Comments
 (0)