Skip to content

Commit

Permalink
fix: reduction de la longueur du JWT token (#850)
Browse files Browse the repository at this point in the history
* fix: reduction de la longueur du JWT token

* fix: ajout de monitoring sentry

* fix: ajout de commentaire pour retirer OldIScope
  • Loading branch information
remy-auricoste authored Nov 23, 2023
1 parent cb95f0c commit 5eef1d2
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 55 deletions.
71 changes: 58 additions & 13 deletions server/src/security/accessTokenService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRouteSchema, "method" | "path" | "params" | "querystring"> & WithSecurityScheme

export type IScope<Schema extends SchemaWithSecurity> = {
// TODO à retirer à partir du 01/02/2024
type OldIScope<Schema extends SchemaWithSecurity> = {
schema: Schema
options:
| "all"
Expand All @@ -23,8 +30,25 @@ export type IScope<Schema extends SchemaWithSecurity> = {
}
}

export const generateScope = <Schema extends SchemaWithSecurity>(scope: IScope<Schema>): IScope<Schema> => {
return scope
type NewIScope<Schema extends SchemaWithSecurity> = {
method: Schema["method"]
path: Schema["path"]
options:
| "all"
| {
params: Schema["params"] extends AnyZodObject ? Jsonify<z.input<Schema["params"]>> : undefined
querystring: Schema["querystring"] extends AnyZodObject ? Jsonify<z.input<Schema["querystring"]>> : undefined
}
resources: {
[key in keyof Schema["securityScheme"]["ressources"]]: ReadonlyArray<string>
}
}

type IScope<Schema extends SchemaWithSecurity> = NewIScope<Schema> | OldIScope<Schema>

export const generateScope = <Schema extends SchemaWithSecurity>(scope: Omit<NewIScope<Schema>, "method" | "path"> & { schema: Schema }): NewIScope<Schema> => {
const { schema, options, resources } = scope
return { options, resources, path: schema.path, method: schema.method }
}

export type IAccessToken<Schema extends SchemaWithSecurity = SchemaWithSecurity> = {
Expand Down Expand Up @@ -58,25 +82,45 @@ function getAudience({

export function generateAccessToken(
user: IUserRecruteur | IAccessToken["identity"],
scopes: ReadonlyArray<IScope<ISecuredRouteSchema>>,
scopes: ReadonlyArray<NewIScope<ISecuredRouteSchema>>,
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<ISecuredRouteSchema> = {
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<Schema extends SchemaWithSecurity>(scope: IScope<Schema>) {
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<Schema extends SchemaWithSecurity>(token: IAccessToken<Schema> | null, schema: Schema): IScope<Schema> | 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<Schema extends SchemaWithSecurity>(
Expand Down Expand Up @@ -117,10 +161,11 @@ export function parseAccessToken<Schema extends SchemaWithSecurity>(
}

function scopesToAudiences<Schema extends SchemaWithSecurity>(scopes: ReadonlyArray<IScope<Schema>>) {
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"
? {}
Expand All @@ -130,5 +175,5 @@ function scopesToAudiences<Schema extends SchemaWithSecurity>(scopes: ReadonlyAr
},
skipParamsReplacement: scope.options === "all",
})
)
})
}
6 changes: 1 addition & 5 deletions server/src/security/authenticationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -100,10 +100,6 @@ async function authAccessToken<S extends ISecuredRouteSchema>(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<S extends ISecuredRouteSchema>(schema: S, req: FastifyRequest) {
if (!schema.securityScheme) {
throw Boom.internal("Missing securityScheme")
Expand Down
5 changes: 1 addition & 4 deletions server/src/security/authorisationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,10 +25,6 @@ type IRequest = Pick<FastifyRequest, "user" | "params" | "query">
// 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<string, Primitive>
return obj[path.key]
Expand Down
Loading

0 comments on commit 5eef1d2

Please sign in to comment.