@snehal96/unimail
Version:
Unified email fetching & document extraction layer for modern web apps
134 lines (133 loc) • 5.46 kB
JavaScript
import { ConfidentialClientApplication } from '@azure/msal-node';
/**
* Microsoft-specific OAuth implementation for Outlook/Microsoft 365
*/
export class OutlookOAuthProvider {
/**
* Initialize the Microsoft OAuth flow and generate an authorization URL
*/
async initializeOAuthFlow(options) {
const msalApp = this.createMsalApp(options);
// Generate a unique state value for security
const state = options.state || Math.random().toString(36).substring(2);
// List of Microsoft Graph API permission scopes for mail access
const scopes = options.scopes || [
'Mail.Read',
'offline_access', // Required for refresh tokens
'openid',
'profile',
'User.Read'
];
// Create authorization URL
const authCodeUrlParameters = {
scopes,
redirectUri: options.redirectUri,
prompt: options.prompt || 'consent',
state
};
try {
const authUrl = await msalApp.getAuthCodeUrl(authCodeUrlParameters);
return { authUrl, state };
}
catch (error) {
throw new Error(`Failed to initialize OAuth flow: ${error.message}`);
}
}
/**
* Handle the OAuth callback and exchange the code for tokens
*/
async handleCallback(code, options) {
const msalApp = this.createMsalApp(options);
try {
const tokenRequest = {
code,
scopes: options.scopes,
redirectUri: options.redirectUri,
};
const response = await msalApp.acquireTokenByCode(tokenRequest);
if (!response || !response.accessToken) {
throw new Error('No access token returned from Microsoft OAuth flow');
}
// Cast to our interface to handle types properly
const authResponse = response;
// Calculate expiry time
const expiresAt = authResponse.expiresOn
? authResponse.expiresOn.getTime()
: (Date.now() + 3600 * 1000);
return {
accessToken: authResponse.accessToken,
refreshToken: authResponse.refreshToken || '',
expiresAt,
tokenType: authResponse.tokenType || 'Bearer',
scope: authResponse.scopes?.join(' '),
idToken: authResponse.idToken || '',
};
}
catch (error) {
throw new Error(`Failed to exchange authorization code: ${error.message}`);
}
}
/**
* Refresh an access token using a refresh token
*/
async refreshToken(refreshToken, options) {
const msalApp = this.createMsalApp(options);
try {
const tokenRequest = {
refreshToken,
scopes: options.scopes,
};
const response = await msalApp.acquireTokenByRefreshToken(tokenRequest);
if (!response || !response.accessToken) {
throw new Error('No access token returned when refreshing token');
}
// Cast to our interface to handle types properly
const authResponse = response;
// Calculate expiry time
const expiresAt = authResponse.expiresOn
? authResponse.expiresOn.getTime()
: (Date.now() + 3600 * 1000);
return {
accessToken: authResponse.accessToken,
refreshToken: authResponse.refreshToken || refreshToken, // Keep existing refresh token if new one not provided
expiresAt,
tokenType: authResponse.tokenType || 'Bearer',
scope: authResponse.scopes?.join(' '),
idToken: authResponse.idToken || '',
};
}
catch (error) {
throw new Error(`Failed to refresh token: ${error.message}`);
}
}
/**
* Revoke a token
* Note: Microsoft identity platform doesn't have a specific token revocation endpoint like Google
* Best practice is to clear the token from storage and implement a short token lifetime
*/
async revokeToken(token, options) {
// Microsoft Graph doesn't provide an API to revoke tokens directly
// The token must be removed from storage and will expire based on its lifetime
// If a real revocation is needed, you could implement additional logic to:
// 1. Force the user to re-authenticate next time
// 2. Remove refresh tokens from your storage
// 3. Consider integration with Azure AD API to revoke tokens for your application
console.warn('Token revocation for Microsoft OAuth is not fully supported');
return true; // Return success as we've done what we can
}
/**
* Create a MSAL ConfidentialClientApplication with the provided options
* @private
*/
createMsalApp(options) {
const msalConfig = {
auth: {
clientId: options.clientId,
clientSecret: options.clientSecret,
// Add tenant ID if available, otherwise use common endpoint for multi-tenant apps
authority: `https://login.microsoftonline.com/${options.tenantId || 'common'}`
}
};
return new ConfidentialClientApplication(msalConfig);
}
}