11import pkceChallenge from "pkce-challenge" ;
22import { LATEST_PROTOCOL_VERSION } from "../types.js" ;
3- import type { OAuthClientMetadata , OAuthClientInformation , OAuthTokens , OAuthMetadata , OAuthClientInformationFull , OAuthProtectedResourceMetadata } from "../shared/auth.js" ;
3+ import {
4+ OAuthClientMetadata ,
5+ OAuthClientInformation ,
6+ OAuthTokens ,
7+ OAuthMetadata ,
8+ OAuthClientInformationFull ,
9+ OAuthProtectedResourceMetadata ,
10+ OAuthErrorResponseSchema
11+ } from "../shared/auth.js" ;
412import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthProtectedResourceMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
513import { checkResourceAllowed , resourceUrlFromServerUrl } from "../shared/auth-utils.js" ;
14+ import {
15+ InvalidClientError ,
16+ InvalidGrantError ,
17+ OAUTH_ERRORS ,
18+ OAuthError ,
19+ ServerError ,
20+ UnauthorizedClientError
21+ } from "../server/auth/errors.js" ;
622
723/**
824 * Implements an end-to-end OAuth client to be used with one MCP server.
@@ -101,6 +117,13 @@ export interface OAuthClientProvider {
101117 * Implementations must verify the returned resource matches the MCP server.
102118 */
103119 validateResourceURL ?( serverUrl : string | URL , resource ?: string ) : Promise < URL | undefined > ;
120+
121+ /**
122+ * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
123+ * credentials, in the case where the server has indicated that they are no longer valid.
124+ * This avoids requiring the user to intervene manually.
125+ */
126+ invalidateCredentials ?( scope : 'all' | 'client' | 'tokens' | 'verifier' ) : void | Promise < void > ;
104127}
105128
106129export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -219,13 +242,65 @@ function applyPublicAuth(clientId: string, params: URLSearchParams): void {
219242 params . set ( "client_id" , clientId ) ;
220243}
221244
245+ /**
246+ * Parses an OAuth error response from a string or Response object.
247+ *
248+ * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
249+ * and an instance of the appropriate OAuthError subclass will be returned.
250+ * If parsing fails, it falls back to a generic ServerError that includes
251+ * the response status (if available) and original content.
252+ *
253+ * @param input - A Response object or string containing the error response
254+ * @returns A Promise that resolves to an OAuthError instance
255+ */
256+ export async function parseErrorResponse ( input : Response | string ) : Promise < OAuthError > {
257+ const statusCode = input instanceof Response ? input . status : undefined ;
258+ const body = input instanceof Response ? await input . text ( ) : input ;
259+
260+ try {
261+ const result = OAuthErrorResponseSchema . parse ( JSON . parse ( body ) ) ;
262+ const { error, error_description, error_uri } = result ;
263+ const errorClass = OAUTH_ERRORS [ error ] || ServerError ;
264+ return new errorClass ( error_description || '' , error_uri ) ;
265+ } catch ( error ) {
266+ // Not a valid OAuth error response, but try to inform the user of the raw data anyway
267+ const errorMessage = `${ statusCode ? `HTTP ${ statusCode } : ` : '' } Invalid OAuth error response: ${ error } . Raw body: ${ body } ` ;
268+ return new ServerError ( errorMessage ) ;
269+ }
270+ }
271+
222272/**
223273 * Orchestrates the full auth flow with a server.
224274 *
225275 * This can be used as a single entry point for all authorization functionality,
226276 * instead of linking together the other lower-level functions in this module.
227277 */
228278export async function auth (
279+ provider : OAuthClientProvider ,
280+ options : {
281+ serverUrl : string | URL ;
282+ authorizationCode ?: string ;
283+ scope ?: string ;
284+ resourceMetadataUrl ?: URL } ) : Promise < AuthResult > {
285+
286+ try {
287+ return await authInternal ( provider , options ) ;
288+ } catch ( error ) {
289+ // Handle recoverable error types by invalidating credentials and retrying
290+ if ( error instanceof InvalidClientError || error instanceof UnauthorizedClientError ) {
291+ await provider . invalidateCredentials ?.( 'all' ) ;
292+ return await authInternal ( provider , options ) ;
293+ } else if ( error instanceof InvalidGrantError ) {
294+ await provider . invalidateCredentials ?.( 'tokens' ) ;
295+ return await authInternal ( provider , options ) ;
296+ }
297+
298+ // Throw otherwise
299+ throw error
300+ }
301+ }
302+
303+ async function authInternal (
229304 provider : OAuthClientProvider ,
230305 { serverUrl,
231306 authorizationCode,
@@ -289,7 +364,7 @@ export async function auth(
289364 } ) ;
290365
291366 await provider . saveTokens ( tokens ) ;
292- return "AUTHORIZED" ;
367+ return "AUTHORIZED"
293368 }
294369
295370 const tokens = await provider . tokens ( ) ;
@@ -307,9 +382,15 @@ export async function auth(
307382 } ) ;
308383
309384 await provider . saveTokens ( newTokens ) ;
310- return "AUTHORIZED" ;
311- } catch {
312- // Could not refresh OAuth tokens
385+ return "AUTHORIZED"
386+ } catch ( error ) {
387+ // If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
388+ if ( ! ( error instanceof OAuthError ) || error instanceof ServerError ) {
389+ // Could not refresh OAuth tokens
390+ } else {
391+ // Refresh failed for another reason, re-throw
392+ throw error ;
393+ }
313394 }
314395 }
315396
@@ -327,7 +408,7 @@ export async function auth(
327408
328409 await provider . saveCodeVerifier ( codeVerifier ) ;
329410 await provider . redirectToAuthorization ( authorizationUrl ) ;
330- return "REDIRECT" ;
411+ return "REDIRECT"
331412}
332413
333414export async function selectResourceURL ( serverUrl : string | URL , provider : OAuthClientProvider , resourceMetadata ?: OAuthProtectedResourceMetadata ) : Promise < URL | undefined > {
@@ -707,7 +788,7 @@ export async function exchangeAuthorization(
707788 } ) ;
708789
709790 if ( ! response . ok ) {
710- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
791+ throw await parseErrorResponse ( response ) ;
711792 }
712793
713794 return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -788,7 +869,7 @@ export async function refreshAuthorization(
788869 body : params ,
789870 } ) ;
790871 if ( ! response . ok ) {
791- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
872+ throw await parseErrorResponse ( response ) ;
792873 }
793874
794875 return OAuthTokensSchema . parse ( { refresh_token : refreshToken , ...( await response . json ( ) ) } ) ;
@@ -828,7 +909,7 @@ export async function registerClient(
828909 } ) ;
829910
830911 if ( ! response . ok ) {
831- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
912+ throw await parseErrorResponse ( response ) ;
832913 }
833914
834915 return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments