Skip to content

Commit

Permalink
[#IOPID-1148] Add login type opt-in to audit log data (#1078)
Browse files Browse the repository at this point in the history
  • Loading branch information
gquadrati authored Dec 1, 2023
1 parent 5bcccf6 commit 304fc27
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 66 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"@pagopa/io-functions-cgn-sdk": "x",
"@pagopa/io-functions-commons": "^28.8.0",
"@pagopa/io-functions-eucovidcerts-sdk": "x",
"@pagopa/io-spid-commons": "^13.2.2",
"@pagopa/io-spid-commons": "^13.3.0",
"@pagopa/ts-commons": "^12.3.0",
"@pagopa/winston-ts": "^2.2.0",
"applicationinsights": "^1.8.10",
Expand Down
19 changes: 17 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as TE from "fp-ts/lib/TaskEither";
import { pipe } from "fp-ts/lib/function";
import { ResponseSuccessJson } from "@pagopa/ts-commons/lib/responses";
import { Second } from "@pagopa/ts-commons/lib/units";
import { FiscalCode } from "generated/io-bonus-api/FiscalCode";
import { ServerInfo } from "../generated/public/ServerInfo";

import { VersionPerPlatform } from "../generated/public/VersionPerPlatform";
Expand Down Expand Up @@ -91,6 +92,7 @@ import {
ALLOWED_CIE_TEST_FISCAL_CODES,
LOCKED_PROFILES_STORAGE_CONNECTION_STRING,
LOCKED_PROFILES_TABLE_NAME,
isUserElegibleForFastLogin,
} from "./config";
import AuthenticationController from "./controllers/authenticationController";
import MessagesController from "./controllers/messagesController";
Expand Down Expand Up @@ -185,7 +187,12 @@ import {
import { LollipopApiClient } from "./clients/lollipop";
import { ISessionStorage } from "./services/ISessionStorage";
import { FirstLollipopConsumerClient } from "./clients/firstLollipopConsumer";
import { AdditionalLoginProps, acsRequestMapper } from "./utils/fastLogin";
import {
AdditionalLoginProps,
LoginTypeEnum,
acsRequestMapper,
getLoginType,
} from "./utils/fastLogin";
import {
fastLoginEndpoint,
generateNonceEndpoint,
Expand Down Expand Up @@ -744,7 +751,15 @@ export async function newApp({
SPID_LOG_STORAGE_CONNECTION_STRING,
SPID_LOG_QUEUE_NAME
);
const spidLogCallback = makeSpidLogCallback(spidQueueClient);
const spidLogCallback = makeSpidLogCallback(
spidQueueClient,
(fiscalCode: FiscalCode, loginType?: LoginTypeEnum) =>
getLoginType(
loginType,
isUserElegibleForFastLogin(fiscalCode),
FF_LOLLIPOP_ENABLED
)
);
const timer = TimeTracer();
return pipe(
TE.tryCatch(
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { CommaSeparatedListOf } from "./utils/separated-list";
import { LollipopApiClient } from "./clients/lollipop";
import { FirstLollipopConsumerClient } from "./clients/firstLollipopConsumer";
import { getFastLoginLollipopConsumerClient } from "./clients/fastLoginLollipopConsumerClient";
import { getIsUserElegibleForfastLogin } from "./utils/fastLogin";

// Without this, the environment variables loaded by dotenv aren't available in
// this file.
Expand Down Expand Up @@ -840,6 +841,11 @@ export const LV_TEST_USERS = pipe(
})
);

export const isUserElegibleForFastLogin = getIsUserElegibleForfastLogin(
LV_TEST_USERS,
FF_FAST_LOGIN
);

// Support Token
export const JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY = pipe(
process.env.JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY,
Expand Down
15 changes: 7 additions & 8 deletions src/controllers/__tests__/authenticationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
lollipopData,
} from "../../__mocks__/lollipop";
import * as authCtrl from "../authenticationController";
import * as config from "../../config";
import { withoutUndefinedValues } from "@pagopa/ts-commons/lib/types";
import { LoginTypeEnum } from "../../utils/fastLogin";
import {
Expand Down Expand Up @@ -209,7 +210,7 @@ jest.mock("../../services/usersLoginLogService", () => {
});

jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.spyOn(config, "isUserElegibleForFastLogin")
.mockImplementation((_) => false);

const anActivatedPubKey = {
Expand Down Expand Up @@ -1065,7 +1066,7 @@ describe("AuthenticationController|>LV|>acs", () => {
const res = mockRes();

jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.spyOn(config, "isUserElegibleForFastLogin")
.mockImplementationOnce((_) => isUserElegible);

mockGetLollipop.mockResolvedValueOnce(
Expand Down Expand Up @@ -1162,7 +1163,7 @@ describe("AuthenticationController|>LV|>acs", () => {
const res = mockRes();

jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.spyOn(config, "isUserElegibleForFastLogin")
.mockImplementationOnce((_) => true);

mockIsBlockedUser.mockReturnValue(Promise.resolve(E.right(false)));
Expand Down Expand Up @@ -1201,7 +1202,7 @@ describe("AuthenticationController|>LV|>acs", () => {
isUserAuthenticationLockedMock.mockReturnValueOnce(TE.of(true));

jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.spyOn(config, "isUserElegibleForFastLogin")
.mockImplementationOnce((_) => false);

mockGetLollipop.mockResolvedValueOnce(E.right(O.some(anotherAssertionRef)));
Expand Down Expand Up @@ -1235,7 +1236,7 @@ describe("AuthenticationController|>LV|>acs", () => {
const res = mockRes();

jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.spyOn(config, "isUserElegibleForFastLogin")
.mockImplementationOnce((_) => true);

mockIsBlockedUser.mockReturnValue(Promise.resolve(E.right(false)));
Expand All @@ -1255,9 +1256,7 @@ describe("AuthenticationController|>LV|>acs", () => {

describe("AuthenticationController|>LV|>acs|>notify user login", () => {
const setupLollipopScenario = () => {
jest
.spyOn(authCtrl, "isUserElegibleForFastLogin")
.mockReturnValueOnce(true);
jest.spyOn(config, "isUserElegibleForFastLogin").mockReturnValueOnce(true);

mockGetLollipop.mockResolvedValueOnce(E.right(O.some(anotherAssertionRef)));
mockDelLollipop.mockResolvedValueOnce(E.right(true));
Expand Down
34 changes: 11 additions & 23 deletions src/controllers/authenticationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
AdditionalLoginPropsT,
LoginTypeEnum,
acsRequestMapper,
getIsUserElegibleForfastLogin,
getLoginType,
} from "../utils/fastLogin";
import { isOlderThan } from "../utils/date";
import { AssertionRef } from "../../generated/lollipop-api/AssertionRef";
Expand All @@ -65,10 +65,9 @@ import {
ClientErrorRedirectionUrlParams,
clientProfileRedirectionUrl,
FF_IOLOGIN,
FF_FAST_LOGIN,
IOLOGIN_CANARY_USERS_SHA_REGEX,
IOLOGIN_USERS_LIST,
LV_TEST_USERS,
isUserElegibleForFastLogin,
} from "../config";
import { ISessionStorage } from "../services/ISessionStorage";
import ProfileService from "../services/profileService";
Expand Down Expand Up @@ -122,11 +121,6 @@ export const isUserElegibleForIoLoginUrlScheme =
FF_IOLOGIN
);

export const isUserElegibleForFastLogin = getIsUserElegibleForfastLogin(
LV_TEST_USERS,
FF_FAST_LOGIN
);

export default class AuthenticationController {
// eslint-disable-next-line max-params
constructor(
Expand Down Expand Up @@ -233,25 +227,19 @@ export default class AuthenticationController {
const isUserElegibleForFastLoginResult = isUserElegibleForFastLogin(
spidUser.fiscalNumber
);

// LV functionality is enable only if Lollipop is enabled.
// With FF set to BETA or CANARY, only whitelisted CF can use the LV functionality (the token TTL is reduced if login type is `LV`).
// With FF set to ALL all the user can use the LV (the token TTL is reduced if login type is `LV`).
// Otherwise LV is disabled.
const [sessionTTL, lollipopKeyTTL, loginType] =
this.lollipopParams.isLollipopEnabled &&
additionalProps?.loginType === LoginTypeEnum.LV &&
isUserElegibleForFastLoginResult
? [
this.lvTokenDurationSecs,
this.lvLongSessionDurationSecs,
LoginTypeEnum.LV,
]
: [
this.standardTokenDurationSecs,
this.standardTokenDurationSecs,
LoginTypeEnum.LEGACY,
];
const loginType = getLoginType(
additionalProps?.loginType,
isUserElegibleForFastLoginResult,
this.lollipopParams.isLollipopEnabled
);
const [sessionTTL, lollipopKeyTTL] =
loginType === LoginTypeEnum.LV
? [this.lvTokenDurationSecs, this.lvLongSessionDurationSecs]
: [this.standardTokenDurationSecs, this.standardTokenDurationSecs];

// Retrieve user IP from request
const errorOrUserIp = IPString.decode(spidUser.getAcsOriginalRequest()?.ip);
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/fastLoginController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ import {
ResponseErrorUnauthorized,
ResponseErrorUnexpectedAuthProblem,
} from "../utils/responses";
import { isUserElegibleForFastLogin } from "../config";
import {
isUserElegibleForFastLogin,
SESSION_ID_LENGTH_BYTES,
SESSION_TOKEN_LENGTH_BYTES,
} from "./authenticationController";
Expand Down
37 changes: 37 additions & 0 deletions src/utils/__tests__/fastLogin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LoginTypeEnum, getLoginType } from "../fastLogin";

describe("fastLogin|>getIsUserElegibleForfastLogin", () => {
it.each`
loginType | isUserElegibleForFastLogin | expectedResult
${undefined} | ${false} | ${LoginTypeEnum.LEGACY}
${undefined} | ${true} | ${LoginTypeEnum.LEGACY}
${LoginTypeEnum.LEGACY} | ${false} | ${LoginTypeEnum.LEGACY}
${LoginTypeEnum.LEGACY} | ${true} | ${LoginTypeEnum.LEGACY}
${LoginTypeEnum.LV} | ${false} | ${LoginTypeEnum.LEGACY}
${LoginTypeEnum.LV} | ${true} | ${LoginTypeEnum.LV}
`(
"should return $expectedResult when loginType is $loginType, user is eligible for fast-login $isUserEligibleForFastLogin and lollipop is enabled",
async ({ loginType, isUserElegibleForFastLogin, expectedResult }) => {
const result = getLoginType(loginType, isUserElegibleForFastLogin, true);

expect(result).toEqual(expectedResult);
}
);

it.each`
loginType | isUserElegibleForFastLogin
${undefined} | ${false}
${undefined} | ${true}
${LoginTypeEnum.LEGACY} | ${false}
${LoginTypeEnum.LEGACY} | ${true}
${LoginTypeEnum.LV} | ${false}
${LoginTypeEnum.LV} | ${true}
`(
"should return LoginTypeEnum.LEGACY when loginType is $loginType, user is eligible for fast-login $isUserEligibleForFastLogin and lollipop is NOT enabled",
async ({ loginType, isUserElegibleForFastLogin }) => {
const result = getLoginType(loginType, isUserElegibleForFastLogin, false);

expect(result).toEqual(LoginTypeEnum.LEGACY);
}
);
});
81 changes: 56 additions & 25 deletions src/utils/__tests__/spid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getSpidLevelFromSAMLResponse,
makeSpidLogCallback,
} from "../spid";
import { LoginTypeEnum } from "../fastLogin";

const aDOMSamlRequest = new DOMParser().parseFromString(
aSAMLRequest,
Expand Down Expand Up @@ -69,37 +70,65 @@ describe("SPID logs", () => {
const spidEmail = getSpidEmailFromAssertion(aDOMSamlResponse);
expect(spidEmail).toEqual(some("spid.tech@agid.gov.it"));
});
});
describe("SPID logs|>makeSpidLogCallback", () => {
const anIP = "127.0.0.0";

it("should enqueue valid payload on SPID response", () => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};
makeSpidLogCallback(mockQueueClient as unknown as QueueClient)(
"1.1.1.1",
aSAMLRequest,
aSAMLResponse
);
expect(mockQueueClient.sendMessage).toHaveBeenCalled();
});
const getLoginTypeMock = jest.fn().mockReturnValue(LoginTypeEnum.LEGACY);

it.each`
finalLoginType
${LoginTypeEnum.LEGACY}
${LoginTypeEnum.LV}
`(
"should enqueue valid payload on SPID response when final login type is $finalLoginType",
({ finalLoginType }) => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};

getLoginTypeMock.mockReturnValueOnce(finalLoginType);

makeSpidLogCallback(
mockQueueClient as unknown as QueueClient,
getLoginTypeMock
)(anIP, aSAMLRequest, aSAMLResponse, {
// NOTE: this is relevant for this test, only getLoginType result will be considered
loginType: LoginTypeEnum.LEGACY,
});
expect(mockQueueClient.sendMessage).toHaveBeenCalled();

const b64 = mockQueueClient.sendMessage.mock.calls[0][0];
const val = JSON.parse(Buffer.from(b64, "base64").toString("binary"));

expect(val).toMatchObject(
expect.objectContaining({
loginType: finalLoginType,
})
);
}
);

it("should NOT enqueue invalid IP on SPID response", () => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};
makeSpidLogCallback(mockQueueClient as unknown as QueueClient)(
"X",
aSAMLRequest,
aSAMLResponse
);
makeSpidLogCallback(
mockQueueClient as unknown as QueueClient,
getLoginTypeMock
)("X", aSAMLRequest, aSAMLResponse);
expect(mockQueueClient.sendMessage).not.toHaveBeenCalled();
});

it("should NOT enqueue undefined payload on SPID request", () => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};
makeSpidLogCallback(mockQueueClient as unknown as QueueClient)(
"1.1.1.1",
makeSpidLogCallback(
mockQueueClient as unknown as QueueClient,
getLoginTypeMock
)(
anIP,
// tslint:disable-next-line: no-any
undefined as any,
aSAMLResponse
Expand All @@ -111,8 +140,11 @@ describe("SPID logs", () => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};
makeSpidLogCallback(mockQueueClient as unknown as QueueClient)(
"1.1.1.1",
makeSpidLogCallback(
mockQueueClient as unknown as QueueClient,
getLoginTypeMock
)(
anIP,
aSAMLRequest,
// tslint:disable-next-line: no-any
undefined as any
Expand All @@ -124,11 +156,10 @@ describe("SPID logs", () => {
const mockQueueClient = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
};
makeSpidLogCallback(mockQueueClient as unknown as QueueClient)(
"1.1.1.1",
aSAMLRequest,
"RESPONSE"
);
makeSpidLogCallback(
mockQueueClient as unknown as QueueClient,
getLoginTypeMock
)(anIP, aSAMLRequest, "RESPONSE");
expect(mockQueueClient.sendMessage).not.toHaveBeenCalled();
});
});
11 changes: 11 additions & 0 deletions src/utils/fastLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ export const getIsUserElegibleForfastLogin = (
(_fiscalCode) => false,
FF_FastLogin
);

export const getLoginType = (
loginType: LoginTypeEnum | undefined,
isUserEligibleForFastLogin: boolean,
isLollipopEnabled: boolean
): LoginTypeEnum =>
isLollipopEnabled &&
loginType === LoginTypeEnum.LV &&
isUserEligibleForFastLogin
? LoginTypeEnum.LV
: LoginTypeEnum.LEGACY;
Loading

0 comments on commit 304fc27

Please sign in to comment.