1
1
import pkceChallenge from "pkce-challenge" ;
2
- import { LATEST_PROTOCOL_VERSION } from "../types.js" ;
3
- import type { OAuthClientMetadata , OAuthClientInformation , OAuthTokens , OAuthMetadata , OAuthClientInformationFull } from "../shared/auth.js" ;
4
- import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
2
+ import { LATEST_PROTOCOL_VERSION } from "../types.js" ;
3
+ import type {
4
+ OAuthClientInformation ,
5
+ OAuthClientInformationFull ,
6
+ OAuthClientMetadata ,
7
+ OAuthMetadata ,
8
+ OAuthTokens
9
+ } from "../shared/auth.js" ;
10
+ import {
11
+ OAuthClientInformationFullSchema ,
12
+ OAuthErrorResponseSchema ,
13
+ OAuthMetadataSchema ,
14
+ OAuthTokensSchema
15
+ } from "../shared/auth.js" ;
16
+ import {
17
+ InvalidClientError ,
18
+ InvalidGrantError ,
19
+ OAUTH_ERRORS ,
20
+ OAuthError ,
21
+ ServerError ,
22
+ UnauthorizedClientError
23
+ } from "../server/auth/errors.js" ;
5
24
6
25
/**
7
26
* Implements an end-to-end OAuth client to be used with one MCP server.
@@ -66,6 +85,13 @@ export interface OAuthClientProvider {
66
85
* the authorization result.
67
86
*/
68
87
codeVerifier ( ) : string | Promise < string > ;
88
+
89
+ /**
90
+ * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
91
+ * credentials, in the case where the server has indicated that they are no longer valid.
92
+ * This avoids requiring the user to intervene manually.
93
+ */
94
+ invalidateCredentials ?( scope : 'all' | 'client' | 'tokens' | 'verifier' ) : void | Promise < void > ;
69
95
}
70
96
71
97
export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -76,6 +102,33 @@ export class UnauthorizedError extends Error {
76
102
}
77
103
}
78
104
105
+ /**
106
+ * Parses an OAuth error response from a string or Response object.
107
+ *
108
+ * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
109
+ * and an instance of the appropriate OAuthError subclass will be returned.
110
+ * If parsing fails, it falls back to a generic ServerError that includes
111
+ * the response status (if available) and original content.
112
+ *
113
+ * @param input - A Response object or string containing the error response
114
+ * @returns A Promise that resolves to an OAuthError instance
115
+ */
116
+ export async function parseErrorResponse ( input : Response | string ) : Promise < OAuthError > {
117
+ const statusCode = input instanceof Response ? input . status : undefined ;
118
+ const body = input instanceof Response ? await input . text ( ) : input ;
119
+
120
+ try {
121
+ const result = OAuthErrorResponseSchema . parse ( JSON . parse ( body ) ) ;
122
+ const { error, error_description, error_uri } = result ;
123
+ const errorClass = OAUTH_ERRORS [ error ] || ServerError ;
124
+ return new errorClass ( error_description || '' , error_uri ) ;
125
+ } catch ( error ) {
126
+ // Not a valid OAuth error response, but try to inform the user of the raw data anyway
127
+ const errorMessage = `${ statusCode ? `HTTP ${ statusCode } : ` : '' } Invalid OAuth error response: ${ error } . Raw body: ${ body } ` ;
128
+ return new ServerError ( errorMessage ) ;
129
+ }
130
+ }
131
+
79
132
/**
80
133
* Orchestrates the full auth flow with a server.
81
134
*
@@ -84,7 +137,30 @@ export class UnauthorizedError extends Error {
84
137
*/
85
138
export async function auth (
86
139
provider : OAuthClientProvider ,
87
- { serverUrl, authorizationCode } : { serverUrl : string | URL , authorizationCode ?: string } ) : Promise < AuthResult > {
140
+ options : { serverUrl : string | URL , authorizationCode ?: string } ,
141
+ ) : Promise < AuthResult > {
142
+ try {
143
+ return await authInternal ( provider , options ) ;
144
+ } catch ( error ) {
145
+ // Handle recoverable error types by invalidating credentials and retrying
146
+ if ( error instanceof InvalidClientError || error instanceof UnauthorizedClientError ) {
147
+ await provider . invalidateCredentials ?.( 'all' ) ;
148
+ return await authInternal ( provider , options ) ;
149
+ } else if ( error instanceof InvalidGrantError ) {
150
+ await provider . invalidateCredentials ?.( 'tokens' ) ;
151
+ return await authInternal ( provider , options ) ;
152
+ }
153
+
154
+ // Throw otherwise
155
+ throw error
156
+ }
157
+ }
158
+
159
+ async function authInternal (
160
+ provider : OAuthClientProvider ,
161
+ options : { serverUrl : string | URL , authorizationCode ?: string } ,
162
+ ) : Promise < AuthResult > {
163
+ const { serverUrl, authorizationCode } = options ;
88
164
const metadata = await discoverOAuthMetadata ( serverUrl ) ;
89
165
90
166
// Handle client registration if needed
@@ -119,7 +195,7 @@ export async function auth(
119
195
} ) ;
120
196
121
197
await provider . saveTokens ( tokens ) ;
122
- return "AUTHORIZED" ;
198
+ return "AUTHORIZED"
123
199
}
124
200
125
201
const tokens = await provider . tokens ( ) ;
@@ -135,14 +211,20 @@ export async function auth(
135
211
} ) ;
136
212
137
213
await provider . saveTokens ( newTokens ) ;
138
- return "AUTHORIZED" ;
214
+ return "AUTHORIZED"
139
215
} catch ( error ) {
140
- console . error ( "Could not refresh OAuth tokens:" , error ) ;
216
+ // 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.
217
+ if ( ! ( error instanceof OAuthError ) || error instanceof ServerError ) {
218
+ console . error ( "Could not refresh OAuth tokens:" , error ) ;
219
+ } else {
220
+ console . warn ( `OAuth token refresh failed: ${ JSON . stringify ( error . toResponseObject ( ) ) } ` ) ;
221
+ throw error ;
222
+ }
141
223
}
142
224
}
143
225
144
226
// Start new authorization flow
145
- const { authorizationUrl, codeVerifier } = await startAuthorization ( serverUrl , {
227
+ const { authorizationUrl, codeVerifier} = await startAuthorization ( serverUrl , {
146
228
metadata,
147
229
clientInformation,
148
230
redirectUrl : provider . redirectUrl ,
@@ -151,7 +233,7 @@ export async function auth(
151
233
152
234
await provider . saveCodeVerifier ( codeVerifier ) ;
153
235
await provider . redirectToAuthorization ( authorizationUrl ) ;
154
- return "REDIRECT" ;
236
+ return "REDIRECT"
155
237
}
156
238
157
239
/**
@@ -316,7 +398,7 @@ export async function exchangeAuthorization(
316
398
} ) ;
317
399
318
400
if ( ! response . ok ) {
319
- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
401
+ throw await parseErrorResponse ( response ) ;
320
402
}
321
403
322
404
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -375,7 +457,7 @@ export async function refreshAuthorization(
375
457
} ) ;
376
458
377
459
if ( ! response . ok ) {
378
- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
460
+ throw await parseErrorResponse ( response ) ;
379
461
}
380
462
381
463
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -415,7 +497,7 @@ export async function registerClient(
415
497
} ) ;
416
498
417
499
if ( ! response . ok ) {
418
- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
500
+ throw await parseErrorResponse ( response ) ;
419
501
}
420
502
421
503
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments