From be5e7029e06b47643a6f440f5321feff7b9efe06 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Tue, 11 Aug 2026 17:14:25 +0200 Subject: [PATCH] Remove fp-ts from login parts --- src/middleware/lollipopMiddleware.ts | 143 +++++++++-------------- src/persistence/appInfo.ts | 39 ++++--- src/persistence/lollipop.ts | 24 ++-- src/persistence/profile/profile.ts | 10 +- src/persistence/sessionInfo.ts | 1 - src/routers/features/fastLogin/index.ts | 36 +++--- src/routers/features/featureFlagUtils.ts | 62 +++++----- src/routers/public.ts | 6 +- src/routers/session.ts | 30 +++-- src/utils/file.ts | 19 ++- 10 files changed, 164 insertions(+), 206 deletions(-) diff --git a/src/middleware/lollipopMiddleware.ts b/src/middleware/lollipopMiddleware.ts index add1c16b0..5b3a73bbe 100644 --- a/src/middleware/lollipopMiddleware.ts +++ b/src/middleware/lollipopMiddleware.ts @@ -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 @@ -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; @@ -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> => { + 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; diff --git a/src/persistence/appInfo.ts b/src/persistence/appInfo.ts index 6358dd849..635731ddb 100644 --- a/src/persistence/appInfo.ts +++ b/src/persistence/appInfo.ts @@ -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"; @@ -19,12 +16,12 @@ const osPerDevice: DeviceOS = { type AppInfo = { appVersion: string | undefined; - appOs: O.Option; + appOs: osPlatform | undefined; }; const appInfo: AppInfo = { appVersion: undefined, - appOs: O.none + appOs: undefined }; export function getAppVersion() { @@ -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) { @@ -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; + const keyMaybe = keys.find(key => + normalizedUserAgent.includes(key.toLowerCase()) ); + if (!keyMaybe) { + return undefined; + } + + return osPerDevice[keyMaybe]; +}; diff --git a/src/persistence/lollipop.ts b/src/persistence/lollipop.ts index 629575a8a..8bd2527b4 100644 --- a/src/persistence/lollipop.ts +++ b/src/persistence/lollipop.ts @@ -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"; @@ -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 ); +}; diff --git a/src/persistence/profile/profile.ts b/src/persistence/profile/profile.ts index 3a5683b19..80e8f8962 100644 --- a/src/persistence/profile/profile.ts +++ b/src/persistence/profile/profile.ts @@ -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"; @@ -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 { @@ -90,7 +89,7 @@ const initProfile = () => { }; export const setProfileEmailValidated = (value: boolean) => { - if (R.isEmpty(currentProfile)) { + if (isEmptyRecord(currentProfile)) { return; } currentProfile = { @@ -101,7 +100,7 @@ export const setProfileEmailValidated = (value: boolean) => { }; export const setProfileEmailAlreadyTaken = (value: boolean) => { - if (R.isEmpty(currentProfile)) { + if (isEmptyRecord(currentProfile)) { return; } currentProfile = { @@ -185,3 +184,6 @@ const profileSuccessOperations: ProfileOperationsType = { payload: InitializedProfile } }; + +const isEmptyRecord = (input: Record): boolean => + Object.keys(input).length === 0; diff --git a/src/persistence/sessionInfo.ts b/src/persistence/sessionInfo.ts index 4c252830d..f16c3c14e 100644 --- a/src/persistence/sessionInfo.ts +++ b/src/persistence/sessionInfo.ts @@ -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"; diff --git a/src/routers/features/fastLogin/index.ts b/src/routers/features/fastLogin/index.ts index 29a15aa32..a4d058035 100644 --- a/src/routers/features/fastLogin/index.ts +++ b/src/routers/features/fastLogin/index.ts @@ -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 { @@ -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); + }) ); diff --git a/src/routers/features/featureFlagUtils.ts b/src/routers/features/featureFlagUtils.ts index 22a6f7fa7..8ee165ab0 100644 --- a/src/routers/features/featureFlagUtils.ts +++ b/src/routers/features/featureFlagUtils.ts @@ -1,5 +1,3 @@ -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import * as E from "fp-ts/lib/Either"; import { PatternString } from "@pagopa/ts-commons/lib/strings"; import { compare } from "compare-versions"; @@ -19,38 +17,36 @@ type FeatureFlagWithMinAppVersion = Extract< }[keyof T] >; -export const isVersionValidAndActive = (version: string | undefined) => - pipe( - version, - PatternString(`^(?!0(.0)*$)\\d+(\\.\\d+)*$`).decode, - E.fold( - _ => false, - minAppVersion => - pipe( - getAppVersion(), - PatternString(`^(?!0(.0)*$)\\d+(\\.\\d+)*$`).decode, - E.fold( - _ => false, - userAppVersion => compare(minAppVersion, userAppVersion, "<=") - ) - ) - ) +export const isVersionValidAndActive = ( + version: string | undefined +): boolean => { + const versionEither = PatternString(`^(?!0(.0)*$)\\d+(\\.\\d+)*$`).decode( + version ); + if (E.isLeft(versionEither)) { + return false; + } + + const appVersionEither = PatternString(`^(?!0(.0)*$)\\d+(\\.\\d+)*$`).decode( + getAppVersion() + ); + if (E.isLeft(appVersionEither)) { + return false; + } + + return compare(versionEither.right, appVersionEither.right, "<="); +}; export const isFeatureFlagWithMinVersionEnabled = ( featureFlag: FeatureFlagWithMinAppVersion -) => - pipe( - O.fromNullable(backendStatus.config[featureFlag]?.min_app_version), - O.fold( - () => false, - (min_app_version: VersionPerPlatform) => - pipe( - getAppOs(), - O.fold( - () => false, - os => isVersionValidAndActive(min_app_version[os]) - ) - ) - ) - ); +): boolean => { + const minAppVersion = backendStatus.config[featureFlag]?.min_app_version; + if (!minAppVersion) { + return false; + } + const operatingSystem = getAppOs(); + if (!operatingSystem) { + return false; + } + return isVersionValidAndActive(minAppVersion[operatingSystem]); +}; diff --git a/src/routers/public.ts b/src/routers/public.ts index 351117065..4a47e1a82 100644 --- a/src/routers/public.ts +++ b/src/routers/public.ts @@ -4,9 +4,9 @@ import * as zlib from "zlib"; import { JwkPublicKey, parseJwkOrError } from "@pagopa/ts-commons/lib/jwk"; import chalk from "chalk"; +import { calculateJwkThumbprint } from "jose"; import { Response, Router } from "express"; import * as E from "fp-ts/lib/Either"; -import * as jose from "jose"; import { parseStringPromise } from "xml2js"; import { assetsFolder, ioDevServerConfig } from "../config"; import { WALLET_PAYMENT_PATH } from "../features/payments/utils/payment"; @@ -80,7 +80,7 @@ addHandler( return; } - const thumbprint = await jose.calculateJwkThumbprint( + const thumbprint = await calculateJwkThumbprint( jwkPK.right, DEFAULT_LOLLIPOP_HASH_ALGORITHM ); @@ -191,7 +191,7 @@ addHandler( res.sendStatus(400); return; } - const thumbprint = await jose.calculateJwkThumbprint( + const thumbprint = await calculateJwkThumbprint( jwkPK.right, DEFAULT_LOLLIPOP_HASH_ALGORITHM ); diff --git a/src/routers/session.ts b/src/routers/session.ts index 7b4feeec5..8987e157b 100644 --- a/src/routers/session.ts +++ b/src/routers/session.ts @@ -1,7 +1,5 @@ import { Router } from "express"; import { fakerIT as faker } from "@faker-js/faker"; -import { pipe } from "fp-ts/lib/function"; -import * as O from "fp-ts/lib/Option"; import { addHandler } from "../payloads/response"; import { getCustomSession, @@ -16,21 +14,19 @@ addHandler( sessionRouter, "get", addApiAuthV1Prefix("/session"), - ({ query }, res) => - pipe( - getCustomSession(query), - O.fromNullable, - O.fold( - () => res.sendStatus(401), - customSession => - res.json({ - ...customSession.payload, - ...(shouldAddLollipopAssertionRef(query) && { - lollipopAssertionRef: getAssertionRef() - }) - }) - ) - ) + ({ query }, res) => { + const sessionMaybe = getCustomSession(query); + if (!sessionMaybe) { + res.sendStatus(401); + return; + } + res.json({ + ...sessionMaybe.payload, + ...(shouldAddLollipopAssertionRef(query) && { + lollipopAssertionRef: getAssertionRef() + }) + }); + } ); addHandler(sessionRouter, "get", addApiV1Prefix("/token/support"), (_, res) => diff --git a/src/utils/file.ts b/src/utils/file.ts index 24f75d002..a62696046 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,7 +1,6 @@ import fs from "fs"; import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import { Response } from "express"; -import { pipe } from "fp-ts/lib/function"; import * as E from "fp-ts/lib/Either"; import { Validation } from "io-ts"; import { unknownToString } from "./error"; @@ -65,15 +64,15 @@ export const readFileAndDecode = ( filename: string, decode: (i: T) => Validation, res: Response -): Response => - pipe( - readFileAsJSON(filename), - decode, - E.fold( - errors => res.status(500).send(readableReport(errors)), - v => res.json(v) - ) - ); +) => { + const jsonFromFile = readFileAsJSON(filename); + const decodeEither = decode(jsonFromFile); + if (E.isLeft(decodeEither)) { + res.status(500).send(readableReport(decodeEither.left)); + return; + } + res.json(decodeEither.right); +}; export const contentTypeMapping: Record = { pdf: "application/pdf",