From 4f3829629fec1b7f4d243072213873b360388f75 Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Wed, 23 Oct 2024 15:46:41 +0200 Subject: [PATCH 1/2] refactor: removed unused Support Token functionality --- .env.example | 5 -- src/app.ts | 13 ---- src/config.ts | 38 ----------- .../__tests__/supportController.test.ts | 63 ------------------- src/controllers/supportController.ts | 52 --------------- src/services/__tests__/tokenService.test.ts | 35 ----------- src/services/tokenService.ts | 32 ---------- 7 files changed, 238 deletions(-) delete mode 100644 src/controllers/__tests__/supportController.test.ts delete mode 100644 src/controllers/supportController.ts diff --git a/.env.example b/.env.example index add2392b8..880de5d31 100644 --- a/.env.example +++ b/.env.example @@ -34,11 +34,6 @@ FF_IO_SIGN_ENABLED=1 FF_IO_WALLET_ENABLED=1 FF_IO_WALLET_TRIAL_ENABLED=1 -JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY="-----BEGIN RSA PRIVATE KEY-----\n -\n ------END RSA PRIVATE KEY-----" -JWT_SUPPORT_TOKEN_ISSUER=io-backend - # ------------------------------------ # FnAppMessages Env Variables # ------------------------------------ diff --git a/src/app.ts b/src/app.ts index 6b4cdc229..a5c51652a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -95,7 +95,6 @@ import { import ServicesAppBackendController from "./controllers/serviceAppBackendController"; import SessionLockController from "./controllers/sessionLockController"; import { getUserForMyPortal } from "./controllers/ssoController"; -import SupportController from "./controllers/supportController"; import UserDataProcessingController from "./controllers/userDataProcessingController"; import { ISessionStorage } from "./services/ISessionStorage"; import AuthenticationLockService from "./services/authenticationLockService"; @@ -460,7 +459,6 @@ export async function newApp({ PAGOPA_PROXY_SERVICE, USER_METADATA_STORAGE, USER_DATA_PROCESSING_SERVICE, - TOKEN_SERVICE, authMiddlewares.bearerSession, LOLLIPOP_API_CLIENT ); @@ -723,7 +721,6 @@ function registerAPIRoutes( pagoPaProxyService: PagoPAProxyService, userMetadataStorage: RedisUserMetadataStorage, userDataProcessingService: UserDataProcessingService, - tokenService: TokenService, // eslint-disable-next-line @typescript-eslint/no-explicit-any bearerSessionTokenAuth: any, lollipopClient: ReturnType @@ -762,10 +759,6 @@ function registerAPIRoutes( const userDataProcessingController: UserDataProcessingController = new UserDataProcessingController(userDataProcessingService); - const supportController: SupportController = new SupportController( - tokenService - ); - app.get( `${basePath}/profile`, bearerSessionTokenAuth, @@ -962,12 +955,6 @@ function registerAPIRoutes( pagoPAProxyController ) ); - - app.get( - `${basePath}/token/support`, - bearerSessionTokenAuth, - toExpressHandler(supportController.getSupportToken, supportController) - ); } // eslint-disable-next-line max-params diff --git a/src/config.ts b/src/config.ts index c3f18a5c0..f2c7002da 100644 --- a/src/config.ts +++ b/src/config.ts @@ -401,44 +401,6 @@ export const FF_IO_SIGN_ENABLED = process.env.FF_IO_SIGN_ENABLED === "1"; export const FF_EUCOVIDCERT_ENABLED = process.env.FF_EUCOVIDCERT_ENABLED === "1"; -// Support Token -export const JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY = pipe( - process.env.JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY, - NonEmptyString.decode, - E.getOrElseW((errs) => { - log.error( - `Missing or invalid JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY environment variable: ${readableReport( - errs - )}` - ); - return process.exit(1); - }) -); -export const JWT_SUPPORT_TOKEN_ISSUER = pipe( - process.env.JWT_SUPPORT_TOKEN_ISSUER, - NonEmptyString.decode, - E.getOrElseW((errs) => { - log.error( - `Missing or invalid JWT_SUPPORT_TOKEN_ISSUER environment variable: ${readableReport( - errs - )}` - ); - return process.exit(1); - }) -); - -const DEFAULT_JWT_SUPPORT_TOKEN_EXPIRATION = 604800 as Second; -export const JWT_SUPPORT_TOKEN_EXPIRATION: Second = pipe( - process.env.JWT_SUPPORT_TOKEN_EXPIRATION, - IntegerFromString.decode, - E.getOrElseW(() => DEFAULT_JWT_SUPPORT_TOKEN_EXPIRATION) -) as Second; - -log.info( - "JWT support token expiration set to %s seconds", - JWT_SUPPORT_TOKEN_EXPIRATION -); - export const TEST_CGN_FISCAL_CODES = pipe( process.env.TEST_CGN_FISCAL_CODES || "", CommaSeparatedListOf(FiscalCode).decode, diff --git a/src/controllers/__tests__/supportController.test.ts b/src/controllers/__tests__/supportController.test.ts deleted file mode 100644 index 576e60a21..000000000 --- a/src/controllers/__tests__/supportController.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as TE from "fp-ts/lib/TaskEither"; -import mockReq from "../../__mocks__/request"; -import TokenService from "../../services/tokenService"; -import { mockedUser } from "../../__mocks__/user_mock"; -import SupportController from "../supportController"; - -const mockGetSupportToken = jest.fn(); -jest.mock("../../services/tokenService", () => { - return { - default: jest.fn().mockImplementation(() => ({ - getJwtSupportToken: mockGetSupportToken - })) - }; -}); - -const aSupportToken = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJJU1NVRVIiLCJmaXNjYWxDb2RlIjoiQUFBQUFBODVBMjBBNTAxQSIsImlhdCI6MTUxNjIzOTAyMn0.JjuBKb2TEzyhofs_LwwRYwmPJ_ROKUDa_sK1frDTkvc"; - -describe("SupportController#getSupportToken", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should return a valid support token by calling TokenService with valid values", async () => { - const req = mockReq(); - - mockGetSupportToken.mockReturnValue(TE.of(aSupportToken)); - - req.user = mockedUser; - - const tokenService = new TokenService(); - const controller = new SupportController(tokenService); - - const response = await controller.getSupportToken(req); - - expect(response.kind).toEqual("IResponseSuccessJson"); - if (response.kind === "IResponseSuccessJson") { - expect(response.value.access_token).toEqual(aSupportToken); - } - }); - - it("should return an error if JWT Token generation fails", async () => { - const req = mockReq(); - - mockGetSupportToken.mockReturnValue( - TE.left(new Error("ERROR while generating JWT support token")) - ); - req.user = mockedUser; - - const tokenService = new TokenService(); - const controller = new SupportController(tokenService); - - const response = await controller.getSupportToken(req); - - // getUserDataProcessing is not called - expect(response.kind).toEqual("IResponseErrorInternal"); - if (response.kind === "IResponseErrorInternal") { - expect(response.detail).toEqual( - "Internal server error: ERROR while generating JWT support token" - ); - } - }); -}); diff --git a/src/controllers/supportController.ts b/src/controllers/supportController.ts deleted file mode 100644 index 4919b2d8e..000000000 --- a/src/controllers/supportController.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This controller returns info used to support logged user - */ - -import * as express from "express"; -import { - IResponseErrorInternal, - IResponseErrorValidation, - IResponseSuccessJson, - ResponseErrorInternal, - ResponseSuccessJson, -} from "@pagopa/ts-commons/lib/responses"; -import * as TE from "fp-ts/lib/TaskEither"; -import { pipe } from "fp-ts/lib/function"; -import { SupportToken } from "../../generated/backend/SupportToken"; -import { - JWT_SUPPORT_TOKEN_EXPIRATION, - JWT_SUPPORT_TOKEN_ISSUER, - JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY, -} from "../../src/config"; -import TokenService from "../../src/services/tokenService"; -import { withUserFromRequest } from "../types/user"; - -export default class SupportController { - constructor(private readonly tokenService: TokenService) {} - public readonly getSupportToken = ( - req: express.Request - ): Promise< - | IResponseErrorInternal - | IResponseErrorValidation - | IResponseSuccessJson - > => - withUserFromRequest(req, async (user) => - pipe( - this.tokenService.getJwtSupportToken( - JWT_SUPPORT_TOKEN_PRIVATE_RSA_KEY, - user.fiscal_code, - JWT_SUPPORT_TOKEN_EXPIRATION, - JWT_SUPPORT_TOKEN_ISSUER - ), - TE.map((token) => - SupportToken.encode({ - access_token: token, - expires_in: JWT_SUPPORT_TOKEN_EXPIRATION, - }) - ), - TE.mapLeft((e) => ResponseErrorInternal(e.message)), - TE.map(ResponseSuccessJson), - TE.toUnion - )() - ); -} diff --git a/src/services/__tests__/tokenService.test.ts b/src/services/__tests__/tokenService.test.ts index 73379a675..c0650c439 100644 --- a/src/services/__tests__/tokenService.test.ts +++ b/src/services/__tests__/tokenService.test.ts @@ -13,15 +13,6 @@ const aLastname = "Rossi" as NonEmptyString; const aFiscalCode = "AAAAAAAAAAAAAAA" as FiscalCode; const anEmailAddress = "mario.rossi@test.it" as EmailString; const aSharedSecret = "ASHAREDSECRET123" as NonEmptyString; -const aPrivateRsaKey = `-----BEGIN RSA PRIVATE KEY----- -MIIBOgIBAAJBAPX91rBDbLk5Pr0/lf4y1a8oz75sYa+slTqpfVHUrYb22qy4rY6Z -B0rXvTeLPgCAXUfGFJu4qSJcbu7yhBrPx30CAwEAAQJBALRCvEVUU2L0IRabdvXd -GJuP45ReZcNPS9e+BhimKjcgVFmyrpmiItNBHKFyTM8uL8dHXen1ReUgZOHcPKpV -MF0CIQD8KxN+ZhrxPIMPEJJJOO/Pn4y3iZRowulkaFDFUMUzzwIhAPm6vD95LAJW -DyC2relGDbA6h/YrBg38fcr1KQgxe0bzAiAcUL30oIR/+BqDU4oJnNIYz0KezV0T -0mcgtjHzphkuswIgXbRK1IpUECBYls7VHNXTZw/fWmg0YmUeklxBZDik6C8CIBXl -niQ7qszA7Uel9+wv2DwzWj+8OUcRzJAGOVD8cy2S ------END RSA PRIVATE KEY-----` as NonEmptyString; const tokenTtl = 60 as Second; const aTokenIssuer = "ISSUER" as NonEmptyString; @@ -52,32 +43,6 @@ describe("TokenService#getNewTokenAsync", () => { }); }); -describe("TokenService#getSupportToken", () => { - it("should generate a new support token", async () => { - // generate new token - const tokenService = new TokenService(); - const errorOrNewJwtToken = await tokenService.getJwtSupportToken( - aPrivateRsaKey, - aFiscalCode, - tokenTtl, - aTokenIssuer - )(); - expect(E.isRight(errorOrNewJwtToken)).toBeTruthy(); - }); - - it("should return an error if an error occurs during token generation", async () => { - // generate new token - const tokenService = new TokenService(); - const errorOrNewJwtToken = await tokenService.getJwtSupportToken( - "aPrivateRsaKey" as NonEmptyString, - aFiscalCode, - tokenTtl, - aTokenIssuer - )(); - expect(E.isLeft(errorOrNewJwtToken)).toBeTruthy(); - }); -}); - describe("TokenService#getZendeskSupportToken", () => { it("should generate a new zendesk support token", async () => { // generate new token diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts index 3f1d8f98d..12880e647 100644 --- a/src/services/tokenService.ts +++ b/src/services/tokenService.ts @@ -37,38 +37,6 @@ export default class TokenService { return asyncRandomBytes(length).then((_) => _.toString("hex")); } - /** - * Generates a new support token containing the logged user's fiscalCode. - * - * @param privateKey: The RSA's private key used to sign this JWT token - * @param fiscalCode: The logged user's FiscalCode - * @param tokenTtl: Token Time To live (expressed in seconds) - * @param issuer: The Token issuer - */ - public getJwtSupportToken( - privateKey: NonEmptyString, - fiscalCode: FiscalCode, - tokenTtl: Second, - issuer: NonEmptyString - ): TaskEither { - return pipe( - TE.taskify((cb) => - jwt.sign( - { fiscalCode }, - privateKey, - { - algorithm: "RS256", - expiresIn: `${tokenTtl} seconds`, - issuer, - jwtid: ulid(), - }, - cb - ) - )(), - TE.mapLeft(E.toError) - ); - } - /** * Generates a new zendesk support token containing the logged user's fiscalCode and email address. * From 170b2a6886d274f83e765240ac34f547d29d186b Mon Sep 17 00:00:00 2001 From: Salvatore Laiso Date: Fri, 25 Oct 2024 10:53:09 +0200 Subject: [PATCH 2/2] fix: remove unsed `TokenService` --- src/app.ts | 4 - src/config.ts | 3 +- .../__tests__/redisSessionStorage.test.ts | 8 -- src/services/__tests__/tokenService.test.ts | 99 ---------------- src/services/tokenService.ts | 109 ------------------ src/utils/__tests__/redis.test.ts | 5 +- 6 files changed, 3 insertions(+), 225 deletions(-) delete mode 100644 src/services/__tests__/tokenService.test.ts delete mode 100644 src/services/tokenService.ts diff --git a/src/app.ts b/src/app.ts index a5c51652a..0f3ee3d9d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -117,7 +117,6 @@ import ProfileService from "./services/profileService"; import RedisSessionStorage from "./services/redisSessionStorage"; import RedisUserMetadataStorage from "./services/redisUserMetadataStorage"; import ServicesAppBackendService from "./services/servicesAppBackendService"; -import TokenService from "./services/tokenService"; import UserDataProcessingService from "./services/userDataProcessingService"; import bearerMyPortalTokenStrategy from "./strategies/bearerMyPortalTokenStrategy"; import bearerSessionTokenStrategy from "./strategies/bearerSessionTokenStrategy"; @@ -307,9 +306,6 @@ export async function newApp({ return pipe( TE.tryCatch( async () => { - // Ceate the Token Service - const TOKEN_SERVICE = new TokenService(); - // Create the profile service const tableClient = TableClient.fromConnectionString( LOCKED_PROFILES_STORAGE_CONNECTION_STRING, diff --git a/src/config.ts b/src/config.ts index f2c7002da..eb9c9c0d4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,10 +17,9 @@ import { setFetchTimeout, toFetch, } from "@pagopa/ts-commons/lib/fetch"; -import { IntegerFromString } from "@pagopa/ts-commons/lib/numbers"; import { NonEmptyString, Ulid } from "@pagopa/ts-commons/lib/strings"; import { FiscalCode } from "@pagopa/ts-commons/lib/strings"; -import { Millisecond, Second } from "@pagopa/ts-commons/lib/units"; +import { Millisecond } from "@pagopa/ts-commons/lib/units"; import { pipe } from "fp-ts/lib/function"; import { CgnAPIClient } from "./clients/cgn"; import { log } from "./utils/logger"; diff --git a/src/services/__tests__/redisSessionStorage.test.ts b/src/services/__tests__/redisSessionStorage.test.ts index 70e37cbea..1b5e06cdd 100644 --- a/src/services/__tests__/redisSessionStorage.test.ts +++ b/src/services/__tests__/redisSessionStorage.test.ts @@ -74,14 +74,6 @@ const anInvalidUser: User = { fiscal_code: anInvalidFiscalCode, }; -const mockGetNewToken = jest.fn(); -jest.mock("../../services/tokenService", () => { - return { - default: jest.fn().mockImplementation(() => ({ - getNewToken: mockGetNewToken, - })), - }; -}); mockSetEx.mockImplementation((_, __, ___) => Promise.resolve("OK")); mockGet.mockImplementation((_) => Promise.resolve(JSON.stringify(aValidUser))); mockDel.mockImplementation((_) => Promise.resolve(1)); diff --git a/src/services/__tests__/tokenService.test.ts b/src/services/__tests__/tokenService.test.ts deleted file mode 100644 index c0650c439..000000000 --- a/src/services/__tests__/tokenService.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as E from "fp-ts/Either" -import { - EmailString, - FiscalCode, - NonEmptyString -} from "@pagopa/ts-commons/lib/strings"; -import { Second } from "@pagopa/ts-commons/lib/units"; -import TokenService from "../tokenService"; -import { pipe } from "fp-ts/lib/function"; - -const aFirstname = "Mario" as NonEmptyString; -const aLastname = "Rossi" as NonEmptyString; -const aFiscalCode = "AAAAAAAAAAAAAAA" as FiscalCode; -const anEmailAddress = "mario.rossi@test.it" as EmailString; -const aSharedSecret = "ASHAREDSECRET123" as NonEmptyString; -const tokenTtl = 60 as Second; -const aTokenIssuer = "ISSUER" as NonEmptyString; - -const aTokenLengthBytes = 48; -const aTokenLengthString = aTokenLengthBytes * 2; // because bytes - -const aPecServerSecretCode = "dummy-code" as NonEmptyString; -const aPecServerJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoiQUFBQUFBQUFBQUFBQUFBIn0.--ycAy9bJ9SYmTJs0TEU0fgLNk9PNCzhkucKUcg0gso"; - -describe("TokenService#getNewToken", () => { - it("generate a new token", () => { - // generate new token - const tokenService = new TokenService(); - const newToken = tokenService.getNewToken(aTokenLengthBytes); - - expect(newToken).toHaveLength(aTokenLengthString); - }); -}); - -describe("TokenService#getNewTokenAsync", () => { - it("generate a new token", async () => { - // generate new token - const tokenService = new TokenService(); - const newToken = await tokenService.getNewTokenAsync(aTokenLengthBytes); - - expect(newToken).toHaveLength(aTokenLengthString); - }); -}); - -describe("TokenService#getZendeskSupportToken", () => { - it("should generate a new zendesk support token", async () => { - // generate new token - const tokenService = new TokenService(); - const errorOrNewJwtToken = await tokenService - .getJwtZendeskSupportToken( - aSharedSecret, - aFirstname, - aLastname, - aFiscalCode, - anEmailAddress, - tokenTtl, - aTokenIssuer - )(); - expect(E.isRight(errorOrNewJwtToken)).toBeTruthy(); - }); - - it("should return an error if an error occurs during token generation", async () => { - // generate new token - const tokenService = new TokenService(); - const errorOrNewJwtToken = await tokenService - .getJwtZendeskSupportToken( - "" as NonEmptyString, - aFirstname, - aLastname, - aFiscalCode, - anEmailAddress, - tokenTtl, - aTokenIssuer - )(); - expect(E.isLeft(errorOrNewJwtToken)).toBeTruthy(); - }); -}); - -describe("TokenService#getPecServerTokenHandler", () => { - it("should generate a jwt token for Pec Server", async () => { - const tokenService = new TokenService(); - const pecServerJwt = await tokenService - .getPecServerTokenHandler(aFiscalCode)({ - secret: aPecServerSecretCode - } as any)(); - - expect(E.isRight(pecServerJwt)).toBeTruthy(); - expect(pipe(pecServerJwt, E.getOrElse(()=>""))).toBe(aPecServerJwt); - }); - - it("should return an error if an error occurs during token generation", async () => { - const tokenService = new TokenService(); - const pecServerJwt = await tokenService - .getPecServerTokenHandler(aFiscalCode)({ secret: "" } as any)(); - - expect(E.isLeft(pecServerJwt)).toBeTruthy(); - }); -}); diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts deleted file mode 100644 index 12880e647..000000000 --- a/src/services/tokenService.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * This file contains methods for dealing with session tokens. - */ - -import * as crypto from "crypto"; -import { promisify } from "util"; -import * as E from "fp-ts/lib/Either"; -import * as TE from "fp-ts/lib/TaskEither"; -import { - FiscalCode, - NonEmptyString, - EmailString, -} from "@pagopa/ts-commons/lib/strings"; -import { Second } from "@pagopa/ts-commons/lib/units"; -import * as jwt from "jsonwebtoken"; -import { ulid } from "ulid"; -import { TaskEither } from "fp-ts/lib/TaskEither"; -import { pipe } from "fp-ts/lib/function"; -import { PecServerConfig } from "src/config"; - -const asyncRandomBytes = promisify(crypto.randomBytes); - -export default class TokenService { - /** - * Generates a new random token. - */ - public getNewToken(length: number): string { - // Use the crypto.randomBytes as token. - return crypto.randomBytes(length).toString("hex"); - } - - /** - * Generates a new random token. - */ - public getNewTokenAsync(length: number): Promise { - // Use the crypto.randomBytes as token. - return asyncRandomBytes(length).then((_) => _.toString("hex")); - } - - /** - * Generates a new zendesk support token containing the logged user's fiscalCode and email address. - * - * @param secret: The shared secret used to sign this JWT token - * @param name: The logged user's first name - * @param familyName: The logged user's last name - * @param fiscalCode: The logged user's fiscal code - * @param emailAddress: The logged user's email address - * @param tokenTtl: Token Time To live (expressed in seconds) - * @param issuer: The Token issuer - */ - // eslint-disable-next-line max-params - public getJwtZendeskSupportToken( - secret: NonEmptyString, - name: NonEmptyString, - familyName: NonEmptyString, - fiscalCode: FiscalCode, - emailAddress: EmailString, - tokenTtl: Second, - issuer: NonEmptyString - ): TaskEither { - return pipe( - TE.taskify((cb) => - jwt.sign( - { - email: emailAddress, - external_id: fiscalCode, - iat: new Date().getTime() / 1000, - jti: ulid(), - name: `${name} ${familyName}`, - }, - secret, - { - algorithm: "HS256", - expiresIn: `${tokenTtl} seconds`, - issuer, - }, - cb - ) - )(), - TE.mapLeft(E.toError) - ); - } - - /** - * Generates a new PEC-SERVER support token containing the logged user's fiscalCode. - * - * @param config: The Pec Server configuration - * @param fiscalCode: The logged user's fiscal code - */ - public readonly getPecServerTokenHandler = - (fiscalCode: FiscalCode) => - (config: PecServerConfig): TE.TaskEither => - pipe( - TE.taskify((cb) => - jwt.sign( - { - account: fiscalCode, - }, - config.secret, - { - algorithm: "HS256", - noTimestamp: true, - }, - cb - ) - )(), - TE.mapLeft(E.toError) - ); -} diff --git a/src/utils/__tests__/redis.test.ts b/src/utils/__tests__/redis.test.ts index 4ee7322b0..29dc43f5b 100644 --- a/src/utils/__tests__/redis.test.ts +++ b/src/utils/__tests__/redis.test.ts @@ -1,4 +1,4 @@ -import TokenService from "../../services/tokenService"; +import { mockSessionToken } from "../../__mocks__/user_mock"; import { obfuscateTokensInfo } from "../redis"; export const SESSION_TOKEN_LENGTH_BYTES = 48; @@ -7,8 +7,7 @@ const getAnErrorMessage = (token: string) => `{"code":"UNCERTAIN_STATE","command":"MGET","args":["${token}"],"origin":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"}}`; describe("obfuscateTokensInfo", () => { it("should offuscate a token string", () => { - const token = new TokenService().getNewToken(SESSION_TOKEN_LENGTH_BYTES); - const errorMessage = getAnErrorMessage(`SESSION-${token}`); + const errorMessage = getAnErrorMessage(`SESSION-${mockSessionToken}`); expect(obfuscateTokensInfo(errorMessage)).toEqual( getAnErrorMessage(`SESSION-redacted`) );