/** * Browser-based OAuth Client Provider for MCP * * Implements the OAuthClientProvider interface from @modelcontextprotocol/sdk * to handle user-facing OAuth flows in a browser environment. */ import type { OAuthClientMetadata, OAuthClientInformationMixed, OAuthTokens, } from "@modelcontextprotocol/sdk/shared/auth.js"; import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; import { secureStorage } from "../utils/storage"; import { STORAGE_KEYS } from "../config/constants"; export interface BrowserOAuthProviderOptions { /** * The redirect URI for OAuth callbacks */ redirectUri: string; /** * Optional client name for metadata */ clientName?: string; /** * Optional scopes to request */ scopes?: string[]; } /** * OAuth provider for browser-based authorization code flow with PKCE. * * This provider handles: * - Dynamic client registration * - PKCE code verifier management * - Token storage in localStorage/secure storage * - Browser redirects for authorization * * @example * const provider = new BrowserOAuthProvider({ * redirectUri: window.location.origin + "/#/oauth/callback", * clientName: "MCP WebGPU Client", * scopes: ["read", "write"] * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { * authProvider: provider * }); */ export class BrowserOAuthProvider implements OAuthClientProvider { private _redirectUri: string; private _clientName: string; private _scopes: string[]; constructor(options: BrowserOAuthProviderOptions) { this._redirectUri = options.redirectUri; this._clientName = options.clientName || "MCP WebGPU Client"; this._scopes = options.scopes || []; } get redirectUrl(): string { return this._redirectUri; } get clientMetadata(): OAuthClientMetadata { return { client_name: this._clientName, redirect_uris: [this._redirectUri], grant_types: ["authorization_code"], response_types: ["code"], token_endpoint_auth_method: "none", // Public client (browser-based) }; } /** * Load saved client information from localStorage */ async clientInformation(): Promise { const clientId = localStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_ID); if (!clientId) { return undefined; } const clientSecret = await secureStorage.getItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET); return { client_id: clientId, ...(clientSecret ? { client_secret: clientSecret } : {}), }; } /** * Save client information after dynamic registration */ async saveClientInformation(info: OAuthClientInformationMixed): Promise { localStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_ID, info.client_id); if ("client_secret" in info && info.client_secret) { await secureStorage.setItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET, info.client_secret); } } /** * Load saved OAuth tokens */ async tokens(): Promise { const accessToken = await secureStorage.getItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN); if (!accessToken) { return undefined; } const refreshToken = await secureStorage.getItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN); return { access_token: accessToken, token_type: "Bearer", ...(refreshToken ? { refresh_token: refreshToken } : {}), }; } /** * Save OAuth tokens after successful authorization */ async saveTokens(tokens: OAuthTokens): Promise { await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token); if (tokens.refresh_token) { await secureStorage.setItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN, tokens.refresh_token); } } /** * Redirect browser to authorization URL */ redirectToAuthorization(authorizationUrl: URL): void { window.location.href = authorizationUrl.toString(); } /** * Save PKCE code verifier before redirect */ saveCodeVerifier(codeVerifier: string): void { localStorage.setItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER, codeVerifier); } /** * Load PKCE code verifier for token exchange */ codeVerifier(): string { const verifier = localStorage.getItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER); if (!verifier) { throw new Error("No code verifier found in storage"); } return verifier; } /** * Generate OAuth state parameter for CSRF protection */ state(): string { const state = crypto.randomUUID(); localStorage.setItem(STORAGE_KEYS.OAUTH_STATE, state); return state; } /** * Invalidate stored credentials */ async invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier'): Promise { switch (scope) { case 'all': localStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_ID); await secureStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET); await secureStorage.removeItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN); await secureStorage.removeItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN); localStorage.removeItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER); localStorage.removeItem(STORAGE_KEYS.OAUTH_STATE); break; case 'client': localStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_ID); await secureStorage.removeItem(STORAGE_KEYS.OAUTH_CLIENT_SECRET); break; case 'tokens': await secureStorage.removeItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN); await secureStorage.removeItem(STORAGE_KEYS.OAUTH_REFRESH_TOKEN); break; case 'verifier': localStorage.removeItem(STORAGE_KEYS.OAUTH_CODE_VERIFIER); break; } } /** * Prepare token request parameters for authorization code exchange */ async prepareTokenRequest(): Promise { const params = new URLSearchParams({ grant_type: "authorization_code", redirect_uri: this._redirectUri, }); if (this._scopes.length > 0) { params.set("scope", this._scopes.join(" ")); } return params; } }