fix: avoid teams sso token key collisions

This commit is contained in:
Tak Hoffman
2026-04-10 19:28:16 -05:00
parent 1fb2e18f47
commit 32ad88da98
2 changed files with 115 additions and 2 deletions

View File

@@ -0,0 +1,72 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createMSTeamsSsoTokenStoreFs } from "./sso-token-store.js";
describe("msteams sso token store (fs)", () => {
it("keeps distinct tokens when connectionName and userId contain the legacy delimiter", async () => {
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-sso-"));
const storePath = path.join(stateDir, "msteams-sso-tokens.json");
const store = createMSTeamsSsoTokenStoreFs({ storePath });
const first = {
connectionName: "conn::alpha",
userId: "user",
token: "token-a",
updatedAt: "2026-04-10T00:00:00.000Z",
} as const;
const second = {
connectionName: "conn",
userId: "alpha::user",
token: "token-b",
updatedAt: "2026-04-10T00:00:01.000Z",
} as const;
await store.save(first);
await store.save(second);
expect(await store.get(first)).toEqual(first);
expect(await store.get(second)).toEqual(second);
const raw = JSON.parse(await fs.readFile(storePath, "utf8")) as {
tokens: Record<string, unknown>;
};
expect(Object.keys(raw.tokens)).toHaveLength(2);
});
it("loads legacy flat-key files by rebuilding keys from stored token payloads", async () => {
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-sso-legacy-"));
const storePath = path.join(stateDir, "msteams-sso-tokens.json");
await fs.writeFile(
storePath,
`${JSON.stringify(
{
version: 1,
tokens: {
"legacy::wrong-key": {
connectionName: "conn",
userId: "user-1",
token: "token-1",
updatedAt: "2026-04-10T00:00:00.000Z",
},
},
},
null,
2,
)}\n`,
"utf8",
);
const store = createMSTeamsSsoTokenStoreFs({ storePath });
expect(
await store.get({
connectionName: "conn",
userId: "user-1",
}),
).toMatchObject({
token: "token-1",
updatedAt: "2026-04-10T00:00:00.000Z",
});
});
});

View File

@@ -40,9 +40,39 @@ type SsoStoreData = {
};
const STORE_FILENAME = "msteams-sso-tokens.json";
const STORE_KEY_VERSION_PREFIX = "v2:";
function makeKey(connectionName: string, userId: string): string {
return `${connectionName}::${userId}`;
return `${STORE_KEY_VERSION_PREFIX}${Buffer.from(
JSON.stringify([connectionName, userId]),
"utf8",
).toString("base64url")}`;
}
function normalizeStoredToken(value: unknown): MSTeamsSsoStoredToken | null {
if (!value || typeof value !== "object") {
return null;
}
const token = value as Partial<MSTeamsSsoStoredToken>;
if (
typeof token.connectionName !== "string" ||
!token.connectionName ||
typeof token.userId !== "string" ||
!token.userId ||
typeof token.token !== "string" ||
!token.token ||
typeof token.updatedAt !== "string" ||
!token.updatedAt
) {
return null;
}
return {
connectionName: token.connectionName,
userId: token.userId,
token: token.token,
...(typeof token.expiresAt === "string" ? { expiresAt: token.expiresAt } : {}),
updatedAt: token.updatedAt,
};
}
function isSsoStoreData(value: unknown): value is SsoStoreData {
@@ -74,7 +104,18 @@ export function createMSTeamsSsoTokenStoreFs(params?: {
if (!isSsoStoreData(value)) {
return { version: 1, tokens: {} };
}
return value;
const tokens: Record<string, MSTeamsSsoStoredToken> = {};
for (const stored of Object.values(value.tokens)) {
const normalized = normalizeStoredToken(stored);
if (!normalized) {
continue;
}
tokens[makeKey(normalized.connectionName, normalized.userId)] = normalized;
}
return {
version: 1,
tokens,
};
};
return {