From 5eef1d2665952d53ef711e456b28c07ea0352de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Auricoste?= Date: Thu, 23 Nov 2023 10:49:18 +0100 Subject: [PATCH] fix: reduction de la longueur du JWT token (#850) * fix: reduction de la longueur du JWT token * fix: ajout de monitoring sentry * fix: ajout de commentaire pour retirer OldIScope --- server/src/security/accessTokenService.ts | 71 +++++++++++++++---- server/src/security/authenticationService.ts | 6 +- server/src/security/authorisationService.ts | 5 +- .../security/authorisationService.test.ts | 66 ++++++++--------- shared/index.ts | 1 + shared/utils/assertUnreachable.ts | 3 + shared/utils/index.ts | 1 + 7 files changed, 98 insertions(+), 55 deletions(-) create mode 100644 shared/utils/assertUnreachable.ts create mode 100644 shared/utils/index.ts diff --git a/server/src/security/accessTokenService.ts b/server/src/security/accessTokenService.ts index 2bfa7e3100..b6a3aca78a 100644 --- a/server/src/security/accessTokenService.ts +++ b/server/src/security/accessTokenService.ts @@ -3,14 +3,21 @@ import jwt from "jsonwebtoken" import { PathParam, QueryString, WithQueryStringAndPathParam, generateUri } from "shared/helpers/generateUri" import { IUserRecruteur } from "shared/models" import { IRouteSchema, ISecuredRouteSchema, WithSecurityScheme } from "shared/routes/common.routes" +import { assertUnreachable } from "shared/utils" import { Jsonify } from "type-fest" import { AnyZodObject, z } from "zod" +import { sentryCaptureException } from "@/common/utils/sentryUtils" import config from "@/config" +const INTERNET_EXPLORER_V10_MAX_LENGTH = 2083 +const OUTLOOK_URL_MAX_LENGTH = 2048 +const URL_MAX_LENGTH = Math.min(INTERNET_EXPLORER_V10_MAX_LENGTH, OUTLOOK_URL_MAX_LENGTH) + type SchemaWithSecurity = Pick & WithSecurityScheme -export type IScope = { +// TODO à retirer à partir du 01/02/2024 +type OldIScope = { schema: Schema options: | "all" @@ -23,8 +30,25 @@ export type IScope = { } } -export const generateScope = (scope: IScope): IScope => { - return scope +type NewIScope = { + method: Schema["method"] + path: Schema["path"] + options: + | "all" + | { + params: Schema["params"] extends AnyZodObject ? Jsonify> : undefined + querystring: Schema["querystring"] extends AnyZodObject ? Jsonify> : undefined + } + resources: { + [key in keyof Schema["securityScheme"]["ressources"]]: ReadonlyArray + } +} + +type IScope = NewIScope | OldIScope + +export const generateScope = (scope: Omit, "method" | "path"> & { schema: Schema }): NewIScope => { + const { schema, options, resources } = scope + return { options, resources, path: schema.path, method: schema.method } } export type IAccessToken = { @@ -58,25 +82,45 @@ function getAudience({ export function generateAccessToken( user: IUserRecruteur | IAccessToken["identity"], - scopes: ReadonlyArray>, + scopes: ReadonlyArray>, options: { expiresIn?: string } = {} ): string { - const audiences = scopesToAudiences(scopes) const identity: IAccessToken["identity"] = "_id" in user ? { type: "IUserRecruteur", _id: user._id.toString(), email: user.email.toLowerCase() } : user const data: IAccessToken = { identity, scopes, } - return jwt.sign(data, config.auth.user.jwtSecret, { - audience: audiences, + const token = jwt.sign(data, config.auth.user.jwtSecret, { expiresIn: options.expiresIn ?? config.auth.user.expiresIn, issuer: config.publicUrl, }) + if (token.length > URL_MAX_LENGTH) { + sentryCaptureException(Boom.internal(`Token généré trop long : ${token.length}`)) + } + return token +} + +function getMethodAndPath(scope: IScope) { + if ("schema" in scope) { + const { schema } = scope + const { method, path } = schema + return { method, path } + } else if ("method" in scope && "path" in scope) { + const { method, path } = scope + return { method, path } + } else { + assertUnreachable(scope) + } } export function getAccessTokenScope(token: IAccessToken | null, schema: Schema): IScope | null { - return token?.scopes.find((s) => s.schema.path === schema.path && s.schema.method === schema.method) ?? null + return ( + token?.scopes.find((scope) => { + const { method, path } = getMethodAndPath(scope) + return path === schema.path && method === schema.method + }) ?? null + ) } export function parseAccessToken( @@ -117,10 +161,11 @@ export function parseAccessToken( } function scopesToAudiences(scopes: ReadonlyArray>) { - return scopes.map((scope) => - getAudience({ - method: scope.schema.method, - path: scope.schema.path, + return scopes.map((scope) => { + const { method, path } = getMethodAndPath(scope) + return getAudience({ + method, + path, options: scope.options === "all" ? {} @@ -130,5 +175,5 @@ function scopesToAudiences(scopes: ReadonlyAr }, skipParamsReplacement: scope.options === "all", }) - ) + }) } diff --git a/server/src/security/authenticationService.ts b/server/src/security/authenticationService.ts index 0d0925ca28..304df6ae7d 100644 --- a/server/src/security/authenticationService.ts +++ b/server/src/security/authenticationService.ts @@ -2,7 +2,7 @@ import { captureException } from "@sentry/node" import Boom from "boom" import { FastifyRequest } from "fastify" import jwt, { JwtPayload } from "jsonwebtoken" -import { ICredential } from "shared" +import { ICredential, assertUnreachable } from "shared" import { PathParam, QueryString } from "shared/helpers/generateUri" import { IUserRecruteur } from "shared/models/usersRecruteur.model" import { ISecuredRouteSchema, WithSecurityScheme } from "shared/routes/common.routes" @@ -100,10 +100,6 @@ async function authAccessToken(req: FastifyReques return token ? { type: "IAccessToken", value: token } : null } -function assertUnreachable(_x: never): never { - throw new Error("Didn't expect to get here") -} - export async function authenticationMiddleware(schema: S, req: FastifyRequest) { if (!schema.securityScheme) { throw Boom.internal("Missing securityScheme") diff --git a/server/src/security/authorisationService.ts b/server/src/security/authorisationService.ts index 1b77f400bd..976ff8f2a7 100644 --- a/server/src/security/authorisationService.ts +++ b/server/src/security/authorisationService.ts @@ -3,6 +3,7 @@ import { FastifyRequest } from "fastify" import { IApplication, IJob, IRecruiter, IUserRecruteur } from "shared/models" import { IRouteSchema, WithSecurityScheme } from "shared/routes/common.routes" import { AccessPermission, AccessResourcePath, AdminRole, CfaRole, OpcoRole, RecruiterRole, Role } from "shared/security/permissions" +import { assertUnreachable } from "shared/utils" import { Primitive } from "type-fest" import { Recruiter, UserRecruteur, Application } from "@/common/model" @@ -24,10 +25,6 @@ type IRequest = Pick // TODO: job.delegations // TODO: Unit schema access path properly defined (exists in Zod schema) -function assertUnreachable(_x: never): never { - throw new Error("Didn't expect to get here") -} - function getAccessResourcePathValue(path: AccessResourcePath, req: IRequest): any { const obj = req[path.type] as Record return obj[path.key] diff --git a/server/tests/unit/security/authorisationService.test.ts b/server/tests/unit/security/authorisationService.test.ts index d0bc0275d9..ee1d4ef970 100644 --- a/server/tests/unit/security/authorisationService.test.ts +++ b/server/tests/unit/security/authorisationService.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it } from "vitest" import { Fixture, Generator } from "zod-fixture" import { Application, Credential, Recruiter, UserRecruteur } from "@/common/model" -import { IAccessToken } from "@/security/accessTokenService" +import { IAccessToken, generateScope } from "@/security/accessTokenService" import { authorizationnMiddleware } from "@/security/authorisationService" import { useMongo } from "@tests/utils/mongo.utils" @@ -1629,7 +1629,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1641,8 +1641,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -1656,7 +1656,7 @@ describe("authorisationService", () => { resources: { recruiter: [recruteurO1E1R1._id.toString()], }, - }, + }), ], }, } @@ -1692,7 +1692,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1704,8 +1704,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -1719,7 +1719,7 @@ describe("authorisationService", () => { resources: { recruiter: [recruteurO1E1R1._id.toString()], }, - }, + }), ], }, } @@ -1773,7 +1773,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1785,8 +1785,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -1800,7 +1800,7 @@ describe("authorisationService", () => { resources: { job: recruteurO1E1R1.jobs.map((j) => j._id.toString()), }, - }, + }), ], }, } @@ -1836,7 +1836,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1848,8 +1848,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -1863,7 +1863,7 @@ describe("authorisationService", () => { resources: { job: recruteurO1E1R1.jobs.map((j) => j._id.toString()), }, - }, + }), ], }, } @@ -1917,7 +1917,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1929,8 +1929,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -1944,7 +1944,7 @@ describe("authorisationService", () => { resources: { application: [applicationO1E1R1J1A1._id.toString()], }, - }, + }), ], }, } @@ -1980,7 +1980,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -1992,8 +1992,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -2007,7 +2007,7 @@ describe("authorisationService", () => { resources: { application: [applicationO1E1R1J1A1._id.toString()], }, - }, + }), ], }, } @@ -2061,7 +2061,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -2073,8 +2073,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -2088,7 +2088,7 @@ describe("authorisationService", () => { resources: { user: [opcoUserO1U1._id.toString()], }, - }, + }), ], }, } @@ -2124,7 +2124,7 @@ describe("authorisationService", () => { value: { identity: { type: "cfa", email: "mail@mail.com", siret: "55327987900672" }, scopes: [ - { + generateScope({ schema: { method: "post", path: "/path/:id", @@ -2136,8 +2136,8 @@ describe("authorisationService", () => { }, options: "all", resources: {}, - }, - { + }), + generateScope({ schema: { method: "get", path: "/path/:id", @@ -2151,7 +2151,7 @@ describe("authorisationService", () => { resources: { user: [opcoUserO1U1._id.toString()], }, - }, + }), ], }, } diff --git a/shared/index.ts b/shared/index.ts index f84fa40b5e..f3665765f6 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -1,2 +1,3 @@ export * from "./models" export * from "./routes" +export * from "./utils" diff --git a/shared/utils/assertUnreachable.ts b/shared/utils/assertUnreachable.ts new file mode 100644 index 0000000000..2b57116fbc --- /dev/null +++ b/shared/utils/assertUnreachable.ts @@ -0,0 +1,3 @@ +export function assertUnreachable(_x: never): never { + throw new Error("Didn't expect to get here") +} diff --git a/shared/utils/index.ts b/shared/utils/index.ts new file mode 100644 index 0000000000..9bfe69277e --- /dev/null +++ b/shared/utils/index.ts @@ -0,0 +1 @@ +export * from "./assertUnreachable"