Spaces:
Running
Running
| /** | |
| * 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<OAuthClientInformationMixed | undefined> { | |
| 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<void> { | |
| 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<OAuthTokens | undefined> { | |
| 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<void> { | |
| 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<void> { | |
| 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<URLSearchParams> { | |
| 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; | |
| } | |
| } | |