diff --git a/server/_core/oauth.ts b/server/_core/oauth.ts index 67e9af2..1745f95 100644 --- a/server/_core/oauth.ts +++ b/server/_core/oauth.ts @@ -1,6 +1,7 @@ import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; import type { Express, Request, Response } from "express"; import * as db from "../db"; +import { normalizeLoginMethod } from "../authMethod"; import { getSessionCookieOptions } from "./cookies"; import { sdk } from "./sdk"; @@ -32,7 +33,7 @@ export function registerOAuthRoutes(app: Express) { openId: userInfo.openId, name: userInfo.name || null, email: userInfo.email ?? "unknown@example.com", - loginMethod: (userInfo.loginMethod ?? userInfo.platform ?? "manus") as "manus" | "local" | "azure-ad", + loginMethod: normalizeLoginMethod(userInfo.loginMethod ?? userInfo.platform), lastSignedIn: new Date(), }); diff --git a/server/_core/sdk.ts b/server/_core/sdk.ts index eae78f6..f3f2a3a 100644 --- a/server/_core/sdk.ts +++ b/server/_core/sdk.ts @@ -2,6 +2,7 @@ import { AXIOS_TIMEOUT_MS, COOKIE_NAME, ONE_YEAR_MS } from "@shared/const"; import { ForbiddenError } from "@shared/_core/errors"; import axios, { type AxiosInstance } from "axios"; import { parse as parseCookieHeader } from "cookie"; +import { normalizeLoginMethod } from "../authMethod"; import type { Request } from "express"; import { SignJWT, jwtVerify } from "jose"; import type { User } from "../../drizzle/schema"; @@ -296,7 +297,7 @@ class SDKServer { openId: userInfo.openId, name: userInfo.name || null, email: userInfo.email ?? "unknown@example.com", - loginMethod: (userInfo.loginMethod ?? userInfo.platform ?? "manus") as "manus" | "local" | "azure-ad", + loginMethod: normalizeLoginMethod(userInfo.loginMethod ?? userInfo.platform), lastSignedIn: signedInAt, }); user = await db.getUserByOpenId(userInfo.openId); diff --git a/server/authMethod.test.ts b/server/authMethod.test.ts new file mode 100644 index 0000000..ef3d5ff --- /dev/null +++ b/server/authMethod.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { normalizeLoginMethod } from "./authMethod"; + +describe("normalizeLoginMethod", () => { + it("conserve les valeurs de l’enum DB", () => { + expect(normalizeLoginMethod("local")).toBe("local"); + expect(normalizeLoginMethod("azure-ad")).toBe("azure-ad"); + }); + + it("convertit les identifiants de fournisseur Microsoft", () => { + expect(normalizeLoginMethod("Microsoft OAuth")).toBe("azure-ad"); + expect(normalizeLoginMethod("AZURE_ENTRA")).toBe("azure-ad"); + }); + + it("utilise Manus pour toute valeur inconnue au lieu d’échouer en base", () => { + expect(normalizeLoginMethod("platform-v2")).toBe("manus"); + expect(normalizeLoginMethod(undefined)).toBe("manus"); + }); +}); diff --git a/server/authMethod.ts b/server/authMethod.ts new file mode 100644 index 0000000..9cc7a45 --- /dev/null +++ b/server/authMethod.ts @@ -0,0 +1,23 @@ +/** Values accepted by the `users.loginMethod` database enum. */ +export type LoginMethod = "manus" | "local" | "azure-ad"; + +const LOGIN_METHODS = new Set(["manus", "local", "azure-ad"]); + +/** + * Maps provider-specific identifiers to the limited database enum. + * OAuth identity payloads are external input and must never be persisted verbatim. + */ +export function normalizeLoginMethod(value: unknown): LoginMethod { + if (typeof value !== "string") return "manus"; + + const normalized = value.trim().toLowerCase(); + if (LOGIN_METHODS.has(normalized as LoginMethod)) { + return normalized as LoginMethod; + } + + if (normalized.includes("azure") || normalized.includes("microsoft")) { + return "azure-ad"; + } + + return "manus"; +} diff --git a/todo.md b/todo.md index 1e91fed..37593e2 100644 --- a/todo.md +++ b/todo.md @@ -701,3 +701,4 @@ - [x] Renforcer les contrôles d’accès et la gestion d’erreur des endpoints critiques - [x] Documenter les modules métier, les invariants et les décisions techniques critiques - [x] Ajouter des tests de non-régression ciblés et vérifier build, types et tests +- [x] Normaliser les valeurs OAuth de loginMethod avant écriture en base