diff --git a/src/domain/auth/types/auth-user.ts b/src/domain/auth/types/auth-user.ts index f16f38b0..6a567b16 100644 --- a/src/domain/auth/types/auth-user.ts +++ b/src/domain/auth/types/auth-user.ts @@ -7,7 +7,7 @@ export interface AuthUser { fullname: string; id: string | number; name?: string; - /** JWT / login `org_scope`. Only `all` may see org-wide account lists. */ + /** Login payload field. Not trusted for org-wide UI gates; use the JWT claim. */ orgScope?: string; accountId?: string | number; } diff --git a/src/lib/auth/org-scope.ts b/src/lib/auth/org-scope.ts index 95afebd1..681a9e37 100644 --- a/src/lib/auth/org-scope.ts +++ b/src/lib/auth/org-scope.ts @@ -33,13 +33,12 @@ function readStringClaim(source: Record | null, keys: string[]) return ""; } -/** Org-wide only when the authenticated scope is explicitly `all`. Fail-closed otherwise. */ +/** Org-wide only when the JWT claim is explicitly `all`. Fail-closed otherwise. */ export function isOrgWideScope(user: AuthUser | null | undefined): boolean { - if (!user) { + if (!user || typeof user.token !== "string") { return false; } - const claims = typeof user.token === "string" ? readJwtClaims(user.token) : null; - const fromJwt = readStringClaim(claims, ["org_scope", "orgScope", "OrgScope"]); - const fromUser = typeof user.orgScope === "string" ? user.orgScope.trim() : ""; - return (fromJwt || fromUser).toLowerCase() === ORG_SCOPE_ALL; + const claims = readJwtClaims(user.token); + const scope = readStringClaim(claims, ["org_scope", "orgScope", "OrgScope"]); + return scope.toLowerCase() === ORG_SCOPE_ALL; } diff --git a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx index c6ccb517..9acafb21 100644 --- a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx @@ -50,7 +50,7 @@ function encodeJwt(payload: Record): string { return `eyJhbGciOiJub25lIn0.${b64}.sig`; } -function authUser(token: string): AuthUser { +function authUser(token: string, orgScope?: string): AuthUser { return { token, expiration: new Date(Date.now() + 60_000).toISOString(), @@ -59,6 +59,7 @@ function authUser(token: string): AuthUser { phoneNumber: "", fullname: "Test User", id: 1, + orgScope, }; } @@ -132,4 +133,12 @@ describe("WizardStepLocationAccountSelect", () => { expect(screen.queryByText(FOREIGN_ACCOUNT)).not.toBeInTheDocument(); expect(screen.queryByText("Amazon")).not.toBeInTheDocument(); }); + + it("keeps the list disabled when a scoped token has no org_scope and storage says all", () => { + renderSelect(authUser(encodeJwt({ account_id: 7 }), "all")); + + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }, false); + expect(screen.queryByRole("button", { name: /select account/i })).not.toBeInTheDocument(); + expect(screen.queryByText(FOREIGN_ACCOUNT)).not.toBeInTheDocument(); + }); }); diff --git a/src/test/lib/auth/org-scope.test.ts b/src/test/lib/auth/org-scope.test.ts index 0782fd13..51f074c7 100644 --- a/src/test/lib/auth/org-scope.test.ts +++ b/src/test/lib/auth/org-scope.test.ts @@ -36,16 +36,19 @@ describe("isOrgWideScope", () => { expect(isOrgWideScope(user(encodeJwt({ org_scope: "account", account_id: 7 })))).toBe(false); }); - it("uses the login orgScope when the token is not a JWT", () => { - expect(isOrgWideScope(user("session-token", "all"))).toBe(true); - expect(isOrgWideScope(user("session-token", "account"))).toBe(false); + it("ignores a stored orgScope when the token is not a JWT", () => { + expect(isOrgWideScope(user("session-token", "all"))).toBe(false); }); - it("prefers a scoped JWT over a stale org-wide login field", () => { + it("ignores a stale stored orgScope when the JWT has no org_scope", () => { + expect(isOrgWideScope(user(encodeJwt({ account_id: 7 }), "all"))).toBe(false); + }); + + it("ignores a stale stored orgScope when the JWT is scoped", () => { expect(isOrgWideScope(user(encodeJwt({ org_scope: "account" }), "all"))).toBe(false); }); - it("is false when neither the JWT nor the user declares all", () => { + it("is false when the token has no org_scope claim", () => { expect(isOrgWideScope(user("wo-visual-token"))).toBe(false); }); });