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 1 commit
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
23 changes: 23 additions & 0 deletions src/controllers/__tests__/authenticationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,29 @@ describe("AuthenticationController#acs", () => {
detail: "Redis error"
});
});

it("should return a forbidden error response if user isn't adult", async () => {
const res = mockRes();

const todayDate = new Date();
const notAdultUser = {
...validUserPayload,
dateOfBirth: `${todayDate.getFullYear() - 17}-01-01`
};
const response = await controller.acs(notAdultUser);
response.apply(res);

expect(controller).toBeTruthy();
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 { isAdult } 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)
.map(isAdult)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34282a4

.getOrElse(true)
) {
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
34 changes: 34 additions & 0 deletions src/utils/__tests__/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isAdult } from "../date";

const today = new Date();

describe("Check if a birthdate is for an adult user", () => {
it("should return true if the user has more than 18 years old", () => {
const validAdultBirthdate = `${today.getFullYear() - 19}-01-01`;
expect(isAdult(validAdultBirthdate)).toBeTruthy();
});

it("should return true if the user has exactly 18 years old", () => {
const currentMount =
today.getMonth() + 1 < 10
? `0${today.getMonth() + 1}`
: today.getMonth() + 1;
const validAdultBirthdate = `${today.getFullYear() -
18}-${currentMount}-${today.getDate()}`;
expect(isAdult(validAdultBirthdate)).toBeTruthy();
});

it("should return false if the the user has less than 18 years old", () => {
const currentMount =
today.getMonth() + 1 < 10
? `0${today.getMonth() + 1}`
: today.getMonth() + 1;
const validNotAdultBirthdate = `${today.getFullYear() -
18}-${currentMount}-${today.getDate() - 1}`;
expect(isAdult(validNotAdultBirthdate)).toBeFalsy();
});

it("should return false if the birthdate is invalid", () => {
expect(isAdult("2000-13-32")).toBeFalsy();
});
});
34 changes: 34 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { isRight } from "fp-ts/lib/Either";
import * as t from "io-ts";

export const dateRegex = /^(?<year>[12]\d{3})-(?<month>0[1-9]|1[0-2])-(?<day>0[1-9]|[12]\d|3[01])$/;
const dateRegexGroups = t.interface({
day: t.string,
month: t.string,
year: t.string
});

export const isAdult = (dateOfBirth: string): boolean => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd refactor this method to take currentDate as input. This will simplify tests of isAdult() that should be deterministic and not use new Date() (tests should use hardcoded dates)

something like --> const isOlderThan = (years: number) => (dateOfBirth: Date, when: Date) => { ... }

moreover I'd move the date parsing into the caller and, if we already include something like date-fns (or momentjs) use a library rather than a regex to parse the input date

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll use date-fns that is already present into the project to handle date parsing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 34282a4

const result = dateRegex.exec(dateOfBirth);
const date = dateRegexGroups.decode(result?.groups);
if (isRight(date)) {
const year = Number(date.value.year);
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1;
const month = Number(date.value.month);
const dayNumber = Number(date.value.day);
const currentDayNumber = currentDate.getDate();
if (
year + 18 > currentYear ||
(year + 18 === currentYear &&
(currentMonth > month ||
(currentMonth === month && currentDayNumber > dayNumber)))
) {
return false;
} else {
return true;
}
}
return false;
};
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