From be004425fa3ee54b8063042efe4876aec9dfdc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 14 Aug 2026 22:50:48 +0200 Subject: [PATCH] fix: Add username to the tokenStoreKey so the saved token is invalidated when the user changes in the environment (#426) Co-authored-by: Gregory Schier --- plugins/auth-oauth2/src/grants/password.ts | 1 + plugins/auth-oauth2/src/index.ts | 9 ++++ plugins/auth-oauth2/src/store.ts | 2 + plugins/auth-oauth2/tests/store.test.ts | 56 ++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 plugins/auth-oauth2/tests/store.test.ts diff --git a/plugins/auth-oauth2/src/grants/password.ts b/plugins/auth-oauth2/src/grants/password.ts index 02f3daa2..dbf55b31 100644 --- a/plugins/auth-oauth2/src/grants/password.ts +++ b/plugins/auth-oauth2/src/grants/password.ts @@ -32,6 +32,7 @@ export async function getPassword( clientId, accessTokenUrl, authorizationUrl: null, + username, }; const token = await getOrRefreshAccessToken(ctx, tokenArgs, { accessTokenUrl, diff --git a/plugins/auth-oauth2/src/index.ts b/plugins/auth-oauth2/src/index.ts index edf6821b..02113a18 100644 --- a/plugins/auth-oauth2/src/index.ts +++ b/plugins/auth-oauth2/src/index.ts @@ -103,6 +103,7 @@ export const plugin: PluginDefinition = { authorizationUrl: stringArg(values, "authorizationUrl"), accessTokenUrl: stringArg(values, "accessTokenUrl"), clientId: stringArg(values, "clientId"), + username: usernameArg(values), }; const token = await getToken(ctx, tokenArgs); if (token == null) { @@ -128,6 +129,7 @@ export const plugin: PluginDefinition = { authorizationUrl: stringArg(values, "authorizationUrl"), accessTokenUrl: stringArg(values, "accessTokenUrl"), clientId: stringArg(values, "clientId"), + username: usernameArg(values), }; if (await deleteToken(ctx, tokenArgs)) { await ctx.toast.show({ @@ -478,6 +480,7 @@ export const plugin: PluginDefinition = { authorizationUrl: stringArg(values, "authorizationUrl"), accessTokenUrl: stringArg(values, "accessTokenUrl"), clientId: stringArg(values, "clientId"), + username: usernameArg(values), }; const token = await getToken(ctx, tokenArgs); if (token == null) { @@ -609,6 +612,12 @@ function stringArgOrNull( return `${arg}`; } +/** Only the password grant stores its token under a username, so the others must not key by one */ +function usernameArg(values: Record): string | null { + const grantType = String(values.grantType ?? defaultGrantType); + return grantType === "password" ? stringArgOrNull(values, "username") : null; +} + function stringArg(values: Record, name: string): string { const arg = stringArgOrNull(values, name); if (!arg) return ""; diff --git a/plugins/auth-oauth2/src/store.ts b/plugins/auth-oauth2/src/store.ts index 49752253..6f55c746 100644 --- a/plugins/auth-oauth2/src/store.ts +++ b/plugins/auth-oauth2/src/store.ts @@ -47,6 +47,7 @@ export interface TokenStoreArgs { clientId: string; accessTokenUrl: string | null; authorizationUrl: string | null; + username?: string | null; } /** @@ -59,6 +60,7 @@ function tokenStoreKey(args: TokenStoreArgs) { if (args.clientId) hash.update(args.clientId.trim()); if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, "")); if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, "")); + if (args.username) hash.update(args.username); const key = hash.digest("hex"); return ["token", key].join("::"); } diff --git a/plugins/auth-oauth2/tests/store.test.ts b/plugins/auth-oauth2/tests/store.test.ts new file mode 100644 index 00000000..65c04234 --- /dev/null +++ b/plugins/auth-oauth2/tests/store.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "vite-plus/test"; +import type { TokenStoreArgs } from "../src/store"; +import { getToken, storeToken } from "../src/store"; + +function createMockContext() { + const values = new Map(); + + return { + store: { + async set(key: string, value: T) { + values.set(key, value); + }, + async get(key: string) { + return values.get(key) as T | undefined; + }, + }, + } as never; +} + +describe("token store", () => { + test("separates password grant tokens when the username changes", async () => { + const ctx = createMockContext(); + + const aliceArgs: TokenStoreArgs = { + contextId: "request-1", + clientId: "client-123", + accessTokenUrl: "https://auth.example.com/token", + authorizationUrl: null, + username: "alice@example.com", + }; + const bobArgs: TokenStoreArgs = { ...aliceArgs, username: "bob@example.com" }; + + await storeToken(ctx, aliceArgs, { access_token: "alice-token" }); + + expect((await getToken(ctx, aliceArgs))?.response.access_token).toBe("alice-token"); + expect(await getToken(ctx, bobArgs)).toBeUndefined(); + }); + + test("keeps the same key for grants without a username", async () => { + const ctx = createMockContext(); + + const args: TokenStoreArgs = { + contextId: "request-1", + clientId: "client-123", + accessTokenUrl: "https://auth.example.com/token", + authorizationUrl: null, + }; + + await storeToken(ctx, args, { access_token: "cc-token" }); + + expect((await getToken(ctx, { ...args, username: null }))?.response.access_token).toBe( + "cc-token", + ); + expect((await getToken(ctx, { ...args, username: "" }))?.response.access_token).toBe("cc-token"); + }); +});