Skip to content
This repository was archived by the owner on Aug 13, 2026. It is now read-only.
Closed
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
143 changes: 56 additions & 87 deletions src/middleware/lollipopMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";
import * as T from "fp-ts/lib/Task";
import * as B from "fp-ts/lib/boolean";
import * as E from "fp-ts/lib/Either";
import * as jose from "jose";
import { Request, Response } from "express-serve-static-core";
import { verifySignatureHeader } from "@mattrglobal/http-signatures";
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import {
getPublicKey,
isAssertionRefStillValid
Expand All @@ -16,6 +10,7 @@ import { ioDevServerConfig } from "../config";
import { signAlgorithmToVerifierMap } from "../utils/httpSignature";
import { serverUrl } from "../utils/server";
import { getProblemJson } from "../payloads/error";
import { unknownToString } from "../utils/error";

type LollipopHTTPStatusError = {
code: number;
Expand All @@ -29,89 +24,63 @@ export const isLollipopConfigEnabled = () =>

export const lollipopMiddleware =
(nextMiddleware: (embeddedRequest: Request, _: Response) => void) =>
(request: Request, response: Response) => {
pipe(
isLollipopConfigEnabled(),
B.fold(
() => nextMiddleware(request, response),
() =>
pipe(
TE.tryCatch(
() => verifyLollipopSignatureHeader(request, response),
_ => _ as Error
),
TE.map(verificationResult =>
pipe(
verificationResult,
E.foldW(
error => response.status(error.code).send(error.problemJson),
_ => nextMiddleware(request, response)
)
)
)
)()
)
);
async (request: Request, response: Response) => {
const isLollipopEnabled = isLollipopConfigEnabled();
if (isLollipopEnabled) {
const verificationEither = await verifyLollipopSignatureHeader(
request,
response
);
if (E.isLeft(verificationEither)) {
response
.status(verificationEither.left.code)
.send(verificationEither.left.problemJson);
return;
}
}
nextMiddleware(request, response);
};

const verifyLollipopSignatureHeader = (req: Request, _: Response) =>
pipe(
isAssertionRefStillValid(),
B.fold(
() => T.of(toFailureEither(403, "AssertionRef Invalid or Expired")),
() =>
pipe(
req.headers["signature-input"],
NonEmptyString.decode,
E.foldW(
_ => T.of(toFailureEither(400, "signature-input header is empty")),
() =>
pipe(
getPublicKey(),
O.fromNullable,
O.foldW(
() => T.of(toFailureEither(403, "Public key not found")),
publicKey =>
pipe(
TE.tryCatch(
() =>
verifySignatureHeader(
toVerifySignatureHeaderOptions(req, publicKey)
).unwrapOr({ verified: false }),
e => e as Error
),
TE.foldW(
e =>
T.of(
toFailureEither(
500,
e.message,
JSON.stringify(e.stack)
)
),
verificationResult =>
pipe(
verificationResult.verified,
B.fold(
() =>
T.of(
toFailureEither(
400,
"Invalid signature",
JSON.stringify(verificationResult)
)
),
() => T.of(toSuccessEither())
)
)
)
)
)
)
)
)
)
)();
const verifyLollipopSignatureHeader = async (
req: Request,
_: Response
): Promise<E.Either<LollipopHTTPStatusError, true>> => {
const isAssertionRefValid = isAssertionRefStillValid();
if (!isAssertionRefValid) {
return toFailureEither(403, "AssertionRef Invalid or Expired");
}

const signatureInput = req.headers["signature-input"];
if (typeof signatureInput !== "string" || signatureInput.length <= 0) {
return toFailureEither(400, "signature-input header is empty");
}

const publicKey = getPublicKey();
if (!publicKey) {
return toFailureEither(403, "Public key not found");
}

try {
const verificationResult = await verifySignatureHeader(
toVerifySignatureHeaderOptions(req, publicKey)
).unwrapOr({ verified: false });
if (!verificationResult.verified) {
return toFailureEither(
400,
"Invalid signature",
JSON.stringify(verificationResult)
);
}

return toSuccessEither();
} catch (e) {
const title =
e instanceof Error ? e.message : "lollipop signature verification failed";
const details =
e instanceof Error ? JSON.stringify(e.stack) : unknownToString(e);
return toFailureEither(500, title, details);
}
};

const toVerifySignatureHeaderOptions = (req: Request, publicKey: jose.JWK) => {
const headers = req.headers;
Expand Down
39 changes: 20 additions & 19 deletions src/persistence/appInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";
import { Request } from "express";

type osPlatform = "ios" | "android";
Expand All @@ -19,12 +16,12 @@ const osPerDevice: DeviceOS = {

type AppInfo = {
appVersion: string | undefined;
appOs: O.Option<osPlatform>;
appOs: osPlatform | undefined;
};

const appInfo: AppInfo = {
appVersion: undefined,
appOs: O.none
appOs: undefined
};

export function getAppVersion() {
Expand All @@ -36,7 +33,7 @@ export const clearAppInfo = () => {
// eslint-disable-next-line functional/immutable-data
appInfo.appVersion = undefined;
// eslint-disable-next-line functional/immutable-data
appInfo.appOs = O.none;
appInfo.appOs = undefined;
};

export function setAppInfo(req: Request) {
Expand All @@ -49,17 +46,21 @@ export function setAppInfo(req: Request) {
appInfo.appOs = os;
}

const getOsFromUserAgent = (req: Request) =>
pipe(
req.get("user-agent"),
O.fromNullable,
O.fold(
() => O.none,
userAgent =>
pipe(
Object.keys(osPerDevice),
A.findFirst(k => userAgent.includes(k)),
O.map(a => osPerDevice[a as keyof typeof osPerDevice])
)
)
const getOsFromUserAgent = (req: Request) => {
const userAgentMaybe = req.get("user-agent");
if (!userAgentMaybe) {
return undefined;
}

const normalizedUserAgent = userAgentMaybe.toLowerCase();

const keys = Object.keys(osPerDevice) as Array<keyof typeof osPerDevice>;
const keyMaybe = keys.find(key =>
normalizedUserAgent.includes(key.toLowerCase())
);
if (!keyMaybe) {
return undefined;
}

return osPerDevice[keyMaybe];
};
24 changes: 11 additions & 13 deletions src/persistence/lollipop.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as jose from "jose";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import { AssertionRef } from "../../generated/definitions/session_manager/AssertionRef";
import { DEFAULT_LOLLIPOP_HASH_ALGORITHM } from "../routers/public";
import { ioDevServerConfig } from "../config";
Expand Down Expand Up @@ -82,15 +80,15 @@ export function concretizeEphemeralInfo() {
}

// if is a ttl is defined in config for assertion ref, it checks its expiration, otherwise it is considered infinite
export const isAssertionRefStillValid = () =>
pipe(
ioDevServerConfig.features.lollipop.assertionRefValidityMS,
O.fromNullable,
O.fold(
() => true,
validity =>
!!lollipopInfo.instantiationDate &&
getDateMsDifference(new Date(), lollipopInfo.instantiationDate) <
validity
)
export const isAssertionRefStillValid = () => {
const assertionRefValidityMS =
ioDevServerConfig.features.lollipop.assertionRefValidityMS;
if (!assertionRefValidityMS) {
return true;
}
return (
!!lollipopInfo.instantiationDate &&
getDateMsDifference(new Date(), lollipopInfo.instantiationDate) <
assertionRefValidityMS
);
};
10 changes: 6 additions & 4 deletions src/persistence/profile/profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as R from "fp-ts/lib/Record";
import * as E from "fp-ts/lib/Either";
import { fakerIT as faker } from "@faker-js/faker";
import { Request } from "express";
Expand All @@ -16,7 +15,7 @@ import { CustomResponse, ResponseProblem } from "../../utils/responseTypes";
let currentProfile: InitializedProfile = {} as InitializedProfile;

export const getProfile = (): ProfileOperationsType["get"] => {
if (R.isEmpty(currentProfile)) {
if (isEmptyRecord(currentProfile)) {
initProfile();
}
return {
Expand Down Expand Up @@ -90,7 +89,7 @@ const initProfile = () => {
};

export const setProfileEmailValidated = (value: boolean) => {
if (R.isEmpty(currentProfile)) {
if (isEmptyRecord(currentProfile)) {
return;
}
currentProfile = {
Expand All @@ -101,7 +100,7 @@ export const setProfileEmailValidated = (value: boolean) => {
};

export const setProfileEmailAlreadyTaken = (value: boolean) => {
if (R.isEmpty(currentProfile)) {
if (isEmptyRecord(currentProfile)) {
return;
}
currentProfile = {
Expand Down Expand Up @@ -185,3 +184,6 @@ const profileSuccessOperations: ProfileOperationsType = {
payload: InitializedProfile
}
};

const isEmptyRecord = (input: Record<string, unknown>): boolean =>
Object.keys(input).length === 0;
1 change: 0 additions & 1 deletion src/persistence/sessionInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fakerIT as faker } from "@faker-js/faker";
import { Request } from "express";

import { ioDevServerConfig } from "../config";
import { isFeatureFlagWithMinVersionEnabled } from "../routers/features/featureFlagUtils";
import { getDateMsDifference } from "../utils/date";
Expand Down
36 changes: 17 additions & 19 deletions src/routers/features/fastLogin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
*/

import { Router } from "express";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as E from "fp-ts/lib/Either";
import { addHandler } from "../../../payloads/response";
import {
Expand Down Expand Up @@ -33,21 +31,21 @@ addHandler(
fastLoginRouter,
"post",
addApiAuthV1Prefix("/fast-login"),
lollipopMiddleware((req, res) =>
pipe(
refreshTokenWithFastLogin(req),
O.fromNullable,
O.fold(
() => res.status(401),
token =>
pipe(
FastLoginResponse.decode({ token }),
E.fold(
() => res.status(403),
response => res.status(200).send(response)
)
)
)
)
)
lollipopMiddleware((req, res) => {
const tokenMaybe = refreshTokenWithFastLogin(req);
if (!tokenMaybe) {
res.status(401);
return;
}

const fastLodingResponseEither = FastLoginResponse.decode({
token: tokenMaybe
});
if (E.isLeft(fastLodingResponseEither)) {
res.status(403);
return;
}

res.status(200).send(fastLodingResponseEither.right);
})
);
Loading
Loading