Skip to content

Commit efffccc

Browse files
committed
fix(msteams): preserve cloud token URL for bounded SSO routes
1 parent 3cc6e8c commit efffccc

3 files changed

Lines changed: 125 additions & 14 deletions

File tree

extensions/msteams/src/monitor.lifecycle.test.ts

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,31 @@ const isSigninInvokeAuthorized = vi.hoisted(() => vi.fn(async () => true));
135135
const isCardActionInvokeAuthorized = vi.hoisted(() => vi.fn(async () => true));
136136
const runMSTeamsFileConsentInvokeHandler = vi.hoisted(() => vi.fn(async () => {}));
137137
const loadMSTeamsSdkWithAuth = vi.hoisted(() =>
138-
vi.fn(async (_creds?: unknown, _options?: unknown) => ({
139-
app: {
140-
on: vi.fn(),
141-
event: vi.fn(),
142-
onTokenExchange: vi.fn(async () => ({ status: 200 })),
143-
onVerifyState: vi.fn(async () => ({ status: 200 })),
144-
initialize: vi.fn(async () => {}),
145-
tokenManager: {
146-
getBotToken: vi.fn(async () => ({ toString: (): string => "bot-token" })),
147-
getGraphToken: vi.fn(async () => ({ toString: (): string => "graph-token" })),
138+
vi.fn(async (_creds?: unknown, options?: unknown) => {
139+
const cloud = (options as { cloud?: string } | undefined)?.cloud;
140+
const tokenServiceUrl =
141+
cloud === "USGov"
142+
? "https://tokengcch.botframework.azure.us"
143+
: cloud === "USGovDoD"
144+
? "https://apiDoD.botframework.azure.us"
145+
: cloud === "China"
146+
? "https://token.botframework.azure.cn"
147+
: "https://token.botframework.com";
148+
return {
149+
app: {
150+
on: vi.fn(),
151+
event: vi.fn(),
152+
onTokenExchange: vi.fn(async () => ({ status: 200 })),
153+
onVerifyState: vi.fn(async () => ({ status: 200 })),
154+
initialize: vi.fn(async () => {}),
155+
cloud: { tokenServiceUrl },
156+
tokenManager: {
157+
getBotToken: vi.fn(async () => ({ toString: (): string => "bot-token" })),
158+
getGraphToken: vi.fn(async () => ({ toString: (): string => "graph-token" })),
159+
},
148160
},
149-
},
150-
})),
161+
};
162+
}),
151163
);
152164

153165
const ssoTokenStore = vi.hoisted(() => ({
@@ -291,6 +303,10 @@ function createJsonResponse(body: unknown, status = 200): globalThis.Response {
291303
});
292304
}
293305

306+
function getFetchInputUrl(input: string | URL | Request): string {
307+
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
308+
}
309+
294310
function createOversizedResponse(): globalThis.Response {
295311
const chunk = new Uint8Array(64 * 1024).fill(0x41);
296312
let sent = 0;
@@ -508,8 +524,7 @@ describe("monitorMSTeamsProvider lifecycle", () => {
508524
});
509525
const fetchMock = vi.fn(
510526
async (input: string | URL | Request, init?: RequestInit): Promise<globalThis.Response> => {
511-
const url =
512-
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
527+
const url = getFetchInputUrl(input);
513528
if (url.includes("/api/usertoken/exchange")) {
514529
expect(init?.method).toBe("POST");
515530
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer mock-token");
@@ -632,6 +647,98 @@ describe("monitorMSTeamsProvider lifecycle", () => {
632647
await task;
633648
});
634649

650+
it("preserves the SDK cloud User Token service URL for registered SSO routes", async () => {
651+
const abort = new AbortController();
652+
const cfg = createConfig(0);
653+
updateMSTeamsConfig(cfg, {
654+
cloud: "USGov",
655+
serviceUrl: "https://smba.infra.gov.teams.microsoft.us/teams",
656+
sso: { enabled: true, connectionName: "graph" },
657+
});
658+
const requestedUrls: string[] = [];
659+
const fetchMock = vi.fn(async (input: string | URL | Request): Promise<globalThis.Response> => {
660+
const url = getFetchInputUrl(input);
661+
requestedUrls.push(url);
662+
if (url.includes("/api/usertoken/exchange")) {
663+
return createJsonResponse({
664+
channelId: "msteams",
665+
connectionName: "graph",
666+
token: "gov-token-exchange",
667+
});
668+
}
669+
if (url.includes("/api/usertoken/GetToken")) {
670+
return createJsonResponse({
671+
channelId: "msteams",
672+
connectionName: "graph",
673+
token: "gov-token-state",
674+
});
675+
}
676+
throw new Error(`unexpected fetch ${url}`);
677+
});
678+
vi.stubGlobal("fetch", fetchMock);
679+
680+
const task = monitorMSTeamsProvider({
681+
cfg,
682+
runtime: createRuntime(),
683+
abortSignal: abort.signal,
684+
conversationStore: createStores().conversationStore,
685+
pollStore: createStores().pollStore,
686+
});
687+
688+
await vi.waitFor(() => {
689+
expect(registerMSTeamsHandlers).toHaveBeenCalled();
690+
});
691+
692+
expect(loadMSTeamsSdkWithAuth.mock.calls[0]?.[1]).toMatchObject({
693+
cloud: "USGov",
694+
serviceUrl: "https://smba.infra.gov.teams.microsoft.us/teams",
695+
oauthDefaultConnectionName: "graph",
696+
});
697+
698+
const tokenExchangeHandler = await getRegisteredMSTeamsRoute("signin.token-exchange");
699+
await expect(
700+
tokenExchangeHandler({
701+
activity: {
702+
type: "invoke",
703+
name: "signin/tokenExchange",
704+
channelId: "msteams",
705+
from: { id: "29:user" },
706+
value: {
707+
id: "exchange-gov",
708+
connectionName: "graph",
709+
token: "exchangeable-token",
710+
},
711+
},
712+
}),
713+
).resolves.toEqual({ status: 200 });
714+
715+
const verifyStateHandler = await getRegisteredMSTeamsRoute("signin.verify-state");
716+
await expect(
717+
verifyStateHandler({
718+
activity: {
719+
type: "invoke",
720+
name: "signin/verifyState",
721+
channelId: "msteams",
722+
from: { id: "29:user" },
723+
value: { state: "654321" },
724+
},
725+
}),
726+
).resolves.toEqual({ status: 200 });
727+
728+
expect(requestedUrls).toHaveLength(2);
729+
expect(requestedUrls).toEqual([
730+
expect.stringMatching(
731+
/^https:\/\/tokengcch\.botframework\.azure\.us\/api\/usertoken\/exchange\?/,
732+
),
733+
expect.stringMatching(
734+
/^https:\/\/tokengcch\.botframework\.azure\.us\/api\/usertoken\/GetToken\?/,
735+
),
736+
]);
737+
738+
abort.abort();
739+
await task;
740+
});
741+
635742
it("rejects oversized User Token responses through registered SSO routes", async () => {
636743
const abort = new AbortController();
637744
const cfg = createConfig(0);

extensions/msteams/src/monitor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,16 @@ export async function monitorMSTeamsProvider(
406406
// Bot Framework token exchange or persist anything).
407407
let ssoDeps: MSTeamsSsoDeps | undefined;
408408
if (msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName) {
409+
const userTokenBaseUrl = app.cloud?.tokenServiceUrl?.trim();
409410
ssoDeps = {
410411
tokenProvider,
411412
tokenStore: createMSTeamsSsoTokenStoreFs(),
412413
connectionName: msteamsCfg.sso.connectionName,
414+
...(userTokenBaseUrl ? { userTokenBaseUrl } : {}),
413415
};
414416
log.debug?.("msteams sso enabled", {
415417
connectionName: msteamsCfg.sso.connectionName,
418+
...(userTokenBaseUrl ? { userTokenBaseUrl } : {}),
416419
});
417420
}
418421

extensions/msteams/src/sdk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export type MSTeamsApp = {
162162
};
163163
cloud?: {
164164
graphScope?: string;
165+
tokenServiceUrl?: string;
165166
};
166167
api: {
167168
serviceUrl?: string;

0 commit comments

Comments
 (0)