Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#168453950] Adult check at login if dateOfBirth is provided #575

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/controllers/__tests__/authenticationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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", () => {
Expand Down
18 changes: 17 additions & 1 deletion src/controllers/authenticationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -62,6 +68,7 @@ export default class AuthenticationController {
// tslint:disable-next-line: max-union-size
| IResponseErrorInternal
| IResponseErrorValidation
| IResponseErrorForbidden
| IResponseErrorTooManyRequests
| IResponseErrorNotFound
| IResponsePermanentRedirect
Expand All @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/utils/__tests__/date.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
12 changes: 12 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -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);
};
22 changes: 22 additions & 0 deletions src/utils/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
*/
Expand Down