/**
* OAuth Authentication Handler
* Handles OAuth 2.0 authorization code flow for user delegation
*/
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { OAuthAuthorizationArgs, OAuthTokenResponse } from '../types/document-generation-types.js';
import { randomUUID } from 'crypto';
// OAuth token storage (in-memory, consider Redis/database for production)
const tokenStore = new Map<string, OAuthTokenResponse>();
/**
* Handle OAuth authorization operations
*/
export async function handleOAuthAuthorization(
args: OAuthAuthorizationArgs,
clientId: string,
clientSecret: string,
tenantId: string,
redirectUri: string
): Promise<string> {
try {
switch (args.action) {
case 'get-auth-url':
return getAuthorizationUrl(args, clientId, tenantId, redirectUri);
case 'exchange-code':
return await exchangeCodeForToken(args, clientId, clientSecret, tenantId, redirectUri);
case 'refresh-token':
return await refreshAccessToken(args, clientId, clientSecret, tenantId);
case 'revoke':
return revokeToken(args);
default:
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown action: ${args.action}`
);
}
} catch (error) {
if (error instanceof McpError) throw error;
throw new McpError(
ErrorCode.InternalError,
`OAuth operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Generate OAuth authorization URL for user consent
*/
function getAuthorizationUrl(
args: OAuthAuthorizationArgs,
clientId: string,
tenantId: string,
redirectUri: string
): string {
// Default scopes for document generation
const defaultScopes = [
'Files.ReadWrite',
'Sites.ReadWrite.All',
'User.Read',
'offline_access'
];
const scopes = args.scopes && args.scopes.length > 0 ? args.scopes : defaultScopes;
const state = args.state || randomUUID();
const authUrl = new URL(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`);
authUrl.searchParams.append('client_id', clientId);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('redirect_uri', redirectUri);
authUrl.searchParams.append('scope', scopes.join(' '));
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('response_mode', 'query');
return JSON.stringify({
success: true,
authorizationUrl: authUrl.toString(),
state: state,
scopes: scopes,
instructions: [
'1. Open the authorization URL in a browser',
'2. Sign in with your Microsoft 365 account',
'3. Grant the requested permissions',
'4. Copy the authorization code from the redirect URL',
'5. Use the exchange-code action to get access token'
],
redirectUri: redirectUri,
expiresIn: '10 minutes'
}, null, 2);
}
/**
* Exchange authorization code for access token
*/
async function exchangeCodeForToken(
args: OAuthAuthorizationArgs,
clientId: string,
clientSecret: string,
tenantId: string,
redirectUri: string
): Promise<string> {
if (!args.code) {
throw new McpError(ErrorCode.InvalidRequest, 'code is required for exchange-code action');
}
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const params = new URLSearchParams();
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('code', args.code);
params.append('redirect_uri', redirectUri);
params.append('grant_type', 'authorization_code');
params.append('scope', 'Files.ReadWrite Sites.ReadWrite.All User.Read offline_access');
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
});
if (!response.ok) {
const errorData = await response.text();
throw new McpError(
ErrorCode.InternalError,
`Token exchange failed: ${response.status} ${response.statusText}. Details: ${errorData}`
);
}
const tokenData: OAuthTokenResponse = await response.json();
// Store token (use session ID in production)
const sessionId = randomUUID();
tokenStore.set(sessionId, tokenData);
return JSON.stringify({
success: true,
sessionId: sessionId,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresIn: tokenData.expires_in,
tokenType: tokenData.token_type,
scope: tokenData.scope,
message: 'Authorization successful! Use this session ID for subsequent document operations.',
instructions: [
'Store the sessionId securely',
'Use the accessToken for Graph API calls',
'Token expires in ' + tokenData.expires_in + ' seconds',
'Use refresh-token action before expiration'
]
}, null, 2);
}
/**
* Refresh access token using refresh token
*/
async function refreshAccessToken(
args: OAuthAuthorizationArgs,
clientId: string,
clientSecret: string,
tenantId: string
): Promise<string> {
if (!args.refreshToken) {
throw new McpError(ErrorCode.InvalidRequest, 'refreshToken is required for refresh-token action');
}
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const params = new URLSearchParams();
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('refresh_token', args.refreshToken);
params.append('grant_type', 'refresh_token');
params.append('scope', 'Files.ReadWrite Sites.ReadWrite.All User.Read offline_access');
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
});
if (!response.ok) {
const errorData = await response.text();
throw new McpError(
ErrorCode.InternalError,
`Token refresh failed: ${response.status} ${response.statusText}. Details: ${errorData}`
);
}
const tokenData: OAuthTokenResponse = await response.json();
// Store new token
const sessionId = randomUUID();
tokenStore.set(sessionId, tokenData);
return JSON.stringify({
success: true,
sessionId: sessionId,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresIn: tokenData.expires_in,
tokenType: tokenData.token_type,
scope: tokenData.scope,
message: 'Token refreshed successfully!'
}, null, 2);
}
/**
* Revoke OAuth token
*/
function revokeToken(args: OAuthAuthorizationArgs): string {
// In production, call Microsoft's revocation endpoint
// For now, just clear from memory
return JSON.stringify({
success: true,
message: 'Token revoked successfully. User will need to re-authorize.',
note: 'In production, this would call Microsoft\'s token revocation endpoint'
}, null, 2);
}
/**
* Get stored token by session ID (helper function)
*/
export function getStoredToken(sessionId: string): OAuthTokenResponse | undefined {
return tokenStore.get(sessionId);
}
/**
* Validate if token is expired (helper function)
*/
export function isTokenExpired(token: OAuthTokenResponse, acquiredTime: number): boolean {
const now = Date.now();
const expirationTime = acquiredTime + (token.expires_in * 1000);
return now >= expirationTime;
}