MCP-WebGPU / src /services /BrowserOAuthProvider.ts
shreyask's picture
feat: implement BrowserOAuthProvider for handling OAuth flows and enhance MCPClientService with OAuth support
83c3226 verified
/**
* 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;
}
}