1
1
import pkceChallenge from "pkce-challenge" ;
2
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" ;
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,74 +137,102 @@ 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 > {
88
- const metadata = await discoverOAuthMetadata ( serverUrl ) ;
89
-
90
- // Handle client registration if needed
91
- let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
92
- if ( ! clientInformation ) {
93
- if ( authorizationCode !== undefined ) {
94
- throw new Error ( "Existing OAuth client information is required when exchanging an authorization code" ) ;
95
- }
96
-
97
- if ( ! provider . saveClientInformation ) {
98
- throw new Error ( "OAuth client information must be saveable for dynamic registration" ) ;
99
- }
100
-
101
- const fullInformation = await registerClient ( serverUrl , {
102
- metadata,
103
- clientMetadata : provider . clientMetadata ,
104
- } ) ;
140
+ options : { serverUrl : string | URL , authorizationCode ?: string } ,
141
+ lastError ?: Error
142
+ ) : Promise < AuthResult > {
143
+ const { serverUrl, authorizationCode } = options
144
+ try {
145
+ const metadata = await discoverOAuthMetadata ( serverUrl ) ;
105
146
106
- await provider . saveClientInformation ( fullInformation ) ;
107
- clientInformation = fullInformation ;
108
- }
147
+ // Handle client registration if needed
148
+ let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
149
+ if ( ! clientInformation ) {
150
+ if ( authorizationCode !== undefined ) {
151
+ throw new Error ( "Existing OAuth client information is required when exchanging an authorization code" ) ;
152
+ }
109
153
110
- // Exchange authorization code for tokens
111
- if ( authorizationCode !== undefined ) {
112
- const codeVerifier = await provider . codeVerifier ( ) ;
113
- const tokens = await exchangeAuthorization ( serverUrl , {
114
- metadata,
115
- clientInformation,
116
- authorizationCode,
117
- codeVerifier,
118
- redirectUri : provider . redirectUrl ,
119
- } ) ;
154
+ if ( ! provider . saveClientInformation ) {
155
+ throw new Error ( "OAuth client information must be saveable for dynamic registration" ) ;
156
+ }
120
157
121
- await provider . saveTokens ( tokens ) ;
122
- return "AUTHORIZED" ;
123
- }
158
+ const fullInformation = await registerClient ( serverUrl , {
159
+ metadata,
160
+ clientMetadata : provider . clientMetadata ,
161
+ } ) ;
124
162
125
- const tokens = await provider . tokens ( ) ;
163
+ await provider . saveClientInformation ( fullInformation ) ;
164
+ clientInformation = fullInformation ;
165
+ }
126
166
127
- // Handle token refresh or new authorization
128
- if ( tokens ?. refresh_token ) {
129
- try {
130
- // Attempt to refresh the token
131
- const newTokens = await refreshAuthorization ( serverUrl , {
167
+ // Exchange authorization code for tokens
168
+ if ( authorizationCode !== undefined ) {
169
+ const codeVerifier = await provider . codeVerifier ( ) ;
170
+ const tokens = await exchangeAuthorization ( serverUrl , {
132
171
metadata,
133
172
clientInformation,
134
- refreshToken : tokens . refresh_token ,
173
+ authorizationCode,
174
+ codeVerifier,
175
+ redirectUri : provider . redirectUrl ,
135
176
} ) ;
136
177
137
- await provider . saveTokens ( newTokens ) ;
178
+ await provider . saveTokens ( tokens ) ;
138
179
return "AUTHORIZED" ;
139
- } catch ( error ) {
140
- console . error ( "Could not refresh OAuth tokens:" , error ) ;
141
180
}
142
- }
143
181
144
- // Start new authorization flow
145
- const { authorizationUrl, codeVerifier } = await startAuthorization ( serverUrl , {
146
- metadata,
147
- clientInformation,
148
- redirectUrl : provider . redirectUrl ,
149
- scope : provider . clientMetadata . scope
150
- } ) ;
182
+ const tokens = await provider . tokens ( ) ;
183
+
184
+ // Handle token refresh or new authorization
185
+ if ( tokens ?. refresh_token ) {
186
+ try {
187
+ // Attempt to refresh the token
188
+ const newTokens = await refreshAuthorization ( serverUrl , {
189
+ metadata,
190
+ clientInformation,
191
+ refreshToken : tokens . refresh_token ,
192
+ } ) ;
193
+
194
+ await provider . saveTokens ( newTokens ) ;
195
+ return "AUTHORIZED" ;
196
+ } catch ( error ) {
197
+ // 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.
198
+ if ( ! ( error instanceof OAuthError ) || error instanceof ServerError ) {
199
+ console . error ( "Could not refresh OAuth tokens:" , error ) ;
200
+ } else {
201
+ console . warn ( `OAuth token refresh failed: ${ JSON . stringify ( error . toResponseObject ( ) ) } ` ) ;
202
+ throw error
203
+ }
204
+ }
205
+ }
151
206
152
- await provider . saveCodeVerifier ( codeVerifier ) ;
153
- await provider . redirectToAuthorization ( authorizationUrl ) ;
154
- return "REDIRECT" ;
207
+ // Start new authorization flow
208
+ const { authorizationUrl, codeVerifier} = await startAuthorization ( serverUrl , {
209
+ metadata,
210
+ clientInformation,
211
+ redirectUrl : provider . redirectUrl ,
212
+ scope : provider . clientMetadata . scope
213
+ } ) ;
214
+
215
+ await provider . saveCodeVerifier ( codeVerifier ) ;
216
+ await provider . redirectToAuthorization ( authorizationUrl ) ;
217
+ return "REDIRECT" ;
218
+ } catch ( error ) {
219
+ switch ( ( error as Error ) . constructor ) {
220
+ // Don't loop forever if the same type of error recurs
221
+ case lastError ?. constructor :
222
+ throw error ;
223
+ // Invalid clients mean the entire local state is now invalid, so clear it all then retry
224
+ case InvalidClientError :
225
+ case UnauthorizedClientError :
226
+ await provider . invalidateCredentials ?.( 'all' )
227
+ return await auth ( provider , options , error as Error )
228
+ // Invalid grants mean clear the tokens and retry
229
+ case InvalidGrantError :
230
+ await provider . invalidateCredentials ?.( 'tokens' )
231
+ return await auth ( provider , options , error as Error )
232
+ default :
233
+ throw error
234
+ }
235
+ }
155
236
}
156
237
157
238
/**
@@ -316,7 +397,7 @@ export async function exchangeAuthorization(
316
397
} ) ;
317
398
318
399
if ( ! response . ok ) {
319
- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
400
+ throw await parseErrorResponse ( response ) ;
320
401
}
321
402
322
403
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -375,7 +456,7 @@ export async function refreshAuthorization(
375
456
} ) ;
376
457
377
458
if ( ! response . ok ) {
378
- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
459
+ throw await parseErrorResponse ( response ) ;
379
460
}
380
461
381
462
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -415,7 +496,7 @@ export async function registerClient(
415
496
} ) ;
416
497
417
498
if ( ! response . ok ) {
418
- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
499
+ throw await parseErrorResponse ( response ) ;
419
500
}
420
501
421
502
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments