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" ;
13
+ import {
14
+ InvalidClientError ,
15
+ InvalidGrantError ,
16
+ OAUTH_ERRORS ,
17
+ OAuthError ,
18
+ ServerError ,
19
+ UnauthorizedClientError
20
+ } from "../server/auth/errors.js" ;
5
21
6
22
/**
7
23
* Implements an end-to-end OAuth client to be used with one MCP server.
@@ -71,6 +87,13 @@ export interface OAuthClientProvider {
71
87
* the authorization result.
72
88
*/
73
89
codeVerifier ( ) : string | Promise < string > ;
90
+
91
+ /**
92
+ * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
93
+ * credentials, in the case where the server has indicated that they are no longer valid.
94
+ * This avoids requiring the user to intervene manually.
95
+ */
96
+ invalidateCredentials ?( scope : 'all' | 'client' | 'tokens' | 'verifier' ) : void | Promise < void > ;
74
97
}
75
98
76
99
export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -81,13 +104,65 @@ export class UnauthorizedError extends Error {
81
104
}
82
105
}
83
106
107
+ /**
108
+ * Parses an OAuth error response from a string or Response object.
109
+ *
110
+ * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
111
+ * and an instance of the appropriate OAuthError subclass will be returned.
112
+ * If parsing fails, it falls back to a generic ServerError that includes
113
+ * the response status (if available) and original content.
114
+ *
115
+ * @param input - A Response object or string containing the error response
116
+ * @returns A Promise that resolves to an OAuthError instance
117
+ */
118
+ export async function parseErrorResponse ( input : Response | string ) : Promise < OAuthError > {
119
+ const statusCode = input instanceof Response ? input . status : undefined ;
120
+ const body = input instanceof Response ? await input . text ( ) : input ;
121
+
122
+ try {
123
+ const result = OAuthErrorResponseSchema . parse ( JSON . parse ( body ) ) ;
124
+ const { error, error_description, error_uri } = result ;
125
+ const errorClass = OAUTH_ERRORS [ error ] || ServerError ;
126
+ return new errorClass ( error_description || '' , error_uri ) ;
127
+ } catch ( error ) {
128
+ // Not a valid OAuth error response, but try to inform the user of the raw data anyway
129
+ const errorMessage = `${ statusCode ? `HTTP ${ statusCode } : ` : '' } Invalid OAuth error response: ${ error } . Raw body: ${ body } ` ;
130
+ return new ServerError ( errorMessage ) ;
131
+ }
132
+ }
133
+
84
134
/**
85
135
* Orchestrates the full auth flow with a server.
86
136
*
87
137
* This can be used as a single entry point for all authorization functionality,
88
138
* instead of linking together the other lower-level functions in this module.
89
139
*/
90
140
export async function auth (
141
+ provider : OAuthClientProvider ,
142
+ options : {
143
+ serverUrl : string | URL ;
144
+ authorizationCode ?: string ;
145
+ scope ?: string ;
146
+ resourceMetadataUrl ?: URL } ) : Promise < AuthResult > {
147
+
148
+ try {
149
+ return await authInternal ( provider , options ) ;
150
+ } catch ( error ) {
151
+ // Handle recoverable error types by invalidating credentials and retrying
152
+ if ( error instanceof InvalidClientError || error instanceof UnauthorizedClientError ) {
153
+ await provider . invalidateCredentials ?.( 'all' ) ;
154
+ return await authInternal ( provider , options ) ;
155
+ } else if ( error instanceof InvalidGrantError ) {
156
+ await provider . invalidateCredentials ?.( 'tokens' ) ;
157
+ return await authInternal ( provider , options ) ;
158
+ }
159
+
160
+ // Throw otherwise
161
+ throw error
162
+ }
163
+ }
164
+
165
+ async function authInternal (
91
166
provider : OAuthClientProvider ,
92
167
{ serverUrl,
93
168
authorizationCode,
@@ -145,7 +220,7 @@ export async function auth(
145
220
} ) ;
146
221
147
222
await provider . saveTokens ( tokens ) ;
148
- return "AUTHORIZED" ;
223
+ return "AUTHORIZED"
149
224
}
150
225
151
226
const tokens = await provider . tokens ( ) ;
@@ -161,9 +236,15 @@ export async function auth(
161
236
} ) ;
162
237
163
238
await provider . saveTokens ( newTokens ) ;
164
- return "AUTHORIZED" ;
239
+ return "AUTHORIZED"
165
240
} catch ( error ) {
166
- console . error ( "Could not refresh OAuth tokens:" , error ) ;
241
+ // 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.
242
+ if ( ! ( error instanceof OAuthError ) || error instanceof ServerError ) {
243
+ console . error ( "Could not refresh OAuth tokens:" , error ) ;
244
+ } else {
245
+ console . warn ( `OAuth token refresh failed: ${ JSON . stringify ( error . toResponseObject ( ) ) } ` ) ;
246
+ throw error ;
247
+ }
167
248
}
168
249
}
169
250
@@ -180,7 +261,7 @@ export async function auth(
180
261
181
262
await provider . saveCodeVerifier ( codeVerifier ) ;
182
263
await provider . redirectToAuthorization ( authorizationUrl ) ;
183
- return "REDIRECT" ;
264
+ return "REDIRECT"
184
265
}
185
266
186
267
/**
@@ -427,7 +508,7 @@ export async function exchangeAuthorization(
427
508
} ) ;
428
509
429
510
if ( ! response . ok ) {
430
- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
511
+ throw await parseErrorResponse ( response ) ;
431
512
}
432
513
433
514
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -485,7 +566,7 @@ export async function refreshAuthorization(
485
566
body : params ,
486
567
} ) ;
487
568
if ( ! response . ok ) {
488
- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
569
+ throw await parseErrorResponse ( response ) ;
489
570
}
490
571
491
572
return OAuthTokensSchema . parse ( { refresh_token : refreshToken , ...( await response . json ( ) ) } ) ;
@@ -525,7 +606,7 @@ export async function registerClient(
525
606
} ) ;
526
607
527
608
if ( ! response . ok ) {
528
- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
609
+ throw await parseErrorResponse ( response ) ;
529
610
}
530
611
531
612
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments