Spaces:
Running
Running
File size: 6,086 Bytes
83c3226 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
/**
* 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;
}
}
|