diff --git a/src/controllers/__tests__/authenticationController.test.ts b/src/controllers/__tests__/authenticationController.test.ts index 650bceb4e..c9f374ba0 100644 --- a/src/controllers/__tests__/authenticationController.test.ts +++ b/src/controllers/__tests__/authenticationController.test.ts @@ -276,6 +276,10 @@ afterEach(() => { clock = clock.uninstall(); }); +// tslint:disable-next-line: no-var-requires +const dateUtils = require("../../utils/date"); +const mockIsOlderThan = jest.spyOn(dateUtils, "isOlderThan"); + describe("AuthenticationController#acs", () => { it("redirects to the correct url if userPayload is a valid User and a profile not exists", async () => { const res = mockRes(); @@ -443,6 +447,38 @@ describe("AuthenticationController#acs", () => { detail: "Redis error" }); }); + + it("should return a forbidden error response if user isn't adult", async () => { + const res = mockRes(); + + // Mock isOlderThan to return false + const mockInnerIsOlderThan = jest.fn(); + mockInnerIsOlderThan.mockImplementationOnce(() => false); + mockIsOlderThan.mockImplementationOnce(() => mockInnerIsOlderThan); + + const expectedDateOfBirth = "2000-01-01"; + const notAdultUser = { + ...validUserPayload, + dateOfBirth: expectedDateOfBirth + }; + const response = await controller.acs(notAdultUser); + response.apply(res); + + expect(controller).toBeTruthy(); + expect(mockIsOlderThan).toBeCalledWith(18); + expect(mockInnerIsOlderThan.mock.calls[0][0]).toEqual( + new Date(expectedDateOfBirth) + ); + expect(res.status).toHaveBeenCalledWith(403); + + const expectedForbiddenResponse = { + detail: expect.any(String), + status: 403, + title: "Forbidden", + type: undefined + }; + expect(res.json).toHaveBeenCalledWith(expectedForbiddenResponse); + }); }); describe("AuthenticationController#getUserIdentity", () => { diff --git a/src/controllers/authenticationController.ts b/src/controllers/authenticationController.ts index 0e609ef47..07b492d68 100644 --- a/src/controllers/authenticationController.ts +++ b/src/controllers/authenticationController.ts @@ -38,7 +38,13 @@ import { withUserFromRequest } from "../types/user"; import { log } from "../utils/logger"; -import { withCatchAsInternalError } from "../utils/responses"; +import { + IResponseErrorForbidden, + ResponseErrorForbidden, + withCatchAsInternalError +} from "../utils/responses"; + +import { isOlderThan } from "../utils/date"; export default class AuthenticationController { constructor( @@ -62,6 +68,7 @@ export default class AuthenticationController { // tslint:disable-next-line: max-union-size | IResponseErrorInternal | IResponseErrorValidation + | IResponseErrorForbidden | IResponseErrorTooManyRequests | IResponseErrorNotFound | IResponsePermanentRedirect @@ -78,6 +85,15 @@ export default class AuthenticationController { } const spidUser = errorOrUser.value; + + // If the user isn't an adult a forbidden response will be provided + if ( + fromNullable(spidUser.dateOfBirth).exists( + _ => !isOlderThan(18)(new Date(_), new Date()) + ) + ) { + return ResponseErrorForbidden("Forbidden", "The user must be an adult"); + } const sessionToken = this.tokenService.getNewToken() as SessionToken; const walletToken = this.tokenService.getNewToken() as WalletToken; const user = toAppUser(spidUser, sessionToken, walletToken); diff --git a/src/utils/__tests__/date.test.ts b/src/utils/__tests__/date.test.ts new file mode 100644 index 000000000..07765e187 --- /dev/null +++ b/src/utils/__tests__/date.test.ts @@ -0,0 +1,22 @@ +import { isOlderThan } from "../date"; + +const toDate = new Date("2020-01-01"); +const olderThanValue = 18; + +describe("Check if a birthdate is for an adult user", () => { + it("should return true if the user has more than 18 years old", () => { + const validOlderDate = new Date("2000-01-01"); + expect(isOlderThan(olderThanValue)(validOlderDate, toDate)).toBeTruthy(); + }); + + it("should return true if the user has exactly 18 years old", () => { + const validOlderDate = new Date("2002-01-01"); + expect(isOlderThan(olderThanValue)(validOlderDate, toDate)).toBeTruthy(); + }); + + it("should return false if the the user has less than 18 years old", () => { + expect( + isOlderThan(olderThanValue)(new Date("2002-01-02"), toDate) + ).toBeFalsy(); + }); +}); diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 000000000..863b7037c --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,12 @@ +import { addYears, isAfter } from "date-fns"; + +/** + * Returns a comparator of two dates that returns true if + * the difference in years is at least the provided value. + */ +export const isOlderThan = (years: number) => ( + dateOfBirth: Date, + when: Date +) => { + return !isAfter(addYears(dateOfBirth, years), when); +}; diff --git a/src/utils/responses.ts b/src/utils/responses.ts index d347ae7db..1f4e22e15 100644 --- a/src/utils/responses.ts +++ b/src/utils/responses.ts @@ -2,7 +2,9 @@ import * as express from "express"; import * as t from "io-ts"; import { errorsToReadableMessages } from "italia-ts-commons/lib/reporters"; import { + HttpStatusCodeEnum, IResponse, + ResponseErrorGeneric, ResponseErrorInternal, ResponseErrorValidation } from "italia-ts-commons/lib/responses"; @@ -24,6 +26,26 @@ export function ResponseNoContent(): IResponseNoContent { }; } +/** + * Interface for a forbidden error response. + */ +export interface IResponseErrorForbidden + extends IResponse<"IResponseErrorForbidden"> { + readonly detail: string; +} +/** + * Returns a forbidden error response with status code 403. + */ +export function ResponseErrorForbidden( + title: string, + detail: string +): IResponseErrorForbidden { + return { + ...ResponseErrorGeneric(HttpStatusCodeEnum.HTTP_STATUS_403, title, detail), + ...{ detail: `${title}: ${detail}`, kind: "IResponseErrorForbidden" } + }; +} + /** * Transforms async failures into internal errors */