@mcp-z/oauth-microsoft 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cjs/index.d.cts +2 -1
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/dcr-router.js.map +1 -1
- package/dist/cjs/lib/dcr-utils.js.map +1 -1
- package/dist/cjs/lib/dcr-verify.js.map +1 -1
- package/dist/cjs/lib/fetch-with-timeout.js.map +1 -1
- package/dist/cjs/lib/loopback-router.d.cts +8 -0
- package/dist/cjs/lib/loopback-router.d.ts +8 -0
- package/dist/cjs/lib/loopback-router.js +219 -0
- package/dist/cjs/lib/loopback-router.js.map +1 -0
- package/dist/cjs/lib/token-verifier.js.map +1 -1
- package/dist/cjs/providers/dcr.js.map +1 -1
- package/dist/cjs/providers/device-code.js.map +1 -1
- package/dist/cjs/providers/loopback-oauth.d.cts +15 -17
- package/dist/cjs/providers/loopback-oauth.d.ts +15 -17
- package/dist/cjs/providers/loopback-oauth.js +190 -156
- package/dist/cjs/providers/loopback-oauth.js.map +1 -1
- package/dist/cjs/schemas/index.js.map +1 -1
- package/dist/cjs/setup/config.d.cts +4 -1
- package/dist/cjs/setup/config.d.ts +4 -1
- package/dist/cjs/setup/config.js +3 -0
- package/dist/cjs/setup/config.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/dcr-router.js.map +1 -1
- package/dist/esm/lib/dcr-utils.js.map +1 -1
- package/dist/esm/lib/dcr-verify.js.map +1 -1
- package/dist/esm/lib/fetch-with-timeout.js.map +1 -1
- package/dist/esm/lib/loopback-router.d.ts +8 -0
- package/dist/esm/lib/loopback-router.js +32 -0
- package/dist/esm/lib/loopback-router.js.map +1 -0
- package/dist/esm/lib/token-verifier.js.map +1 -1
- package/dist/esm/providers/dcr.js.map +1 -1
- package/dist/esm/providers/device-code.js +2 -2
- package/dist/esm/providers/device-code.js.map +1 -1
- package/dist/esm/providers/loopback-oauth.d.ts +15 -17
- package/dist/esm/providers/loopback-oauth.js +133 -115
- package/dist/esm/providers/loopback-oauth.js.map +1 -1
- package/dist/esm/schemas/index.js.map +1 -1
- package/dist/esm/setup/config.d.ts +4 -1
- package/dist/esm/setup/config.js +3 -0
- package/dist/esm/setup/config.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/oauth/oauth-microsoft/src/providers/device-code.ts"],"sourcesContent":["/**\n * Device Code OAuth Implementation for Microsoft\n *\n * Implements OAuth 2.0 Device Authorization Grant (RFC 8628) for headless/limited-input devices.\n * Designed for scenarios where interactive browser flows are impractical (SSH sessions, CI/CD, etc.).\n *\n * Flow:\n * 1. Request device code from Microsoft endpoint\n * 2. Display user_code and verification_uri to user\n * 3. Poll token endpoint until user completes authentication\n * 4. Cache access token + refresh token to storage\n * 5. Refresh tokens when expired\n *\n * Similar to service accounts in usage pattern: single static identity, minimal account management.\n */\n\nimport { getToken, type OAuth2TokenStorageProvider, setToken } from '@mcp-z/oauth';\nimport type { Keyv } from 'keyv';\nimport open from 'open';\nimport { fetchWithTimeout } from '../lib/fetch-with-timeout.ts';\nimport type { AuthContext, CachedToken, EnrichedExtra, Logger, MicrosoftAuthProvider, MicrosoftService } from '../types.ts';\n\n/**\n * Device Code Flow Response\n * Response from Microsoft device authorization endpoint\n */\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n verification_uri_complete?: string;\n expires_in: number;\n interval: number;\n message?: string;\n}\n\n/**\n * Token Response from Microsoft OAuth endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * Device Code Provider Configuration\n */\nexport interface DeviceCodeConfig {\n /** Microsoft service type (e.g., 'outlook') */\n service: MicrosoftService;\n /** Azure AD client ID */\n clientId: string;\n /** Azure AD tenant ID */\n tenantId: string;\n /** OAuth scopes to request (space-separated string or array) */\n scope: string;\n /** Logger instance */\n logger: Logger;\n /** Token storage for caching */\n tokenStore: Keyv<unknown>;\n /** Headless mode - print device code instead of opening browser */\n headless: boolean;\n}\n\n/**\n * DeviceCodeProvider implements OAuth2TokenStorageProvider using Microsoft Device Code Flow\n *\n * This provider:\n * - Initiates device code flow with Microsoft endpoint\n * - Displays user_code and verification_uri for manual authentication\n * - Polls token endpoint until user completes auth\n * - Stores access tokens + refresh tokens in Keyv storage\n * - Refreshes tokens when expired\n * - Provides single static identity (minimal account management like service accounts)\n *\n * @example\n * ```typescript\n * const provider = new DeviceCodeProvider({\n * service: 'outlook',\n * clientId: 'your-client-id',\n * tenantId: 'common',\n * scope: 'https://graph.microsoft.com/Mail.Read',\n * logger: console,\n * tokenStore: new Keyv(),\n * headless: true,\n * });\n *\n * // Get authenticated Microsoft Graph client\n * const token = await provider.getAccessToken('default');\n * ```\n */\nexport class DeviceCodeProvider implements OAuth2TokenStorageProvider {\n private config: DeviceCodeConfig;\n\n constructor(config: DeviceCodeConfig) {\n this.config = config;\n }\n\n /**\n * Start device code flow and poll for token\n *\n * 1. POST to /devicecode endpoint to get device_code and user_code\n * 2. Display verification instructions to user\n * 3. Poll /token endpoint every interval seconds\n * 4. Handle authorization_pending, slow_down, expired_token errors\n * 5. Return token when user completes authentication\n */\n private async startDeviceCodeFlow(accountId: string): Promise<CachedToken> {\n const { clientId, tenantId, scope, logger, headless } = this.config;\n\n // Step 1: Request device code\n const deviceCodeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/devicecode`;\n logger.debug('Requesting device code', { endpoint: deviceCodeEndpoint });\n\n const deviceCodeResponse = await fetchWithTimeout(deviceCodeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: clientId,\n scope: scope,\n }),\n });\n\n if (!deviceCodeResponse.ok) {\n const errorText = await deviceCodeResponse.text();\n throw new Error(`Device code request failed (HTTP ${deviceCodeResponse.status}): ${errorText}`);\n }\n\n const deviceCodeData = (await deviceCodeResponse.json()) as DeviceCodeResponse;\n const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } = deviceCodeData;\n\n // Step 2: Display instructions to user\n logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n logger.info('Device code authentication required');\n logger.info('');\n logger.info(`Please visit: ${verification_uri_complete || verification_uri}`);\n logger.info(`And enter code: ${user_code}`);\n logger.info('');\n logger.info(`Code expires in ${expires_in} seconds`);\n logger.info('Waiting for authentication...');\n logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n\n // Optional: Open browser in non-headless mode\n if (!headless) {\n const urlToOpen = verification_uri_complete || verification_uri;\n try {\n await open(urlToOpen);\n logger.debug('Opened browser to verification URL', { url: urlToOpen });\n } catch (error) {\n logger.debug('Failed to open browser', { error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n // Step 3: Poll token endpoint\n return await this.pollForToken(device_code, interval || 5, accountId);\n }\n\n /**\n * Poll Microsoft token endpoint until user completes authentication\n *\n * Handles Microsoft-specific error codes:\n * - authorization_pending: User hasn't completed auth yet, keep polling\n * - slow_down: Increase polling interval by 5 seconds\n * - authorization_declined: User denied authorization\n * - expired_token: Device code expired (typically after 15 minutes)\n */\n private async pollForToken(deviceCode: string, intervalSeconds: number, accountId: string): Promise<CachedToken> {\n const { clientId, tenantId, logger, service, tokenStore } = this.config;\n const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;\n\n let currentInterval = intervalSeconds;\n const startTime = Date.now();\n\n while (true) {\n // Wait for polling interval\n await new Promise((resolve) => setTimeout(resolve, currentInterval * 1000));\n\n logger.debug('Polling for token', { elapsed: Math.floor((Date.now() - startTime) / 1000), interval: currentInterval });\n\n const response = await fetchWithTimeout(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n client_id: clientId,\n device_code: deviceCode,\n }),\n });\n\n const responseData = (await response.json()) as TokenResponse & { error?: string; error_description?: string };\n\n if (response.ok) {\n // Success! Convert to CachedToken and store\n const tokenData = responseData as TokenResponse;\n const token: CachedToken = {\n accessToken: tokenData.access_token,\n ...(tokenData.refresh_token && { refreshToken: tokenData.refresh_token }),\n expiresAt: Date.now() + (tokenData.expires_in - 60) * 1000, // 60s safety margin\n ...(tokenData.scope && { scope: tokenData.scope }),\n };\n\n // Cache token to storage\n await setToken(tokenStore, { accountId, service }, token);\n logger.info('Device code authentication successful', { accountId });\n\n return token;\n }\n\n // Handle error responses\n const error = responseData.error;\n const errorDescription = responseData.error_description || '';\n\n if (error === 'authorization_pending') {\n // User hasn't completed auth yet - continue polling\n logger.debug('Authorization pending, waiting for user...');\n continue;\n }\n\n if (error === 'slow_down') {\n // Microsoft wants us to slow down polling\n currentInterval += 5;\n logger.debug('Received slow_down, increasing interval', { newInterval: currentInterval });\n continue;\n }\n\n if (error === 'authorization_declined') {\n throw new Error('User declined authorization. Please restart the authentication flow.');\n }\n\n if (error === 'expired_token') {\n throw new Error('Device code expired. Please restart the authentication flow.');\n }\n\n // Unknown error\n throw new Error(`Device code flow failed: ${error} - ${errorDescription}`);\n }\n }\n\n /**\n * Refresh expired access token using refresh token\n *\n * @param refreshToken - Refresh token from previous authentication\n * @returns New cached token with fresh access token\n */\n private async refreshAccessToken(refreshToken: string): Promise<CachedToken> {\n const { clientId, tenantId, scope, logger } = this.config;\n const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;\n\n logger.debug('Refreshing access token');\n\n const response = await fetchWithTimeout(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: refreshToken,\n scope: scope,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (HTTP ${response.status}): ${errorText}`);\n }\n\n const tokenData = (await response.json()) as TokenResponse;\n\n return {\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token || refreshToken, // Some responses may not include new refresh token\n expiresAt: Date.now() + (tokenData.expires_in - 60) * 1000, // 60s safety margin\n scope: tokenData.scope || scope,\n };\n }\n\n /**\n * Check if token is still valid (not expired)\n */\n private isTokenValid(token: CachedToken): boolean {\n return token.expiresAt !== undefined && token.expiresAt > Date.now();\n }\n\n /**\n * Get access token for Microsoft Graph API\n *\n * Flow:\n * 1. Check token storage\n * 2. If valid token exists, return it\n * 3. If expired but has refresh token, try refresh\n * 4. Otherwise, start new device code flow\n *\n * @param accountId - Account identifier. Defaults to 'device-code' (fixed identifier for device code flow).\n * @returns Access token for API requests\n */\n async getAccessToken(accountId?: string): Promise<string> {\n const { logger, service, tokenStore } = this.config;\n const effectiveAccountId = accountId ?? 'device-code';\n\n logger.debug('Getting access token', { service, accountId: effectiveAccountId });\n\n // Check storage for cached token\n const storedToken = await getToken<CachedToken>(tokenStore, { accountId: effectiveAccountId, service });\n\n if (storedToken && this.isTokenValid(storedToken)) {\n logger.debug('Using stored access token', { accountId: effectiveAccountId });\n return storedToken.accessToken;\n }\n\n // If stored token expired but has refresh token, try refresh\n if (storedToken?.refreshToken) {\n try {\n logger.info('Refreshing expired access token', { accountId: effectiveAccountId });\n const refreshedToken = await this.refreshAccessToken(storedToken.refreshToken);\n await setToken(tokenStore, { accountId: effectiveAccountId, service }, refreshedToken);\n return refreshedToken.accessToken;\n } catch (error) {\n logger.info('Token refresh failed', {\n accountId: effectiveAccountId,\n error: error instanceof Error ? error.message : String(error),\n });\n // In headless mode, cannot start interactive device code flow\n if (this.config.headless) {\n throw new Error(`Token refresh failed in headless mode. Cannot start interactive device code flow. Error: ${error instanceof Error ? error.message : String(error)}`);\n }\n // Fall through to new device code flow (interactive mode only)\n }\n }\n\n // No valid token - check if we can start device code flow\n if (this.config.headless) {\n throw new Error('No valid token available in headless mode. Device code flow requires user interaction. ' + 'Please run authentication flow interactively first or provide valid tokens.');\n }\n\n // Interactive mode - start device code flow\n logger.info('Starting device code flow', { accountId: effectiveAccountId });\n const token = await this.startDeviceCodeFlow(effectiveAccountId);\n return token.accessToken;\n }\n\n /**\n * Get user email from Microsoft Graph /me endpoint (pure query)\n *\n * @param accountId - Account identifier\n * @returns User's email address (userPrincipalName or mail field)\n */\n async getUserEmail(accountId?: string): Promise<string> {\n const { logger } = this.config;\n // Device code is single-account mode\n const token = await this.getAccessToken(accountId);\n\n logger.debug('Fetching user email from Microsoft Graph');\n\n const response = await fetchWithTimeout('https://graph.microsoft.com/v1.0/me', {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to get user email (HTTP ${response.status}): ${errorText}`);\n }\n\n const userData = (await response.json()) as { userPrincipalName?: string; mail?: string };\n const email = userData.userPrincipalName || userData.mail;\n\n if (!email) {\n throw new Error('User email not found in Microsoft Graph response');\n }\n\n return email;\n }\n\n /**\n * Create auth provider for Microsoft Graph SDK integration\n *\n * Device code provider ALWAYS uses fixed accountId='device-code'\n * This is by design - device code is a single static identity pattern\n *\n * @param accountId - Account identifier (must be 'device-code' or undefined, otherwise throws error)\n * @returns Auth provider with getAccessToken method\n */\n toAuthProvider(accountId?: string): { getAccessToken: () => Promise<string> } {\n // Device code ONLY works with 'device-code' account ID\n if (accountId !== undefined && accountId !== 'device-code') {\n throw new Error(`DeviceCodeProvider only supports accountId='device-code', got '${accountId}'. Device code uses a single static identity pattern.`);\n }\n\n // ALWAYS use fixed 'device-code' account ID\n const getToken = () => this.getAccessToken('device-code');\n\n return {\n getAccessToken: getToken,\n };\n }\n\n /**\n * Create Microsoft Graph AuthenticationProvider for SDK usage\n *\n * @param accountId - Account identifier\n * @returns AuthenticationProvider that provides access tokens\n */\n private createAuthProvider(accountId?: string): MicrosoftAuthProvider {\n return {\n getAccessToken: async () => {\n return await this.getAccessToken(accountId);\n },\n };\n }\n\n /**\n * Create middleware wrapper for single-user authentication\n *\n * Middleware wraps tool, resource, and prompt handlers and injects authContext into extra parameter.\n * Handlers receive MicrosoftAuthProvider via extra.authContext.auth for API calls.\n *\n * @returns Object with withToolAuth, withResourceAuth, withPromptAuth methods\n *\n * @example\n * ```typescript\n * // Server registration\n * const middleware = provider.authMiddleware();\n * const tools = toolFactories.map(f => f()).map(middleware.withToolAuth);\n * const resources = resourceFactories.map(f => f()).map(middleware.withResourceAuth);\n * const prompts = promptFactories.map(f => f()).map(middleware.withPromptAuth);\n *\n * // Tool handler receives auth\n * async function handler({ id }: In, extra: EnrichedExtra) {\n * // extra.authContext.auth is MicrosoftAuthProvider (from middleware)\n * const graph = Client.initWithMiddleware({ authProvider: extra.authContext.auth });\n * }\n * ```\n */\n authMiddleware() {\n // Shared wrapper logic - extracts extra parameter from specified position\n // Generic T captures the actual module type; handler is cast from unknown to callable\n const wrapAtPosition = <T extends { name: string; handler: unknown; [key: string]: unknown }>(module: T, extraPosition: number): T => {\n const originalHandler = module.handler as (...args: unknown[]) => Promise<unknown>;\n\n const wrappedHandler = async (...allArgs: unknown[]) => {\n // Extract extra from the correct position (defensive: handle arg-less tool pattern)\n // If called with fewer args than expected, use first arg as both args and extra\n let extra: EnrichedExtra;\n if (allArgs.length <= extraPosition) {\n // Arg-less tool pattern: single argument is both args and extra\n extra = (allArgs[0] || {}) as EnrichedExtra;\n allArgs[0] = extra;\n allArgs[extraPosition] = extra;\n } else {\n extra = (allArgs[extraPosition] || {}) as EnrichedExtra;\n allArgs[extraPosition] = extra;\n }\n\n try {\n // Use fixed accountId for storage isolation (like service-account pattern)\n const accountId = 'device-code';\n\n // Create Microsoft Graph authentication provider\n const auth = this.createAuthProvider(accountId);\n\n // Inject authContext and logger into extra parameter\n (extra as { authContext?: AuthContext }).authContext = {\n auth, // MicrosoftAuthProvider for Graph SDK\n accountId, // Account identifier\n };\n (extra as { logger?: unknown }).logger = this.config.logger;\n\n // Call original handler with all args\n return await originalHandler(...allArgs);\n } catch (error) {\n // Wrap auth errors with helpful context\n throw new Error(`Device code authentication failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n };\n\n return {\n ...module,\n handler: wrappedHandler,\n } as T;\n };\n\n return {\n // Use structural constraints to avoid contravariance check on handler type.\n // wrapAtPosition is now generic and returns T directly.\n withToolAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 1),\n withResourceAuth: <T extends { name: string; template?: unknown; config?: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 2),\n withPromptAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 0),\n };\n }\n}\n"],"names":["getToken","setToken","open","fetchWithTimeout","DeviceCodeProvider","startDeviceCodeFlow","accountId","clientId","tenantId","scope","logger","headless","config","deviceCodeEndpoint","debug","endpoint","deviceCodeResponse","method","headers","body","URLSearchParams","client_id","ok","errorText","text","Error","status","deviceCodeData","json","device_code","user_code","verification_uri","verification_uri_complete","expires_in","interval","info","urlToOpen","url","error","message","String","pollForToken","deviceCode","intervalSeconds","service","tokenStore","tokenEndpoint","currentInterval","startTime","Date","now","Promise","resolve","setTimeout","elapsed","Math","floor","response","grant_type","responseData","tokenData","token","accessToken","access_token","refresh_token","refreshToken","expiresAt","errorDescription","error_description","newInterval","refreshAccessToken","isTokenValid","undefined","getAccessToken","effectiveAccountId","storedToken","refreshedToken","getUserEmail","Authorization","userData","email","userPrincipalName","mail","toAuthProvider","createAuthProvider","authMiddleware","wrapAtPosition","module","extraPosition","originalHandler","handler","wrappedHandler","allArgs","extra","length","auth","authContext","withToolAuth","withResourceAuth","withPromptAuth"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/oauth-microsoft/src/providers/device-code.ts"],"sourcesContent":["/**\n * Device Code OAuth Implementation for Microsoft\n *\n * Implements OAuth 2.0 Device Authorization Grant (RFC 8628) for headless/limited-input devices.\n * Designed for scenarios where interactive browser flows are impractical (SSH sessions, CI/CD, etc.).\n *\n * Flow:\n * 1. Request device code from Microsoft endpoint\n * 2. Display user_code and verification_uri to user\n * 3. Poll token endpoint until user completes authentication\n * 4. Cache access token + refresh token to storage\n * 5. Refresh tokens when expired\n *\n * Similar to service accounts in usage pattern: single static identity, minimal account management.\n */\n\nimport { getToken, type OAuth2TokenStorageProvider, setToken } from '@mcp-z/oauth';\nimport type { Keyv } from 'keyv';\nimport open from 'open';\nimport { fetchWithTimeout } from '../lib/fetch-with-timeout.ts';\nimport type { AuthContext, CachedToken, EnrichedExtra, Logger, MicrosoftAuthProvider, MicrosoftService } from '../types.ts';\n\n/**\n * Device Code Flow Response\n * Response from Microsoft device authorization endpoint\n */\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n verification_uri_complete?: string;\n expires_in: number;\n interval: number;\n message?: string;\n}\n\n/**\n * Token Response from Microsoft OAuth endpoint\n */\ninterface TokenResponse {\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n scope?: string;\n token_type?: string;\n}\n\n/**\n * Device Code Provider Configuration\n */\nexport interface DeviceCodeConfig {\n /** Microsoft service type (e.g., 'outlook') */\n service: MicrosoftService;\n /** Azure AD client ID */\n clientId: string;\n /** Azure AD tenant ID */\n tenantId: string;\n /** OAuth scopes to request (space-separated string or array) */\n scope: string;\n /** Logger instance */\n logger: Logger;\n /** Token storage for caching */\n tokenStore: Keyv<unknown>;\n /** Headless mode - print device code instead of opening browser */\n headless: boolean;\n}\n\n/**\n * DeviceCodeProvider implements OAuth2TokenStorageProvider using Microsoft Device Code Flow\n *\n * This provider:\n * - Initiates device code flow with Microsoft endpoint\n * - Displays user_code and verification_uri for manual authentication\n * - Polls token endpoint until user completes auth\n * - Stores access tokens + refresh tokens in Keyv storage\n * - Refreshes tokens when expired\n * - Provides single static identity (minimal account management like service accounts)\n *\n * @example\n * ```typescript\n * const provider = new DeviceCodeProvider({\n * service: 'outlook',\n * clientId: 'your-client-id',\n * tenantId: 'common',\n * scope: 'https://graph.microsoft.com/Mail.Read',\n * logger: console,\n * tokenStore: new Keyv(),\n * headless: true,\n * });\n *\n * // Get authenticated Microsoft Graph client\n * const token = await provider.getAccessToken('default');\n * ```\n */\nexport class DeviceCodeProvider implements OAuth2TokenStorageProvider {\n private config: DeviceCodeConfig;\n\n constructor(config: DeviceCodeConfig) {\n this.config = config;\n }\n\n /**\n * Start device code flow and poll for token\n *\n * 1. POST to /devicecode endpoint to get device_code and user_code\n * 2. Display verification instructions to user\n * 3. Poll /token endpoint every interval seconds\n * 4. Handle authorization_pending, slow_down, expired_token errors\n * 5. Return token when user completes authentication\n */\n private async startDeviceCodeFlow(accountId: string): Promise<CachedToken> {\n const { clientId, tenantId, scope, logger, headless } = this.config;\n\n // Step 1: Request device code\n const deviceCodeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/devicecode`;\n logger.debug('Requesting device code', { endpoint: deviceCodeEndpoint });\n\n const deviceCodeResponse = await fetchWithTimeout(deviceCodeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: clientId,\n scope,\n }),\n });\n\n if (!deviceCodeResponse.ok) {\n const errorText = await deviceCodeResponse.text();\n throw new Error(`Device code request failed (HTTP ${deviceCodeResponse.status}): ${errorText}`);\n }\n\n const deviceCodeData = (await deviceCodeResponse.json()) as DeviceCodeResponse;\n const { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } = deviceCodeData;\n\n // Step 2: Display instructions to user\n logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n logger.info('Device code authentication required');\n logger.info('');\n logger.info(`Please visit: ${verification_uri_complete || verification_uri}`);\n logger.info(`And enter code: ${user_code}`);\n logger.info('');\n logger.info(`Code expires in ${expires_in} seconds`);\n logger.info('Waiting for authentication...');\n logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');\n\n // Optional: Open browser in non-headless mode\n if (!headless) {\n const urlToOpen = verification_uri_complete || verification_uri;\n try {\n await open(urlToOpen);\n logger.debug('Opened browser to verification URL', { url: urlToOpen });\n } catch (error) {\n logger.debug('Failed to open browser', { error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n // Step 3: Poll token endpoint\n return await this.pollForToken(device_code, interval || 5, accountId);\n }\n\n /**\n * Poll Microsoft token endpoint until user completes authentication\n *\n * Handles Microsoft-specific error codes:\n * - authorization_pending: User hasn't completed auth yet, keep polling\n * - slow_down: Increase polling interval by 5 seconds\n * - authorization_declined: User denied authorization\n * - expired_token: Device code expired (typically after 15 minutes)\n */\n private async pollForToken(deviceCode: string, intervalSeconds: number, accountId: string): Promise<CachedToken> {\n const { clientId, tenantId, logger, service, tokenStore } = this.config;\n const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;\n\n let currentInterval = intervalSeconds;\n const startTime = Date.now();\n\n while (true) {\n // Wait for polling interval\n await new Promise((resolve) => setTimeout(resolve, currentInterval * 1000));\n\n logger.debug('Polling for token', { elapsed: Math.floor((Date.now() - startTime) / 1000), interval: currentInterval });\n\n const response = await fetchWithTimeout(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n client_id: clientId,\n device_code: deviceCode,\n }),\n });\n\n const responseData = (await response.json()) as TokenResponse & { error?: string; error_description?: string };\n\n if (response.ok) {\n // Success! Convert to CachedToken and store\n const tokenData = responseData as TokenResponse;\n const token: CachedToken = {\n accessToken: tokenData.access_token,\n ...(tokenData.refresh_token && { refreshToken: tokenData.refresh_token }),\n expiresAt: Date.now() + (tokenData.expires_in - 60) * 1000, // 60s safety margin\n ...(tokenData.scope && { scope: tokenData.scope }),\n };\n\n // Cache token to storage\n await setToken(tokenStore, { accountId, service }, token);\n logger.info('Device code authentication successful', { accountId });\n\n return token;\n }\n\n // Handle error responses\n const error = responseData.error;\n const errorDescription = responseData.error_description || '';\n\n if (error === 'authorization_pending') {\n // User hasn't completed auth yet - continue polling\n logger.debug('Authorization pending, waiting for user...');\n continue;\n }\n\n if (error === 'slow_down') {\n // Microsoft wants us to slow down polling\n currentInterval += 5;\n logger.debug('Received slow_down, increasing interval', { newInterval: currentInterval });\n continue;\n }\n\n if (error === 'authorization_declined') {\n throw new Error('User declined authorization. Please restart the authentication flow.');\n }\n\n if (error === 'expired_token') {\n throw new Error('Device code expired. Please restart the authentication flow.');\n }\n\n // Unknown error\n throw new Error(`Device code flow failed: ${error} - ${errorDescription}`);\n }\n }\n\n /**\n * Refresh expired access token using refresh token\n *\n * @param refreshToken - Refresh token from previous authentication\n * @returns New cached token with fresh access token\n */\n private async refreshAccessToken(refreshToken: string): Promise<CachedToken> {\n const { clientId, tenantId, scope, logger } = this.config;\n const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;\n\n logger.debug('Refreshing access token');\n\n const response = await fetchWithTimeout(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: refreshToken,\n scope,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (HTTP ${response.status}): ${errorText}`);\n }\n\n const tokenData = (await response.json()) as TokenResponse;\n\n return {\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token || refreshToken, // Some responses may not include new refresh token\n expiresAt: Date.now() + (tokenData.expires_in - 60) * 1000, // 60s safety margin\n scope: tokenData.scope || scope,\n };\n }\n\n /**\n * Check if token is still valid (not expired)\n */\n private isTokenValid(token: CachedToken): boolean {\n return token.expiresAt !== undefined && token.expiresAt > Date.now();\n }\n\n /**\n * Get access token for Microsoft Graph API\n *\n * Flow:\n * 1. Check token storage\n * 2. If valid token exists, return it\n * 3. If expired but has refresh token, try refresh\n * 4. Otherwise, start new device code flow\n *\n * @param accountId - Account identifier. Defaults to 'device-code' (fixed identifier for device code flow).\n * @returns Access token for API requests\n */\n async getAccessToken(accountId?: string): Promise<string> {\n const { logger, service, tokenStore } = this.config;\n const effectiveAccountId = accountId ?? 'device-code';\n\n logger.debug('Getting access token', { service, accountId: effectiveAccountId });\n\n // Check storage for cached token\n const storedToken = await getToken<CachedToken>(tokenStore, { accountId: effectiveAccountId, service });\n\n if (storedToken && this.isTokenValid(storedToken)) {\n logger.debug('Using stored access token', { accountId: effectiveAccountId });\n return storedToken.accessToken;\n }\n\n // If stored token expired but has refresh token, try refresh\n if (storedToken?.refreshToken) {\n try {\n logger.info('Refreshing expired access token', { accountId: effectiveAccountId });\n const refreshedToken = await this.refreshAccessToken(storedToken.refreshToken);\n await setToken(tokenStore, { accountId: effectiveAccountId, service }, refreshedToken);\n return refreshedToken.accessToken;\n } catch (error) {\n logger.info('Token refresh failed', {\n accountId: effectiveAccountId,\n error: error instanceof Error ? error.message : String(error),\n });\n // In headless mode, cannot start interactive device code flow\n if (this.config.headless) {\n throw new Error(`Token refresh failed in headless mode. Cannot start interactive device code flow. Error: ${error instanceof Error ? error.message : String(error)}`);\n }\n // Fall through to new device code flow (interactive mode only)\n }\n }\n\n // No valid token - check if we can start device code flow\n if (this.config.headless) {\n throw new Error('No valid token available in headless mode. Device code flow requires user interaction. ' + 'Please run authentication flow interactively first or provide valid tokens.');\n }\n\n // Interactive mode - start device code flow\n logger.info('Starting device code flow', { accountId: effectiveAccountId });\n const token = await this.startDeviceCodeFlow(effectiveAccountId);\n return token.accessToken;\n }\n\n /**\n * Get user email from Microsoft Graph /me endpoint (pure query)\n *\n * @param accountId - Account identifier\n * @returns User's email address (userPrincipalName or mail field)\n */\n async getUserEmail(accountId?: string): Promise<string> {\n const { logger } = this.config;\n // Device code is single-account mode\n const token = await this.getAccessToken(accountId);\n\n logger.debug('Fetching user email from Microsoft Graph');\n\n const response = await fetchWithTimeout('https://graph.microsoft.com/v1.0/me', {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to get user email (HTTP ${response.status}): ${errorText}`);\n }\n\n const userData = (await response.json()) as { userPrincipalName?: string; mail?: string };\n const email = userData.userPrincipalName || userData.mail;\n\n if (!email) {\n throw new Error('User email not found in Microsoft Graph response');\n }\n\n return email;\n }\n\n /**\n * Create auth provider for Microsoft Graph SDK integration\n *\n * Device code provider ALWAYS uses fixed accountId='device-code'\n * This is by design - device code is a single static identity pattern\n *\n * @param accountId - Account identifier (must be 'device-code' or undefined, otherwise throws error)\n * @returns Auth provider with getAccessToken method\n */\n toAuthProvider(accountId?: string): { getAccessToken: () => Promise<string> } {\n // Device code ONLY works with 'device-code' account ID\n if (accountId !== undefined && accountId !== 'device-code') {\n throw new Error(`DeviceCodeProvider only supports accountId='device-code', got '${accountId}'. Device code uses a single static identity pattern.`);\n }\n\n // ALWAYS use fixed 'device-code' account ID\n const getToken = () => this.getAccessToken('device-code');\n\n return {\n getAccessToken: getToken,\n };\n }\n\n /**\n * Create Microsoft Graph AuthenticationProvider for SDK usage\n *\n * @param accountId - Account identifier\n * @returns AuthenticationProvider that provides access tokens\n */\n private createAuthProvider(accountId?: string): MicrosoftAuthProvider {\n return {\n getAccessToken: async () => {\n return await this.getAccessToken(accountId);\n },\n };\n }\n\n /**\n * Create middleware wrapper for single-user authentication\n *\n * Middleware wraps tool, resource, and prompt handlers and injects authContext into extra parameter.\n * Handlers receive MicrosoftAuthProvider via extra.authContext.auth for API calls.\n *\n * @returns Object with withToolAuth, withResourceAuth, withPromptAuth methods\n *\n * @example\n * ```typescript\n * // Server registration\n * const middleware = provider.authMiddleware();\n * const tools = toolFactories.map(f => f()).map(middleware.withToolAuth);\n * const resources = resourceFactories.map(f => f()).map(middleware.withResourceAuth);\n * const prompts = promptFactories.map(f => f()).map(middleware.withPromptAuth);\n *\n * // Tool handler receives auth\n * async function handler({ id }: In, extra: EnrichedExtra) {\n * // extra.authContext.auth is MicrosoftAuthProvider (from middleware)\n * const graph = Client.initWithMiddleware({ authProvider: extra.authContext.auth });\n * }\n * ```\n */\n authMiddleware() {\n // Shared wrapper logic - extracts extra parameter from specified position\n // Generic T captures the actual module type; handler is cast from unknown to callable\n const wrapAtPosition = <T extends { name: string; handler: unknown; [key: string]: unknown }>(module: T, extraPosition: number): T => {\n const originalHandler = module.handler as (...args: unknown[]) => Promise<unknown>;\n\n const wrappedHandler = async (...allArgs: unknown[]) => {\n // Extract extra from the correct position (defensive: handle arg-less tool pattern)\n // If called with fewer args than expected, use first arg as both args and extra\n let extra: EnrichedExtra;\n if (allArgs.length <= extraPosition) {\n // Arg-less tool pattern: single argument is both args and extra\n extra = (allArgs[0] || {}) as EnrichedExtra;\n allArgs[0] = extra;\n allArgs[extraPosition] = extra;\n } else {\n extra = (allArgs[extraPosition] || {}) as EnrichedExtra;\n allArgs[extraPosition] = extra;\n }\n\n try {\n // Use fixed accountId for storage isolation (like service-account pattern)\n const accountId = 'device-code';\n\n // Create Microsoft Graph authentication provider\n const auth = this.createAuthProvider(accountId);\n\n // Inject authContext and logger into extra parameter\n (extra as { authContext?: AuthContext }).authContext = {\n auth, // MicrosoftAuthProvider for Graph SDK\n accountId, // Account identifier\n };\n (extra as { logger?: unknown }).logger = this.config.logger;\n\n // Call original handler with all args\n return await originalHandler(...allArgs);\n } catch (error) {\n // Wrap auth errors with helpful context\n throw new Error(`Device code authentication failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n };\n\n return {\n ...module,\n handler: wrappedHandler,\n } as T;\n };\n\n return {\n // Use structural constraints to avoid contravariance check on handler type.\n // wrapAtPosition is now generic and returns T directly.\n withToolAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 1),\n withResourceAuth: <T extends { name: string; template?: unknown; config?: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 2),\n withPromptAuth: <T extends { name: string; config: unknown; handler: unknown }>(module: T) => wrapAtPosition(module, 0),\n };\n }\n}\n"],"names":["getToken","setToken","open","fetchWithTimeout","DeviceCodeProvider","startDeviceCodeFlow","accountId","clientId","tenantId","scope","logger","headless","config","deviceCodeEndpoint","debug","endpoint","deviceCodeResponse","method","headers","body","URLSearchParams","client_id","ok","errorText","text","Error","status","deviceCodeData","json","device_code","user_code","verification_uri","verification_uri_complete","expires_in","interval","info","urlToOpen","url","error","message","String","pollForToken","deviceCode","intervalSeconds","service","tokenStore","tokenEndpoint","currentInterval","startTime","Date","now","Promise","resolve","setTimeout","elapsed","Math","floor","response","grant_type","responseData","tokenData","token","accessToken","access_token","refresh_token","refreshToken","expiresAt","errorDescription","error_description","newInterval","refreshAccessToken","isTokenValid","undefined","getAccessToken","effectiveAccountId","storedToken","refreshedToken","getUserEmail","Authorization","userData","email","userPrincipalName","mail","toAuthProvider","createAuthProvider","authMiddleware","wrapAtPosition","module","extraPosition","originalHandler","handler","wrappedHandler","allArgs","extra","length","auth","authContext","withToolAuth","withResourceAuth","withPromptAuth"],"mappings":"AAAA;;;;;;;;;;;;;;CAcC,GAED,SAASA,QAAQ,EAAmCC,QAAQ,QAAQ,eAAe;AAEnF,OAAOC,UAAU,OAAO;AACxB,SAASC,gBAAgB,QAAQ,+BAA+B;AAgDhE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMC;IAOX;;;;;;;;GAQC,GACD,MAAcC,oBAAoBC,SAAiB,EAAwB;QACzE,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAG,IAAI,CAACC,MAAM;QAEnE,8BAA8B;QAC9B,MAAMC,qBAAqB,CAAC,kCAAkC,EAAEL,SAAS,uBAAuB,CAAC;QACjGE,OAAOI,KAAK,CAAC,0BAA0B;YAAEC,UAAUF;QAAmB;QAEtE,MAAMG,qBAAqB,MAAMb,iBAAiBU,oBAAoB;YACpEI,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAoC;YAC/DC,MAAM,IAAIC,gBAAgB;gBACxBC,WAAWd;gBACXE;YACF;QACF;QAEA,IAAI,CAACO,mBAAmBM,EAAE,EAAE;YAC1B,MAAMC,YAAY,MAAMP,mBAAmBQ,IAAI;YAC/C,MAAM,IAAIC,MAAM,CAAC,iCAAiC,EAAET,mBAAmBU,MAAM,CAAC,GAAG,EAAEH,WAAW;QAChG;QAEA,MAAMI,iBAAkB,MAAMX,mBAAmBY,IAAI;QACrD,MAAM,EAAEC,WAAW,EAAEC,SAAS,EAAEC,gBAAgB,EAAEC,yBAAyB,EAAEC,UAAU,EAAEC,QAAQ,EAAE,GAAGP;QAEtG,uCAAuC;QACvCjB,OAAOyB,IAAI,CAAC;QACZzB,OAAOyB,IAAI,CAAC;QACZzB,OAAOyB,IAAI,CAAC;QACZzB,OAAOyB,IAAI,CAAC,CAAC,cAAc,EAAEH,6BAA6BD,kBAAkB;QAC5ErB,OAAOyB,IAAI,CAAC,CAAC,gBAAgB,EAAEL,WAAW;QAC1CpB,OAAOyB,IAAI,CAAC;QACZzB,OAAOyB,IAAI,CAAC,CAAC,gBAAgB,EAAEF,WAAW,QAAQ,CAAC;QACnDvB,OAAOyB,IAAI,CAAC;QACZzB,OAAOyB,IAAI,CAAC;QAEZ,8CAA8C;QAC9C,IAAI,CAACxB,UAAU;YACb,MAAMyB,YAAYJ,6BAA6BD;YAC/C,IAAI;gBACF,MAAM7B,KAAKkC;gBACX1B,OAAOI,KAAK,CAAC,sCAAsC;oBAAEuB,KAAKD;gBAAU;YACtE,EAAE,OAAOE,OAAO;gBACd5B,OAAOI,KAAK,CAAC,0BAA0B;oBAAEwB,OAAOA,iBAAiBb,QAAQa,MAAMC,OAAO,GAAGC,OAAOF;gBAAO;YACzG;QACF;QAEA,8BAA8B;QAC9B,OAAO,MAAM,IAAI,CAACG,YAAY,CAACZ,aAAaK,YAAY,GAAG5B;IAC7D;IAEA;;;;;;;;GAQC,GACD,MAAcmC,aAAaC,UAAkB,EAAEC,eAAuB,EAAErC,SAAiB,EAAwB;QAC/G,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAEE,MAAM,EAAEkC,OAAO,EAAEC,UAAU,EAAE,GAAG,IAAI,CAACjC,MAAM;QACvE,MAAMkC,gBAAgB,CAAC,kCAAkC,EAAEtC,SAAS,kBAAkB,CAAC;QAEvF,IAAIuC,kBAAkBJ;QACtB,MAAMK,YAAYC,KAAKC,GAAG;QAE1B,MAAO,KAAM;YACX,4BAA4B;YAC5B,MAAM,IAAIC,QAAQ,CAACC,UAAYC,WAAWD,SAASL,kBAAkB;YAErErC,OAAOI,KAAK,CAAC,qBAAqB;gBAAEwC,SAASC,KAAKC,KAAK,CAAC,AAACP,CAAAA,KAAKC,GAAG,KAAKF,SAAQ,IAAK;gBAAOd,UAAUa;YAAgB;YAEpH,MAAMU,WAAW,MAAMtD,iBAAiB2C,eAAe;gBACrD7B,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAoC;gBAC/DC,MAAM,IAAIC,gBAAgB;oBACxBsC,YAAY;oBACZrC,WAAWd;oBACXsB,aAAaa;gBACf;YACF;YAEA,MAAMiB,eAAgB,MAAMF,SAAS7B,IAAI;YAEzC,IAAI6B,SAASnC,EAAE,EAAE;gBACf,4CAA4C;gBAC5C,MAAMsC,YAAYD;gBAClB,MAAME,QAAqB;oBACzBC,aAAaF,UAAUG,YAAY;oBACnC,GAAIH,UAAUI,aAAa,IAAI;wBAAEC,cAAcL,UAAUI,aAAa;oBAAC,CAAC;oBACxEE,WAAWjB,KAAKC,GAAG,KAAK,AAACU,CAAAA,UAAU3B,UAAU,GAAG,EAAC,IAAK;oBACtD,GAAI2B,UAAUnD,KAAK,IAAI;wBAAEA,OAAOmD,UAAUnD,KAAK;oBAAC,CAAC;gBACnD;gBAEA,yBAAyB;gBACzB,MAAMR,SAAS4C,YAAY;oBAAEvC;oBAAWsC;gBAAQ,GAAGiB;gBACnDnD,OAAOyB,IAAI,CAAC,yCAAyC;oBAAE7B;gBAAU;gBAEjE,OAAOuD;YACT;YAEA,yBAAyB;YACzB,MAAMvB,QAAQqB,aAAarB,KAAK;YAChC,MAAM6B,mBAAmBR,aAAaS,iBAAiB,IAAI;YAE3D,IAAI9B,UAAU,yBAAyB;gBACrC,oDAAoD;gBACpD5B,OAAOI,KAAK,CAAC;gBACb;YACF;YAEA,IAAIwB,UAAU,aAAa;gBACzB,0CAA0C;gBAC1CS,mBAAmB;gBACnBrC,OAAOI,KAAK,CAAC,2CAA2C;oBAAEuD,aAAatB;gBAAgB;gBACvF;YACF;YAEA,IAAIT,UAAU,0BAA0B;gBACtC,MAAM,IAAIb,MAAM;YAClB;YAEA,IAAIa,UAAU,iBAAiB;gBAC7B,MAAM,IAAIb,MAAM;YAClB;YAEA,gBAAgB;YAChB,MAAM,IAAIA,MAAM,CAAC,yBAAyB,EAAEa,MAAM,GAAG,EAAE6B,kBAAkB;QAC3E;IACF;IAEA;;;;;GAKC,GACD,MAAcG,mBAAmBL,YAAoB,EAAwB;QAC3E,MAAM,EAAE1D,QAAQ,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,MAAM,EAAE,GAAG,IAAI,CAACE,MAAM;QACzD,MAAMkC,gBAAgB,CAAC,kCAAkC,EAAEtC,SAAS,kBAAkB,CAAC;QAEvFE,OAAOI,KAAK,CAAC;QAEb,MAAM2C,WAAW,MAAMtD,iBAAiB2C,eAAe;YACrD7B,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAoC;YAC/DC,MAAM,IAAIC,gBAAgB;gBACxBsC,YAAY;gBACZrC,WAAWd;gBACXyD,eAAeC;gBACfxD;YACF;QACF;QAEA,IAAI,CAACgD,SAASnC,EAAE,EAAE;YAChB,MAAMC,YAAY,MAAMkC,SAASjC,IAAI;YACrC,MAAM,IAAIC,MAAM,CAAC,2BAA2B,EAAEgC,SAAS/B,MAAM,CAAC,GAAG,EAAEH,WAAW;QAChF;QAEA,MAAMqC,YAAa,MAAMH,SAAS7B,IAAI;QAEtC,OAAO;YACLkC,aAAaF,UAAUG,YAAY;YACnCE,cAAcL,UAAUI,aAAa,IAAIC;YACzCC,WAAWjB,KAAKC,GAAG,KAAK,AAACU,CAAAA,UAAU3B,UAAU,GAAG,EAAC,IAAK;YACtDxB,OAAOmD,UAAUnD,KAAK,IAAIA;QAC5B;IACF;IAEA;;GAEC,GACD,AAAQ8D,aAAaV,KAAkB,EAAW;QAChD,OAAOA,MAAMK,SAAS,KAAKM,aAAaX,MAAMK,SAAS,GAAGjB,KAAKC,GAAG;IACpE;IAEA;;;;;;;;;;;GAWC,GACD,MAAMuB,eAAenE,SAAkB,EAAmB;QACxD,MAAM,EAAEI,MAAM,EAAEkC,OAAO,EAAEC,UAAU,EAAE,GAAG,IAAI,CAACjC,MAAM;QACnD,MAAM8D,qBAAqBpE,sBAAAA,uBAAAA,YAAa;QAExCI,OAAOI,KAAK,CAAC,wBAAwB;YAAE8B;YAAStC,WAAWoE;QAAmB;QAE9E,iCAAiC;QACjC,MAAMC,cAAc,MAAM3E,SAAsB6C,YAAY;YAAEvC,WAAWoE;YAAoB9B;QAAQ;QAErG,IAAI+B,eAAe,IAAI,CAACJ,YAAY,CAACI,cAAc;YACjDjE,OAAOI,KAAK,CAAC,6BAA6B;gBAAER,WAAWoE;YAAmB;YAC1E,OAAOC,YAAYb,WAAW;QAChC;QAEA,6DAA6D;QAC7D,IAAIa,wBAAAA,kCAAAA,YAAaV,YAAY,EAAE;YAC7B,IAAI;gBACFvD,OAAOyB,IAAI,CAAC,mCAAmC;oBAAE7B,WAAWoE;gBAAmB;gBAC/E,MAAME,iBAAiB,MAAM,IAAI,CAACN,kBAAkB,CAACK,YAAYV,YAAY;gBAC7E,MAAMhE,SAAS4C,YAAY;oBAAEvC,WAAWoE;oBAAoB9B;gBAAQ,GAAGgC;gBACvE,OAAOA,eAAed,WAAW;YACnC,EAAE,OAAOxB,OAAO;gBACd5B,OAAOyB,IAAI,CAAC,wBAAwB;oBAClC7B,WAAWoE;oBACXpC,OAAOA,iBAAiBb,QAAQa,MAAMC,OAAO,GAAGC,OAAOF;gBACzD;gBACA,8DAA8D;gBAC9D,IAAI,IAAI,CAAC1B,MAAM,CAACD,QAAQ,EAAE;oBACxB,MAAM,IAAIc,MAAM,CAAC,yFAAyF,EAAEa,iBAAiBb,QAAQa,MAAMC,OAAO,GAAGC,OAAOF,QAAQ;gBACtK;YACA,+DAA+D;YACjE;QACF;QAEA,0DAA0D;QAC1D,IAAI,IAAI,CAAC1B,MAAM,CAACD,QAAQ,EAAE;YACxB,MAAM,IAAIc,MAAM,4FAA4F;QAC9G;QAEA,4CAA4C;QAC5Cf,OAAOyB,IAAI,CAAC,6BAA6B;YAAE7B,WAAWoE;QAAmB;QACzE,MAAMb,QAAQ,MAAM,IAAI,CAACxD,mBAAmB,CAACqE;QAC7C,OAAOb,MAAMC,WAAW;IAC1B;IAEA;;;;;GAKC,GACD,MAAMe,aAAavE,SAAkB,EAAmB;QACtD,MAAM,EAAEI,MAAM,EAAE,GAAG,IAAI,CAACE,MAAM;QAC9B,qCAAqC;QACrC,MAAMiD,QAAQ,MAAM,IAAI,CAACY,cAAc,CAACnE;QAExCI,OAAOI,KAAK,CAAC;QAEb,MAAM2C,WAAW,MAAMtD,iBAAiB,uCAAuC;YAC7Ee,SAAS;gBAAE4D,eAAe,CAAC,OAAO,EAAEjB,OAAO;YAAC;QAC9C;QAEA,IAAI,CAACJ,SAASnC,EAAE,EAAE;YAChB,MAAMC,YAAY,MAAMkC,SAASjC,IAAI;YACrC,MAAM,IAAIC,MAAM,CAAC,+BAA+B,EAAEgC,SAAS/B,MAAM,CAAC,GAAG,EAAEH,WAAW;QACpF;QAEA,MAAMwD,WAAY,MAAMtB,SAAS7B,IAAI;QACrC,MAAMoD,QAAQD,SAASE,iBAAiB,IAAIF,SAASG,IAAI;QAEzD,IAAI,CAACF,OAAO;YACV,MAAM,IAAIvD,MAAM;QAClB;QAEA,OAAOuD;IACT;IAEA;;;;;;;;GAQC,GACDG,eAAe7E,SAAkB,EAA6C;QAC5E,uDAAuD;QACvD,IAAIA,cAAckE,aAAalE,cAAc,eAAe;YAC1D,MAAM,IAAImB,MAAM,CAAC,+DAA+D,EAAEnB,UAAU,qDAAqD,CAAC;QACpJ;QAEA,4CAA4C;QAC5C,MAAMN,WAAW,IAAM,IAAI,CAACyE,cAAc,CAAC;QAE3C,OAAO;YACLA,gBAAgBzE;QAClB;IACF;IAEA;;;;;GAKC,GACD,AAAQoF,mBAAmB9E,SAAkB,EAAyB;QACpE,OAAO;YACLmE,gBAAgB;gBACd,OAAO,MAAM,IAAI,CAACA,cAAc,CAACnE;YACnC;QACF;IACF;IAEA;;;;;;;;;;;;;;;;;;;;;;GAsBC,GACD+E,iBAAiB;QACf,0EAA0E;QAC1E,sFAAsF;QACtF,MAAMC,iBAAiB,CAAuEC,QAAWC;YACvG,MAAMC,kBAAkBF,OAAOG,OAAO;YAEtC,MAAMC,iBAAiB,OAAO,GAAGC;gBAC/B,oFAAoF;gBACpF,gFAAgF;gBAChF,IAAIC;gBACJ,IAAID,QAAQE,MAAM,IAAIN,eAAe;oBACnC,gEAAgE;oBAChEK,QAASD,OAAO,CAAC,EAAE,IAAI,CAAC;oBACxBA,OAAO,CAAC,EAAE,GAAGC;oBACbD,OAAO,CAACJ,cAAc,GAAGK;gBAC3B,OAAO;oBACLA,QAASD,OAAO,CAACJ,cAAc,IAAI,CAAC;oBACpCI,OAAO,CAACJ,cAAc,GAAGK;gBAC3B;gBAEA,IAAI;oBACF,2EAA2E;oBAC3E,MAAMvF,YAAY;oBAElB,iDAAiD;oBACjD,MAAMyF,OAAO,IAAI,CAACX,kBAAkB,CAAC9E;oBAErC,qDAAqD;oBACpDuF,MAAwCG,WAAW,GAAG;wBACrDD;wBACAzF;oBACF;oBACCuF,MAA+BnF,MAAM,GAAG,IAAI,CAACE,MAAM,CAACF,MAAM;oBAE3D,sCAAsC;oBACtC,OAAO,MAAM+E,mBAAmBG;gBAClC,EAAE,OAAOtD,OAAO;oBACd,wCAAwC;oBACxC,MAAM,IAAIb,MAAM,CAAC,mCAAmC,EAAEa,iBAAiBb,QAAQa,MAAMC,OAAO,GAAGC,OAAOF,QAAQ;gBAChH;YACF;YAEA,OAAO;gBACL,GAAGiD,MAAM;gBACTG,SAASC;YACX;QACF;QAEA,OAAO;YACL,4EAA4E;YAC5E,wDAAwD;YACxDM,cAAc,CAAgEV,SAAcD,eAAeC,QAAQ;YACnHW,kBAAkB,CAAqFX,SAAcD,eAAeC,QAAQ;YAC5IY,gBAAgB,CAAgEZ,SAAcD,eAAeC,QAAQ;QACvH;IACF;IAzYA,YAAY3E,MAAwB,CAAE;QACpC,IAAI,CAACA,MAAM,GAAGA;IAChB;AAwYF"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 7. Close ephemeral server
|
|
16
16
|
*/
|
|
17
17
|
import { type OAuth2TokenStorageProvider } from '@mcp-z/oauth';
|
|
18
|
-
import { type LoopbackOAuthConfig } from '../types.js';
|
|
18
|
+
import { type CachedToken, type LoopbackOAuthConfig } from '../types.js';
|
|
19
19
|
/**
|
|
20
20
|
* Loopback OAuth Client (RFC 8252 Section 7.3)
|
|
21
21
|
*
|
|
@@ -44,14 +44,6 @@ export declare class LoopbackOAuthProvider implements OAuth2TokenStorageProvider
|
|
|
44
44
|
toAuthProvider(accountId?: string): {
|
|
45
45
|
getAccessToken: () => Promise<string>;
|
|
46
46
|
};
|
|
47
|
-
/**
|
|
48
|
-
* Authenticate new account with OAuth flow
|
|
49
|
-
* Triggers account selection, stores token, registers account
|
|
50
|
-
*
|
|
51
|
-
* @returns Email address of newly authenticated account
|
|
52
|
-
* @throws Error in headless mode (cannot open browser for OAuth)
|
|
53
|
-
*/
|
|
54
|
-
authenticateNewAccount(): Promise<string>;
|
|
55
47
|
/**
|
|
56
48
|
* Get user email from Microsoft Graph API (pure query)
|
|
57
49
|
* Used to query email for existing authenticated account
|
|
@@ -60,14 +52,6 @@ export declare class LoopbackOAuthProvider implements OAuth2TokenStorageProvider
|
|
|
60
52
|
* @returns User's email address
|
|
61
53
|
*/
|
|
62
54
|
getUserEmail(accountId?: string): Promise<string>;
|
|
63
|
-
/**
|
|
64
|
-
* Check for existing accounts in token storage (incremental OAuth detection)
|
|
65
|
-
*
|
|
66
|
-
* Uses key-utils helper for forward compatibility with key format changes.
|
|
67
|
-
*
|
|
68
|
-
* @returns Array of account IDs that have tokens for this service
|
|
69
|
-
*/
|
|
70
|
-
private getExistingAccounts;
|
|
71
55
|
private isTokenValid;
|
|
72
56
|
/**
|
|
73
57
|
* Fetch user email from Microsoft Graph using access token
|
|
@@ -80,6 +64,20 @@ export declare class LoopbackOAuthProvider implements OAuth2TokenStorageProvider
|
|
|
80
64
|
private performEphemeralOAuthFlow;
|
|
81
65
|
private exchangeCodeForToken;
|
|
82
66
|
private refreshAccessToken;
|
|
67
|
+
/**
|
|
68
|
+
* Handle OAuth callback from persistent endpoint.
|
|
69
|
+
* Used by HTTP servers with configured redirectUri.
|
|
70
|
+
*
|
|
71
|
+
* @param params - OAuth callback parameters
|
|
72
|
+
* @returns Email and cached token
|
|
73
|
+
*/
|
|
74
|
+
handleOAuthCallback(params: {
|
|
75
|
+
code: string;
|
|
76
|
+
state?: string;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
email: string;
|
|
79
|
+
token: CachedToken;
|
|
80
|
+
}>;
|
|
83
81
|
/**
|
|
84
82
|
* Create auth middleware for single-user context (single active account per service)
|
|
85
83
|
*
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* 5. Handle callback, exchange code for token
|
|
14
14
|
* 6. Cache token to storage
|
|
15
15
|
* 7. Close ephemeral server
|
|
16
|
-
*/ import { addAccount, generatePKCE, getActiveAccount, getErrorTemplate, getSuccessTemplate, getToken,
|
|
16
|
+
*/ import { addAccount, generatePKCE, getActiveAccount, getErrorTemplate, getSuccessTemplate, getToken, setAccountInfo, setActiveAccount, setToken } from '@mcp-z/oauth';
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
17
18
|
import * as http from 'http';
|
|
18
19
|
import open from 'open';
|
|
19
20
|
import { fetchWithTimeout } from '../lib/fetch-with-timeout.js';
|
|
@@ -76,66 +77,55 @@ import { AuthRequiredError } from '../types.js';
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
80
|
+
const { clientId, tenantId, scope, redirectUri } = this.config;
|
|
81
|
+
if (redirectUri) {
|
|
82
|
+
// Persistent callback mode (cloud deployment with configured redirect_uri)
|
|
83
|
+
const { verifier: codeVerifier, challenge: codeChallenge } = generatePKCE();
|
|
84
|
+
const stateId = randomUUID();
|
|
85
|
+
// Store PKCE verifier for callback (5 minute TTL)
|
|
86
|
+
await tokenStore.set(`${service}:pending:${stateId}`, {
|
|
87
|
+
codeVerifier,
|
|
88
|
+
createdAt: Date.now()
|
|
89
|
+
}, 5 * 60 * 1000);
|
|
90
|
+
// Build auth URL with configured redirect_uri
|
|
90
91
|
const authUrl = new URL(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`);
|
|
91
92
|
authUrl.searchParams.set('client_id', clientId);
|
|
93
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
92
94
|
authUrl.searchParams.set('response_type', 'code');
|
|
93
95
|
authUrl.searchParams.set('scope', scope);
|
|
94
96
|
authUrl.searchParams.set('response_mode', 'query');
|
|
97
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
98
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
99
|
+
authUrl.searchParams.set('state', stateId);
|
|
95
100
|
authUrl.searchParams.set('prompt', 'select_account');
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
hint = `Use account-add to authenticate ${effectiveAccountId}`;
|
|
102
|
-
} else {
|
|
103
|
-
hint = 'Use account-add to authenticate interactively';
|
|
104
|
-
}
|
|
105
|
-
const baseDescriptor = {
|
|
101
|
+
logger.info('OAuth required - persistent callback mode', {
|
|
102
|
+
service,
|
|
103
|
+
redirectUri
|
|
104
|
+
});
|
|
105
|
+
throw new AuthRequiredError({
|
|
106
106
|
kind: 'auth_url',
|
|
107
|
-
provider:
|
|
108
|
-
url: authUrl.toString()
|
|
109
|
-
|
|
110
|
-
};
|
|
111
|
-
const descriptor = effectiveAccountId ? {
|
|
112
|
-
...baseDescriptor,
|
|
113
|
-
accountId: effectiveAccountId
|
|
114
|
-
} : baseDescriptor;
|
|
115
|
-
throw new AuthRequiredError(descriptor);
|
|
107
|
+
provider: service,
|
|
108
|
+
url: authUrl.toString()
|
|
109
|
+
});
|
|
116
110
|
}
|
|
117
|
-
//
|
|
111
|
+
// Ephemeral callback mode (local development)
|
|
118
112
|
logger.info('Starting ephemeral OAuth flow', {
|
|
119
113
|
service,
|
|
120
|
-
headless
|
|
114
|
+
headless: this.config.headless
|
|
121
115
|
});
|
|
122
116
|
const { token, email } = await this.performEphemeralOAuthFlow();
|
|
123
|
-
// Store token with email as accountId
|
|
124
117
|
await setToken(tokenStore, {
|
|
125
118
|
accountId: email,
|
|
126
119
|
service
|
|
127
120
|
}, token);
|
|
128
|
-
// Register account in account management system
|
|
129
121
|
await addAccount(tokenStore, {
|
|
130
122
|
service,
|
|
131
123
|
accountId: email
|
|
132
124
|
});
|
|
133
|
-
// Set as active account so subsequent getAccessToken() calls find it
|
|
134
125
|
await setActiveAccount(tokenStore, {
|
|
135
126
|
service,
|
|
136
127
|
accountId: email
|
|
137
128
|
});
|
|
138
|
-
// Store account metadata (email, added timestamp)
|
|
139
129
|
await setAccountInfo(tokenStore, {
|
|
140
130
|
service,
|
|
141
131
|
accountId: email
|
|
@@ -162,51 +152,6 @@ import { AuthRequiredError } from '../types.js';
|
|
|
162
152
|
};
|
|
163
153
|
}
|
|
164
154
|
/**
|
|
165
|
-
* Authenticate new account with OAuth flow
|
|
166
|
-
* Triggers account selection, stores token, registers account
|
|
167
|
-
*
|
|
168
|
-
* @returns Email address of newly authenticated account
|
|
169
|
-
* @throws Error in headless mode (cannot open browser for OAuth)
|
|
170
|
-
*/ async authenticateNewAccount() {
|
|
171
|
-
const { logger, headless, service, tokenStore } = this.config;
|
|
172
|
-
if (headless) {
|
|
173
|
-
throw new Error('Cannot authenticate new account in headless mode - interactive OAuth required');
|
|
174
|
-
}
|
|
175
|
-
logger.info('Starting new account authentication', {
|
|
176
|
-
service
|
|
177
|
-
});
|
|
178
|
-
// Trigger OAuth with account selection
|
|
179
|
-
const { token, email } = await this.performEphemeralOAuthFlow();
|
|
180
|
-
// Store token
|
|
181
|
-
await setToken(tokenStore, {
|
|
182
|
-
accountId: email,
|
|
183
|
-
service
|
|
184
|
-
}, token);
|
|
185
|
-
// Register account
|
|
186
|
-
await addAccount(tokenStore, {
|
|
187
|
-
service,
|
|
188
|
-
accountId: email
|
|
189
|
-
});
|
|
190
|
-
// Set as active account
|
|
191
|
-
await setActiveAccount(tokenStore, {
|
|
192
|
-
service,
|
|
193
|
-
accountId: email
|
|
194
|
-
});
|
|
195
|
-
// Store account metadata
|
|
196
|
-
await setAccountInfo(tokenStore, {
|
|
197
|
-
service,
|
|
198
|
-
accountId: email
|
|
199
|
-
}, {
|
|
200
|
-
email,
|
|
201
|
-
addedAt: new Date().toISOString()
|
|
202
|
-
});
|
|
203
|
-
logger.info('New account authenticated', {
|
|
204
|
-
service,
|
|
205
|
-
email
|
|
206
|
-
});
|
|
207
|
-
return email;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
155
|
* Get user email from Microsoft Graph API (pure query)
|
|
211
156
|
* Used to query email for existing authenticated account
|
|
212
157
|
*
|
|
@@ -228,16 +173,6 @@ import { AuthRequiredError } from '../types.js';
|
|
|
228
173
|
const userInfo = await response.json();
|
|
229
174
|
return (_userInfo_mail = userInfo.mail) !== null && _userInfo_mail !== void 0 ? _userInfo_mail : userInfo.userPrincipalName;
|
|
230
175
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Check for existing accounts in token storage (incremental OAuth detection)
|
|
233
|
-
*
|
|
234
|
-
* Uses key-utils helper for forward compatibility with key format changes.
|
|
235
|
-
*
|
|
236
|
-
* @returns Array of account IDs that have tokens for this service
|
|
237
|
-
*/ async getExistingAccounts() {
|
|
238
|
-
const { service, tokenStore } = this.config;
|
|
239
|
-
return listAccountIds(tokenStore, service);
|
|
240
|
-
}
|
|
241
176
|
isTokenValid(token) {
|
|
242
177
|
if (!token.expiresAt) return true; // No expiry = assume valid
|
|
243
178
|
return Date.now() < token.expiresAt - 60000; // 1 minute buffer
|
|
@@ -269,42 +204,46 @@ import { AuthRequiredError } from '../types.js';
|
|
|
269
204
|
}
|
|
270
205
|
async performEphemeralOAuthFlow() {
|
|
271
206
|
const { clientId, tenantId, scope, headless, logger, redirectUri: configRedirectUri } = this.config;
|
|
272
|
-
//
|
|
273
|
-
let
|
|
274
|
-
let
|
|
275
|
-
|
|
207
|
+
// Server listen configuration (where ephemeral server binds)
|
|
208
|
+
let listenHost = 'localhost'; // Default: localhost for ephemeral loopback
|
|
209
|
+
let listenPort = 0; // Default: OS-assigned ephemeral port
|
|
210
|
+
// Redirect URI configuration (what goes in auth URL and token exchange)
|
|
276
211
|
let callbackPath = '/callback'; // Default callback path
|
|
277
212
|
let useConfiguredUri = false;
|
|
278
213
|
if (configRedirectUri) {
|
|
279
214
|
try {
|
|
280
215
|
const parsed = new URL(configRedirectUri);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
targetPort = Number.parseInt(parsed.port, 10);
|
|
216
|
+
const isLoopback = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
|
217
|
+
if (isLoopback) {
|
|
218
|
+
// Local development: Listen on specific loopback address/port
|
|
219
|
+
listenHost = parsed.hostname;
|
|
220
|
+
listenPort = parsed.port ? Number.parseInt(parsed.port, 10) : 0;
|
|
287
221
|
} else {
|
|
288
|
-
|
|
222
|
+
// Cloud deployment: Listen on 0.0.0.0 with PORT from environment
|
|
223
|
+
// The redirectUri is the PUBLIC URL (e.g., https://example.com/oauth/callback)
|
|
224
|
+
// The server listens on 0.0.0.0:PORT and the load balancer routes to it
|
|
225
|
+
listenHost = '0.0.0.0';
|
|
226
|
+
const envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined;
|
|
227
|
+
listenPort = envPort && Number.isFinite(envPort) ? envPort : 8080;
|
|
289
228
|
}
|
|
290
|
-
// Extract
|
|
229
|
+
// Extract callback path from URL
|
|
291
230
|
if (parsed.pathname && parsed.pathname !== '/') {
|
|
292
231
|
callbackPath = parsed.pathname;
|
|
293
232
|
}
|
|
294
233
|
useConfiguredUri = true;
|
|
295
234
|
logger.debug('Using configured redirect URI', {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
235
|
+
listenHost,
|
|
236
|
+
listenPort,
|
|
237
|
+
callbackPath,
|
|
238
|
+
redirectUri: configRedirectUri,
|
|
239
|
+
isLoopback
|
|
301
240
|
});
|
|
302
241
|
} catch (error) {
|
|
303
242
|
logger.warn('Failed to parse redirectUri, using ephemeral defaults', {
|
|
304
243
|
redirectUri: configRedirectUri,
|
|
305
244
|
error: error instanceof Error ? error.message : String(error)
|
|
306
245
|
});
|
|
307
|
-
// Continue with defaults (
|
|
246
|
+
// Continue with defaults (localhost, port 0, http, /callback)
|
|
308
247
|
}
|
|
309
248
|
}
|
|
310
249
|
return new Promise((resolve, reject)=>{
|
|
@@ -391,8 +330,11 @@ import { AuthRequiredError } from '../types.js';
|
|
|
391
330
|
res.end('Not Found');
|
|
392
331
|
}
|
|
393
332
|
});
|
|
394
|
-
// Listen on
|
|
395
|
-
|
|
333
|
+
// Listen on configured host/port
|
|
334
|
+
// - For loopback (default): localhost with OS-assigned port
|
|
335
|
+
// - For configured loopback: specific localhost port from redirectUri
|
|
336
|
+
// - For cloud deployment: 0.0.0.0:${PORT} from environment
|
|
337
|
+
server.listen(listenPort, listenHost, ()=>{
|
|
396
338
|
const address = server === null || server === void 0 ? void 0 : server.address();
|
|
397
339
|
if (!address || typeof address === 'string') {
|
|
398
340
|
server === null || server === void 0 ? void 0 : server.close();
|
|
@@ -402,11 +344,11 @@ import { AuthRequiredError } from '../types.js';
|
|
|
402
344
|
serverPort = address.port;
|
|
403
345
|
// Construct final redirect URI
|
|
404
346
|
if (useConfiguredUri && configRedirectUri) {
|
|
405
|
-
// Use configured redirect URI as-is for
|
|
347
|
+
// Use configured redirect URI as-is (public URL for cloud, or specific local URL)
|
|
406
348
|
finalRedirectUri = configRedirectUri;
|
|
407
349
|
} else {
|
|
408
|
-
// Construct ephemeral redirect URI with actual server port
|
|
409
|
-
finalRedirectUri =
|
|
350
|
+
// Construct ephemeral redirect URI with actual server port (default local behavior)
|
|
351
|
+
finalRedirectUri = `http://localhost:${serverPort}${callbackPath}`;
|
|
410
352
|
}
|
|
411
353
|
// Build Microsoft auth URL
|
|
412
354
|
const authUrl = new URL(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`);
|
|
@@ -515,6 +457,82 @@ import { AuthRequiredError } from '../types.js';
|
|
|
515
457
|
};
|
|
516
458
|
}
|
|
517
459
|
/**
|
|
460
|
+
* Handle OAuth callback from persistent endpoint.
|
|
461
|
+
* Used by HTTP servers with configured redirectUri.
|
|
462
|
+
*
|
|
463
|
+
* @param params - OAuth callback parameters
|
|
464
|
+
* @returns Email and cached token
|
|
465
|
+
*/ async handleOAuthCallback(params) {
|
|
466
|
+
const { code, state } = params;
|
|
467
|
+
const { logger, service, tokenStore, redirectUri } = this.config;
|
|
468
|
+
if (!state) {
|
|
469
|
+
throw new Error('Missing state parameter in OAuth callback');
|
|
470
|
+
}
|
|
471
|
+
if (!redirectUri) {
|
|
472
|
+
throw new Error('handleOAuthCallback requires configured redirectUri');
|
|
473
|
+
}
|
|
474
|
+
// Load pending auth (includes PKCE verifier)
|
|
475
|
+
const pendingKey = `${service}:pending:${state}`;
|
|
476
|
+
const pendingAuth = await tokenStore.get(pendingKey);
|
|
477
|
+
if (!pendingAuth) {
|
|
478
|
+
throw new Error('Invalid or expired OAuth state. Please try again.');
|
|
479
|
+
}
|
|
480
|
+
// Check TTL (5 minutes)
|
|
481
|
+
if (Date.now() - pendingAuth.createdAt > 5 * 60 * 1000) {
|
|
482
|
+
await tokenStore.delete(pendingKey);
|
|
483
|
+
throw new Error('OAuth state expired. Please try again.');
|
|
484
|
+
}
|
|
485
|
+
logger.info('Processing OAuth callback', {
|
|
486
|
+
service,
|
|
487
|
+
state
|
|
488
|
+
});
|
|
489
|
+
// Exchange code for token
|
|
490
|
+
const tokenResponse = await this.exchangeCodeForToken(code, pendingAuth.codeVerifier, redirectUri);
|
|
491
|
+
// Create cached token
|
|
492
|
+
const cachedToken = {
|
|
493
|
+
accessToken: tokenResponse.access_token,
|
|
494
|
+
refreshToken: tokenResponse.refresh_token,
|
|
495
|
+
expiresAt: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1000 : undefined,
|
|
496
|
+
...tokenResponse.scope !== undefined && {
|
|
497
|
+
scope: tokenResponse.scope
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
// Fetch user email
|
|
501
|
+
const email = await this.fetchUserEmailFromToken(tokenResponse.access_token);
|
|
502
|
+
// Store token
|
|
503
|
+
await setToken(tokenStore, {
|
|
504
|
+
accountId: email,
|
|
505
|
+
service
|
|
506
|
+
}, cachedToken);
|
|
507
|
+
// Add account and set as active
|
|
508
|
+
await addAccount(tokenStore, {
|
|
509
|
+
service,
|
|
510
|
+
accountId: email
|
|
511
|
+
});
|
|
512
|
+
await setActiveAccount(tokenStore, {
|
|
513
|
+
service,
|
|
514
|
+
accountId: email
|
|
515
|
+
});
|
|
516
|
+
// Store account metadata
|
|
517
|
+
await setAccountInfo(tokenStore, {
|
|
518
|
+
service,
|
|
519
|
+
accountId: email
|
|
520
|
+
}, {
|
|
521
|
+
email,
|
|
522
|
+
addedAt: new Date().toISOString()
|
|
523
|
+
});
|
|
524
|
+
// Clean up pending auth
|
|
525
|
+
await tokenStore.delete(pendingKey);
|
|
526
|
+
logger.info('OAuth callback completed', {
|
|
527
|
+
service,
|
|
528
|
+
email
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
email,
|
|
532
|
+
token: cachedToken
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
518
536
|
* Create auth middleware for single-user context (single active account per service)
|
|
519
537
|
*
|
|
520
538
|
* Single-user mode:
|