ARTICLE AD BOX
import { Client } from "@microsoft/microsoft-graph-client";
import type {
CalendarProvider,
OAuthTokens,
Calendar,
ConnectionTestResult,
UserInfo,
Contact,
CreateEventParams,
UpdateEventParams,
DeleteEventParams,
SearchEventsParams,
CreatedEvent
} from "../types";
import {
oauthTokensSchema,
calendarSchema,
microsoftCalendarListSchema,
microsoftCalendarSchema,
connectionTestResultSchema
} from "../types";
import { MICROSOFT_OAUTH_CONFIG, OAUTH_ENDPOINTS } from "../oauth";
export class MicrosoftCalendarProvider implements CalendarProvider {
constructor() {}
getAuthUrl(redirectUri: string, state?: string): string {
const params = new URLSearchParams({
client_id: MICROSOFT_OAUTH_CONFIG.clientId,
response_type: "code",
redirect_uri: redirectUri,
scope: MICROSOFT_OAUTH_CONFIG.scopes.join(" "),
response_mode: "query",
prompt: "consent", // Force consent to always return a refresh token
...(state && { state }),
});
return `${OAUTH_ENDPOINTS.microsoft.auth}?${params.toString()}`;
}
async exchangeCodeForTokens(code: string, redirectUri: string): Promise<OAuthTokens> {
try {
// Use direct OAuth2 token endpoint instead of MSAL
// This gives us the actual refresh_token string (MSAL hides it in its internal cache)
const body = new URLSearchParams({
client_id: MICROSOFT_OAUTH_CONFIG.clientId,
client_secret: MICROSOFT_OAUTH_CONFIG.clientSecret,
code,
redirect_uri: redirectUri,
grant_type: 'authorization_code',
scope: MICROSOFT_OAUTH_CONFIG.scopes.join(' '),
});
const response = await fetch(OAUTH_ENDPOINTS.microsoft.token, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Token exchange failed: ${response.status} ${JSON.stringify(errorData)}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("No access token received from Microsoft");
}
const expiresAt = new Date();
if (data.expires_in) {
expiresAt.setSeconds(expiresAt.getSeconds() + data.expires_in);
} else {
// Default to 1 hour if no expiry provided
expiresAt.setHours(expiresAt.getHours() + 1);
}
console.log('Microsoft token exchange successful', {
hasAccessToken: !!data.access_token,
hasRefreshToken: !!data.refresh_token,
expiresIn: data.expires_in,
scope: data.scope,
});
const tokenData = {
accessToken: data.access_token,
refreshToken: data.refresh_token || undefined, // Real refresh token from OAuth response
expiresAt,
scope: data.scope || undefined,
};
return oauthTokensSchema.parse(tokenData);
} catch (error) {
throw new Error(`Microsoft OAuth token exchange failed: ${error}`);
}
}
async getUserInfo(accessToken: string): Promise<UserInfo> {
try {
const graphClient = Client.init({
authProvider: (done) => {
done(null, accessToken);
},
});
const userInfo = await graphClient
.api("/me")
.select("id,mail,userPrincipalName,displayName")
.get();
// Microsoft may use either 'mail' or 'userPrincipalName' for email
const email = userInfo.mail || userInfo.userPrincipalName;
if (!email || !userInfo.id) {
throw new Error("Required user information not available from Microsoft");
}
return {
email: email,
name: userInfo.displayName || undefined,
id: userInfo.id,
};
} catch (error: any) {
throw new Error(`Failed to fetch Microsoft user info: ${error.message}`);
}
}
async refreshTokens(refreshToken: string): Promise<OAuthTokens> {
try {
// Use direct OAuth2 token endpoint for refresh
const body = new URLSearchParams({
client_id: MICROSOFT_OAUTH_CONFIG.clientId,
client_secret: MICROSOFT_OAUTH_CONFIG.clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token',
scope: MICROSOFT_OAUTH_CONFIG.scopes.join(' '),
});
const response = await fetch(OAUTH_ENDPOINTS.microsoft.token, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Token refresh failed: ${response.status} ${JSON.stringify(errorData)}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("No access token received from Microsoft refresh");
}
const expiresAt = new Date();
if (data.expires_in) {
expiresAt.setSeconds(expiresAt.getSeconds() + data.expires_in);
} else {
expiresAt.setHours(expiresAt.getHours() + 1);
}
console.log('Microsoft token refresh successful', {
hasNewAccessToken: !!data.access_token,
hasNewRefreshToken: !!data.refresh_token,
expiresIn: data.expires_in,
});
const tokenData = {
accessToken: data.access_token,
// Microsoft may rotate refresh tokens - use new one if provided, keep old otherwise
refreshToken: data.refresh_token || refreshToken,
expiresAt,
scope: data.scope || undefined,
};
return oauthTokensSchema.parse(tokenData);
} catch (error) {
throw new Error(`Microsoft token refresh failed: ${error}`);
}
}
Here is full solution.
