Skip to content

Commit

Permalink
[#IOPID-273] Add generateNonce endpoint (#1070)
Browse files Browse the repository at this point in the history
* [#IOPID-273] add generateNonce endpoint

* [#IOPID-273] add tests

* [#IOPID-273] link online specs

* [#IOPID-273] update path

* [#IOPID-273] update fast-login openapi link

* [#IOPID-273] update fast-login commit id
  • Loading branch information
gquadrati authored Oct 31, 2023
1 parent c941eb7 commit 96db7f5
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 20 deletions.
4 changes: 2 additions & 2 deletions openapi/api_fast_login.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ components:
ProblemJson:
$ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v28.6.0/openapi/definitions.yaml#/ProblemJson"
Nonce:
$ref: "https://raw.githubusercontent.com/pagopa/io-functions-fast-login/1b47d311847b7f63dbfd97886122f4b2e94b79b4/api/internal.yaml#/components/schemas/Nonce"
$ref: "https://raw.githubusercontent.com/pagopa/io-functions-fast-login/44dc893c05a08ea3c34c0fa3a37e9667914ffb5d/api/internal.yaml#/components/schemas/Nonce"
GenerateNonceResponse:
$ref: "https://raw.githubusercontent.com/pagopa/io-functions-fast-login/1b47d311847b7f63dbfd97886122f4b2e94b79b4/api/internal.yaml#/components/schemas/GenerateNonceResponse"
$ref: "https://raw.githubusercontent.com/pagopa/io-functions-fast-login/44dc893c05a08ea3c34c0fa3a37e9667914ffb5d/api/internal.yaml#/components/schemas/GenerateNonceResponse"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"generate:api:io-cgn-operator-search": "rimraf generated/io-cgn-operator-search-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-cgn-operator-search/v2.6.2/openapi/index.yaml --no-strict --out-dir generated/io-cgn-operator-search-api --request-types --response-decoders --client",
"generate:api:pagopaproxy": "rimraf generated/pagopa-proxy && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v1.6.0/api-spec/api-for-io.yaml --no-strict --out-dir generated/pagopa-proxy --request-types --response-decoders --client",
"generate:api:lollipop": "rimraf generated/lollipop-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-lollipop/b78dda5f8d1ec64ee1dd4330c038cd00dd597dbf/openapi/internal.yaml --no-strict --out-dir generated/lollipop-api --request-types --response-decoders --client",
"generate:api:fast-login": "rimraf generated/fast-login-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-fast-login/1c49f57331482edb3a1918ee2481b80f65eb6a8b/api/internal.yaml --no-strict --out-dir generated/fast-login-api --request-types --response-decoders --client",
"generate:api:fast-login": "rimraf generated/fast-login-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-fast-login/44dc893c05a08ea3c34c0fa3a37e9667914ffb5d/api/internal.yaml --no-strict --out-dir generated/fast-login-api --request-types --response-decoders --client",
"generate:proxy:eucovidcert-models": "rimraf generated/eucovidcert && gen-api-models --api-spec api_eucovidcert.yaml --out-dir generated/eucovidcert",
"generate:proxy:mitvoucher": "rimraf generated/mitvoucher && gen-api-models --api-spec api_mit_voucher.yaml --out-dir generated/mitvoucher",
"generate:proxy:zendesk-models": "rimraf generated/zendesk && gen-api-models --api-spec api_zendesk.yaml --out-dir generated/zendesk",
Expand Down
10 changes: 9 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ import { LollipopApiClient } from "./clients/lollipop";
import { ISessionStorage } from "./services/ISessionStorage";
import { FirstLollipopConsumerClient } from "./clients/firstLollipopConsumer";
import { AdditionalLoginProps, acsRequestMapper } from "./utils/fastLogin";
import { fastLoginEndpoint } from "./controllers/fastLoginController";
import {
fastLoginEndpoint,
generateNonceEndpoint,
} from "./controllers/fastLoginController";
import { getFastLoginLollipopConsumerClient } from "./clients/fastLoginLollipopConsumerClient";
import { FeatureFlagEnum } from "./utils/featureFlag";
import AuthenticationLockService from "./services/authenticationLockService";
Expand Down Expand Up @@ -1693,6 +1696,11 @@ function registerFastLoginRoutes(
fastLoginLollipopConsumerClient: ReturnType<getFastLoginLollipopConsumerClient>,
tokenService: TokenService
): void {
app.post(
`${basePath}/fast-login/nonce/generate`,
toExpressHandler(generateNonceEndpoint(fastLoginLollipopConsumerClient))
);

app.post(
`${basePath}/fast-login`,
expressLollipopMiddleware(lollipopClient, sessionStorage),
Expand Down
134 changes: 118 additions & 16 deletions src/controllers/__tests__/fastLoginController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import * as RNEA from "fp-ts/lib/ReadonlyNonEmptyArray";
import { aFiscalCode } from "../../__mocks__/user_mock";
import TokenService from "../../services/tokenService";
import { AssertionTypeEnum } from "../../../generated/lollipop-api/AssertionType";
import { fastLoginEndpoint } from "../fastLoginController";
import {
fastLoginEndpoint,
generateNonceEndpoint,
} from "../fastLoginController";
import { Second } from "@pagopa/ts-commons/lib/units";
import { getFastLoginLollipopConsumerClient } from "../../clients/fastLoginLollipopConsumerClient";
import {
Expand Down Expand Up @@ -35,6 +38,16 @@ import { pipe } from "fp-ts/lib/function";
import { UserWithoutTokens } from "../../types/user";
import { SpidLevelEnum } from "../../../generated/backend/SpidLevel";
import { mockRedisClientSelector } from "../../__mocks__/redis";
import {
IResponse,
ResponseErrorInternal,
ResponseSuccessJson,
} from "@pagopa/ts-commons/lib/responses";
import {
ResponseErrorStatusNotDefinedInSpec,
ResponseErrorUnexpectedAuthProblem,
} from "../../utils/responses";
import { readableProblem } from "../../utils/errorsFormatter";

const mockSet = jest.fn();
const mockDel = jest.fn();
Expand Down Expand Up @@ -77,11 +90,22 @@ const validFastLoginLCResponse = {
saml_response: aSAMLResponse,
} as LCFastLoginResponse;

const aValidGenerateNonceResponse = {
nonce: "870c6d89-a3c4-48b1-a796-cdacddaf94b4",
};

const mockLCFastLogin = jest
.fn()
.mockResolvedValue(E.right({ status: 200, value: validFastLoginLCResponse }));
const mockGenerateNonce = jest
.fn()
.mockResolvedValue(
E.right({ status: 200, value: aValidGenerateNonceResponse })
);

const fastLoginLCClient = {
fastLogin: mockLCFastLogin,
generateNonce: mockGenerateNonce,
} as unknown as ReturnType<getFastLoginLollipopConsumerClient>;

const aBearerToken = "token" as LollipopJWTAuthorization;
Expand All @@ -104,14 +128,14 @@ const fastLoginLollipopLocals: LollipopLocalsType = {
["x-pagopa-lollipop-user-id"]: aFiscalCode,
};

const controller = fastLoginEndpoint(
const fastLoginController = fastLoginEndpoint(
fastLoginLCClient,
redisSessionStorage,
tokenService,
sessionTTL
);

describe("fastLoginController", () => {
describe("fastLoginController#fastLogin", () => {
beforeEach(() => {
jest.clearAllMocks();
});
Expand All @@ -138,7 +162,7 @@ describe("fastLoginController", () => {
mockSet.mockResolvedValueOnce(E.right(true));
const expectedClientIp = "10.0.0.2";

const response = await controller(
const response = await fastLoginController(
mockReq({ ip: expectedClientIp }),
fastLoginLollipopLocals
);
Expand All @@ -164,7 +188,7 @@ describe("fastLoginController", () => {
});

it("should fail when lollipop locals are invalid", async () => {
const response = await controller(mockReq(), {
const response = await fastLoginController(mockReq(), {
...fastLoginLollipopLocals,
"x-pagopa-lollipop-user-id": "NOTAFISCALCODE",
});
Expand All @@ -182,7 +206,10 @@ describe("fastLoginController", () => {

it("should fail when the lollipop consumer can't be contacted", async () => {
mockLCFastLogin.mockRejectedValueOnce(null);
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -198,7 +225,10 @@ describe("fastLoginController", () => {

it("should fail when the lollipop consumer gives a decoding error", async () => {
mockLCFastLogin.mockResolvedValueOnce(BadRequest.decode({}));
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -216,7 +246,10 @@ describe("fastLoginController", () => {

it("should return 401 when the lollipop consumer gives a 401 Unauthorized", async () => {
mockLCFastLogin.mockResolvedValueOnce(E.right({ status: 401 }));
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -240,7 +273,10 @@ describe("fastLoginController", () => {
mockLCFastLogin.mockResolvedValueOnce(
E.right({ status: status, value: { detail: "error", title: "error" } })
);
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -259,7 +295,10 @@ describe("fastLoginController", () => {
mockLCFastLogin.mockResolvedValueOnce(
E.right({ status: 200, value: { saml_response: "" } })
);
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -275,7 +314,10 @@ describe("fastLoginController", () => {

it("should return 403 when the user is blocked", async () => {
mockIsBlockedUser.mockResolvedValueOnce(E.right(true));
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -292,7 +334,10 @@ describe("fastLoginController", () => {

it("should return 500 when the session storage could not determine if the user is blocked", async () => {
mockIsBlockedUser.mockResolvedValueOnce(E.left(new Error("error")));
const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -316,7 +361,10 @@ describe("fastLoginController", () => {
mockIsBlockedUser.mockResolvedValueOnce(E.right(false));
makeProxyUser.mockReturnValueOnce(UserWithoutTokens.decode({}));

const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -336,7 +384,10 @@ describe("fastLoginController", () => {
mockIsBlockedUser.mockResolvedValueOnce(E.right(false));
mockSet.mockReturnValueOnce(E.left(new Error("error")));

const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -360,7 +411,10 @@ describe("fastLoginController", () => {
RNEA.map(() => mockGetNewToken.mockResolvedValueOnce(""))
);

const response = await controller(mockReq(), fastLoginLollipopLocals);
const response = await fastLoginController(
mockReq(),
fastLoginLollipopLocals
);
const res = mockRes();
response.apply(res);

Expand All @@ -378,7 +432,7 @@ describe("fastLoginController", () => {
});

it("should return 500 when the user IP has an unexpected value", async () => {
const response = await controller(
const response = await fastLoginController(
mockReq({ ip: "unexpected" }),
fastLoginLollipopLocals
);
Expand All @@ -395,3 +449,51 @@ describe("fastLoginController", () => {
);
});
});

describe("fastLoginController#generateNonce", () => {
beforeEach(() => {
jest.clearAllMocks();
});

const res = mockRes();

const generateNonceController = generateNonceEndpoint(fastLoginLCClient);

it("should return the nonce, when the the downstream component returns it", async () => {
const result = await generateNonceController(res);

expectToMatchResult(
result,
ResponseSuccessJson(aValidGenerateNonceResponse)
);
});

it.each`
title | clientResponse | expectedResult
${"should return InternalServerError when the client return 401"} | ${E.right({ status: 401 })} | ${ResponseErrorUnexpectedAuthProblem()}
${"should return InternalServerError when the client return 500"} | ${E.right({ status: 500, value: { title: "an Error" } })} | ${ResponseErrorInternal(readableProblem({ title: "an Error" }))}
${"should return InternalServerError when the client return 502"} | ${E.right({ status: 502 })} | ${ResponseErrorInternal("An error occurred on upstream service")}
${"should return InternalServerError when the client return 504"} | ${E.right({ status: 504 })} | ${ResponseErrorInternal("An error occurred on upstream service")}
${"should return InternalServerError when the client return a status code not defied in specs"} | ${E.right({ status: 418 })} | ${ResponseErrorStatusNotDefinedInSpec({ status: 418 } as never)}
`("$title", async ({ clientResponse, expectedResult }) => {
mockGenerateNonce.mockResolvedValue(clientResponse);

const result = await generateNonceController(res);

expectToMatchResult(result, expectedResult);
});
});

// ------------------------
// utilities
// ------------------------

function expectToMatchResult(
result: IResponse<any>,
expectedResult: IResponse<any>
) {
expect(result).toMatchObject({
...expectedResult,
apply: expect.any(Function),
});
}
54 changes: 54 additions & 0 deletions src/controllers/fastLoginController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import { ResLocals } from "../utils/express";
import { getFastLoginLollipopConsumerClient } from "../clients/fastLoginLollipopConsumerClient";
import TokenService from "../services/tokenService";
import { ISessionStorage } from "../services/ISessionStorage";
import { GenerateNonceResponse } from "../../generated/fast-login-api/GenerateNonceResponse";
import { FastLoginResponse as LCFastLoginResponse } from "../../generated/fast-login-api/FastLoginResponse";
import { readableProblem } from "../utils/errorsFormatter";
import { makeProxyUserFromSAMLResponse } from "../utils/spid";
import { UserV5 } from "../types/user";
import {
Expand All @@ -36,7 +38,9 @@ import {
} from "../types/token";
import {
IResponseErrorUnauthorized,
ResponseErrorStatusNotDefinedInSpec,
ResponseErrorUnauthorized,
ResponseErrorUnexpectedAuthProblem,
} from "../utils/responses";
import {
isUserElegibleForFastLogin,
Expand Down Expand Up @@ -150,6 +154,56 @@ const createSessionForUser = (
const callLcSetSession = (): TE.TaskEither<IResponseErrorInternal, true> =>
TE.of(true);

// ----------------------------------
// Endpoints
// ----------------------------------

export const generateNonceEndpoint =
(client: ReturnType<getFastLoginLollipopConsumerClient>) =>
async (
_req: express.Request
): Promise<
| IResponseErrorUnauthorized
| IResponseErrorForbiddenNotAuthorized
| IResponseErrorInternal
| IResponseSuccessJson<GenerateNonceResponse>
> =>
pipe(
TE.tryCatch(
() => client.generateNonce({}),
(_) => ResponseErrorInternal("Error while calling fast-login service")
),
TE.chainEitherKW(
E.mapLeft(
flow(readableReportSimplified, (message) =>
ResponseErrorInternal(
`Unexpected response from fast-login service: ${message}`
)
)
)
),
TE.chainW((response) => {
switch (response.status) {
case 200:
return TE.right(ResponseSuccessJson(response.value));
case 401:
return TE.left(ResponseErrorUnexpectedAuthProblem());
case 500:
return TE.left(
ResponseErrorInternal(readableProblem(response.value))
);
case 502:
case 504:
return TE.left(
ResponseErrorInternal("An error occurred on upstream service")
);
default:
return TE.left(ResponseErrorStatusNotDefinedInSpec(response));
}
}),
TE.toUnion
)();

export const fastLoginEndpoint =
(
client: ReturnType<getFastLoginLollipopConsumerClient>,
Expand Down

0 comments on commit 96db7f5

Please sign in to comment.