mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
refactor: share msteams token request flow
This commit is contained in:
@@ -10,6 +10,101 @@ import {
|
||||
/** Five-minute buffer subtracted from token expiry to avoid edge-case clock drift. */
|
||||
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
||||
|
||||
type MSTeamsTokenResponse = {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
function createMSTeamsTokenBody(params: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
grantType: string;
|
||||
scopes: readonly string[];
|
||||
values?: Record<string, string>;
|
||||
}): URLSearchParams {
|
||||
const body = new URLSearchParams({
|
||||
client_id: params.clientId,
|
||||
client_secret: params.clientSecret,
|
||||
grant_type: params.grantType,
|
||||
scope: [...params.scopes].join(" "),
|
||||
});
|
||||
|
||||
for (const [key, value] of Object.entries(params.values ?? {})) {
|
||||
body.set(key, value);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async function fetchMSTeamsTokens(params: {
|
||||
tokenUrl: string;
|
||||
body: URLSearchParams;
|
||||
auditContext: string;
|
||||
failureLabel: string;
|
||||
}): Promise<MSTeamsTokenResponse> {
|
||||
const currentFetch = globalThis.fetch;
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: params.tokenUrl,
|
||||
fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: params.body,
|
||||
signal: AbortSignal.timeout(MSTEAMS_DEFAULT_TOKEN_FETCH_TIMEOUT_MS),
|
||||
},
|
||||
auditContext: params.auditContext,
|
||||
});
|
||||
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`MSTeams ${params.failureLabel} failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
return (await response.json()) as MSTeamsTokenResponse;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function requestMSTeamsDelegatedTokens(params: {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scopes?: readonly string[];
|
||||
grantType: string;
|
||||
values: Record<string, string>;
|
||||
auditContext: string;
|
||||
failureLabel: string;
|
||||
resolveRefreshToken: (data: MSTeamsTokenResponse) => string;
|
||||
}): Promise<MSTeamsDelegatedTokens> {
|
||||
const scopes = params.scopes ?? MSTEAMS_DEFAULT_DELEGATED_SCOPES;
|
||||
const body = createMSTeamsTokenBody({
|
||||
clientId: params.clientId,
|
||||
clientSecret: params.clientSecret,
|
||||
grantType: params.grantType,
|
||||
scopes,
|
||||
values: params.values,
|
||||
});
|
||||
const data = await fetchMSTeamsTokens({
|
||||
tokenUrl: buildMSTeamsTokenEndpoint(params.tenantId),
|
||||
body,
|
||||
auditContext: params.auditContext,
|
||||
failureLabel: params.failureLabel,
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: params.resolveRefreshToken(data),
|
||||
expiresAt: Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS,
|
||||
scopes: data.scope ? data.scope.split(" ") : [...scopes],
|
||||
};
|
||||
}
|
||||
|
||||
export async function exchangeMSTeamsCodeForTokens(params: {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
@@ -18,68 +113,26 @@ export async function exchangeMSTeamsCodeForTokens(params: {
|
||||
verifier: string;
|
||||
scopes?: readonly string[];
|
||||
}): Promise<MSTeamsDelegatedTokens> {
|
||||
const scopes = params.scopes ?? MSTEAMS_DEFAULT_DELEGATED_SCOPES;
|
||||
const tokenUrl = buildMSTeamsTokenEndpoint(params.tenantId);
|
||||
|
||||
const body = new URLSearchParams({
|
||||
client_id: params.clientId,
|
||||
client_secret: params.clientSecret,
|
||||
code: params.code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: MSTEAMS_OAUTH_REDIRECT_URI,
|
||||
code_verifier: params.verifier,
|
||||
scope: [...scopes].join(" "),
|
||||
});
|
||||
|
||||
const currentFetch = globalThis.fetch;
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: tokenUrl,
|
||||
fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body,
|
||||
signal: AbortSignal.timeout(MSTEAMS_DEFAULT_TOKEN_FETCH_TIMEOUT_MS),
|
||||
return await requestMSTeamsDelegatedTokens({
|
||||
tenantId: params.tenantId,
|
||||
clientId: params.clientId,
|
||||
clientSecret: params.clientSecret,
|
||||
grantType: "authorization_code",
|
||||
scopes: params.scopes,
|
||||
values: {
|
||||
code: params.code,
|
||||
redirect_uri: MSTEAMS_OAUTH_REDIRECT_URI,
|
||||
code_verifier: params.verifier,
|
||||
},
|
||||
auditContext: "msteams-oauth-token-exchange",
|
||||
failureLabel: "token exchange",
|
||||
resolveRefreshToken: (data) => {
|
||||
if (!data.refresh_token) {
|
||||
throw new Error("No refresh token received from Azure AD. Please try again.");
|
||||
}
|
||||
return data.refresh_token;
|
||||
},
|
||||
});
|
||||
|
||||
let data: {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope?: string;
|
||||
};
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`MSTeams token exchange failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
data = (await response.json()) as {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope?: string;
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
|
||||
if (!data.refresh_token) {
|
||||
throw new Error("No refresh token received from Azure AD. Please try again.");
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS;
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt,
|
||||
scopes: data.scope ? data.scope.split(" ") : [...scopes],
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshMSTeamsDelegatedTokens(params: {
|
||||
@@ -89,61 +142,17 @@ export async function refreshMSTeamsDelegatedTokens(params: {
|
||||
refreshToken: string;
|
||||
scopes?: readonly string[];
|
||||
}): Promise<MSTeamsDelegatedTokens> {
|
||||
const scopes = params.scopes ?? MSTEAMS_DEFAULT_DELEGATED_SCOPES;
|
||||
const tokenUrl = buildMSTeamsTokenEndpoint(params.tenantId);
|
||||
|
||||
const body = new URLSearchParams({
|
||||
client_id: params.clientId,
|
||||
client_secret: params.clientSecret,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: params.refreshToken,
|
||||
scope: [...scopes].join(" "),
|
||||
});
|
||||
|
||||
const currentFetch = globalThis.fetch;
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: tokenUrl,
|
||||
fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body,
|
||||
signal: AbortSignal.timeout(MSTEAMS_DEFAULT_TOKEN_FETCH_TIMEOUT_MS),
|
||||
return await requestMSTeamsDelegatedTokens({
|
||||
tenantId: params.tenantId,
|
||||
clientId: params.clientId,
|
||||
clientSecret: params.clientSecret,
|
||||
grantType: "refresh_token",
|
||||
scopes: params.scopes,
|
||||
values: {
|
||||
refresh_token: params.refreshToken,
|
||||
},
|
||||
auditContext: "msteams-oauth-token-refresh",
|
||||
failureLabel: "token refresh",
|
||||
resolveRefreshToken: (data) => data.refresh_token ?? params.refreshToken,
|
||||
});
|
||||
|
||||
let data: {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope?: string;
|
||||
};
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`MSTeams token refresh failed (${response.status}): ${errorText}`);
|
||||
}
|
||||
data = (await response.json()) as {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
scope?: string;
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS;
|
||||
|
||||
// Azure may not return a new refresh token on refresh; keep the old one
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token ?? params.refreshToken,
|
||||
expiresAt,
|
||||
scopes: data.scope ? data.scope.split(" ") : [...scopes],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user