mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-22 19:04:56 +00:00
* msteams: add reaction support (inbound handlers + outbound Graph API) * msteams: address PR #51646 review feedback * msteams: remove react from advertised actions (requires Delegated auth) * msteams: address PR #51646 remaining review feedback (dmPolicy, groupPolicy, reactions auth) - Fix 1: DM reaction authorization now uses resolveDmGroupAccessWithLists to enforce dmPolicy modes (open/disabled/allowlist/pairing), matching the message handler. - Fix 2: Group policy in reaction handler already uses resolveDefaultGroupPolicy for global defaults; moved declaration earlier to share with DM path. - Fix 3: Restore read-only "reactions" (list) action with listReactionsMSTeams, which uses GET and works with Application auth. Keep "react" (write) gated behind delegated-auth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * msteams: add shared Graph pagination helper (fetchAllGraphPages) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * msteams: add OAuth2 delegated auth flow (PKCE + authorization code) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * msteams: integrate delegated auth (config, token storage, react enablement) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * msteams: fix critical bugs found in architect review - Fix fetchGraphJson→postGraphJson for setReaction/unsetReaction (was sending GET instead of POST) - Fix CSRF bypass in OAuth parseCallbackInput (missing state no longer falls back silently) - Remove stale delegated-auth warning logs (delegated auth is now implemented) - Add CSRF test case for parseCallbackInput Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * msteams: fix 6 PR #51646 review blockers (PKCE/state separation, CSRF, imports, routing, delegated auth bootstrap) * msteams: fix channel.runtime.ts duplicate imports + graph.ts test mock compat * msteams: fix lint/boundary blockers revealed by CI after rebase - token.ts/graph.test.ts: add curly braces around single-statement ifs (eslint/curly). - oauth.flow.ts: rename unused parseCallbackInput param to _expectedState. - reaction-handler.test.ts: rename unused buildDeps param to _runtime. - send.reactions.ts: drop unnecessary non-null assertions on tuple entries. - setup-surface.ts: drop empty-object spread fallback flagged by unicorn/no-useless-fallback-in-spread. - graph.ts: move GraphPagedResponse/PaginatedResult type defs below requestGraph so the raw fetch() stays on line 47 to match the existing no-raw-channel-fetch allowlist entry. - oauth.token.ts: route the Azure AD token exchange and refresh calls through fetchWithSsrFGuard (matches the pattern in sdk.ts), removing the unguarded raw fetch() callsites flagged by lint:tmp:no-raw-channel-fetch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(msteams): restore absolute Graph pagination helper * fix(msteams): satisfy reaction handler lint --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
import {
|
|
buildMSTeamsAuthUrl,
|
|
generateOAuthState,
|
|
generatePkce,
|
|
parseCallbackInput,
|
|
shouldUseManualOAuthFlow,
|
|
waitForLocalCallback,
|
|
} from "./oauth.flow.js";
|
|
import {
|
|
MSTEAMS_DEFAULT_DELEGATED_SCOPES,
|
|
MSTEAMS_OAUTH_CALLBACK_PORT,
|
|
type MSTeamsDelegatedOAuthContext,
|
|
type MSTeamsDelegatedTokens,
|
|
} from "./oauth.shared.js";
|
|
import { exchangeMSTeamsCodeForTokens } from "./oauth.token.js";
|
|
|
|
export type { MSTeamsDelegatedOAuthContext, MSTeamsDelegatedTokens };
|
|
|
|
export async function loginMSTeamsDelegated(
|
|
ctx: MSTeamsDelegatedOAuthContext,
|
|
params: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
scopes?: readonly string[];
|
|
},
|
|
): Promise<MSTeamsDelegatedTokens> {
|
|
const scopes = params.scopes ?? MSTEAMS_DEFAULT_DELEGATED_SCOPES;
|
|
const needsManual = shouldUseManualOAuthFlow(ctx.isRemote);
|
|
|
|
await ctx.note(
|
|
needsManual
|
|
? [
|
|
"You are running in a remote/VPS environment.",
|
|
"A URL will be shown for you to open in your LOCAL browser.",
|
|
"After signing in, copy the redirect URL and paste it back here.",
|
|
].join("\n")
|
|
: [
|
|
"Browser will open for Microsoft authentication.",
|
|
`Sign in to grant delegated permissions for MSTeams.`,
|
|
`The callback will be captured automatically on localhost:${MSTEAMS_OAUTH_CALLBACK_PORT}.`,
|
|
].join("\n"),
|
|
"MSTeams Delegated OAuth",
|
|
);
|
|
|
|
const { verifier, challenge } = generatePkce();
|
|
const state = generateOAuthState();
|
|
const authUrl = buildMSTeamsAuthUrl({
|
|
tenantId: params.tenantId,
|
|
clientId: params.clientId,
|
|
challenge,
|
|
state,
|
|
scopes,
|
|
});
|
|
|
|
if (needsManual) {
|
|
return manualFlow(ctx, authUrl, state, verifier, params);
|
|
}
|
|
|
|
ctx.progress.update("Complete sign-in in browser...");
|
|
try {
|
|
await ctx.openUrl(authUrl);
|
|
} catch {
|
|
ctx.log(`\nOpen this URL in your browser:\n\n${authUrl}\n`);
|
|
}
|
|
|
|
try {
|
|
const { code } = await waitForLocalCallback({
|
|
expectedState: state,
|
|
timeoutMs: 5 * 60 * 1000,
|
|
onProgress: (msg) => ctx.progress.update(msg),
|
|
});
|
|
ctx.progress.update("Exchanging authorization code for tokens...");
|
|
return await exchangeMSTeamsCodeForTokens({
|
|
tenantId: params.tenantId,
|
|
clientId: params.clientId,
|
|
clientSecret: params.clientSecret,
|
|
code,
|
|
verifier,
|
|
scopes,
|
|
});
|
|
} catch (err) {
|
|
// EADDRINUSE or other listen errors: fall back to manual flow
|
|
if (
|
|
err instanceof Error &&
|
|
(err.message.includes("EADDRINUSE") ||
|
|
err.message.includes("port") ||
|
|
err.message.includes("listen"))
|
|
) {
|
|
ctx.progress.update("Local callback server failed. Switching to manual mode...");
|
|
return manualFlow(ctx, authUrl, state, verifier, params, err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function manualFlow(
|
|
ctx: MSTeamsDelegatedOAuthContext,
|
|
authUrl: string,
|
|
state: string,
|
|
verifier: string,
|
|
params: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
scopes?: readonly string[];
|
|
},
|
|
cause?: Error,
|
|
): Promise<MSTeamsDelegatedTokens> {
|
|
ctx.progress.update("OAuth URL ready");
|
|
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
|
ctx.progress.update("Waiting for you to paste the callback URL...");
|
|
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
|
const parsed = parseCallbackInput(callbackInput, state);
|
|
if ("error" in parsed) {
|
|
throw new Error(parsed.error, cause ? { cause } : undefined);
|
|
}
|
|
if (parsed.state !== state) {
|
|
throw new Error("OAuth state mismatch - please try again", cause ? { cause } : undefined);
|
|
}
|
|
ctx.progress.update("Exchanging authorization code for tokens...");
|
|
return exchangeMSTeamsCodeForTokens({
|
|
tenantId: params.tenantId,
|
|
clientId: params.clientId,
|
|
clientSecret: params.clientSecret,
|
|
code: parsed.code,
|
|
verifier,
|
|
scopes: params.scopes,
|
|
});
|
|
}
|