1
1
import pkceChallenge from "pkce-challenge" ;
2
2
import { 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" ;
4
12
import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthProtectedResourceMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
5
13
import { 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" ;
6
22
7
23
/**
8
24
* Implements an end-to-end OAuth client to be used with one MCP server.
@@ -101,6 +117,13 @@ export interface OAuthClientProvider {
101
117
* Implementations must verify the returned resource matches the MCP server.
102
118
*/
103
119
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 > ;
104
127
}
105
128
106
129
export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -219,13 +242,65 @@ function applyPublicAuth(clientId: string, params: URLSearchParams): void {
219
242
params . set ( "client_id" , clientId ) ;
220
243
}
221
244
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
+
222
272
/**
223
273
* Orchestrates the full auth flow with a server.
224
274
*
225
275
* This can be used as a single entry point for all authorization functionality,
226
276
* instead of linking together the other lower-level functions in this module.
227
277
*/
228
278
export 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 (
229
304
provider : OAuthClientProvider ,
230
305
{ serverUrl,
231
306
authorizationCode,
@@ -289,7 +364,7 @@ export async function auth(
289
364
} ) ;
290
365
291
366
await provider . saveTokens ( tokens ) ;
292
- return "AUTHORIZED" ;
367
+ return "AUTHORIZED"
293
368
}
294
369
295
370
const tokens = await provider . tokens ( ) ;
@@ -307,9 +382,15 @@ export async function auth(
307
382
} ) ;
308
383
309
384
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
+ }
313
394
}
314
395
}
315
396
@@ -327,7 +408,7 @@ export async function auth(
327
408
328
409
await provider . saveCodeVerifier ( codeVerifier ) ;
329
410
await provider . redirectToAuthorization ( authorizationUrl ) ;
330
- return "REDIRECT" ;
411
+ return "REDIRECT"
331
412
}
332
413
333
414
export async function selectResourceURL ( serverUrl : string | URL , provider : OAuthClientProvider , resourceMetadata ?: OAuthProtectedResourceMetadata ) : Promise < URL | undefined > {
@@ -707,7 +788,7 @@ export async function exchangeAuthorization(
707
788
} ) ;
708
789
709
790
if ( ! response . ok ) {
710
- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
791
+ throw await parseErrorResponse ( response ) ;
711
792
}
712
793
713
794
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -788,7 +869,7 @@ export async function refreshAuthorization(
788
869
body : params ,
789
870
} ) ;
790
871
if ( ! response . ok ) {
791
- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
872
+ throw await parseErrorResponse ( response ) ;
792
873
}
793
874
794
875
return OAuthTokensSchema . parse ( { refresh_token : refreshToken , ...( await response . json ( ) ) } ) ;
@@ -828,7 +909,7 @@ export async function registerClient(
828
909
} ) ;
829
910
830
911
if ( ! response . ok ) {
831
- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
912
+ throw await parseErrorResponse ( response ) ;
832
913
}
833
914
834
915
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments