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.
@@ -81,6 +97,13 @@ export interface OAuthClientProvider {
81
97
* Implementations must verify the returned resource matches the MCP server.
82
98
*/
83
99
validateResourceURL ?( serverUrl : string | URL , resource ?: string ) : Promise < URL | undefined > ;
100
+
101
+ /**
102
+ * If implemented, provides a way for the client to invalidate (e.g. delete) the specified
103
+ * credentials, in the case where the server has indicated that they are no longer valid.
104
+ * This avoids requiring the user to intervene manually.
105
+ */
106
+ invalidateCredentials ?( scope : 'all' | 'client' | 'tokens' | 'verifier' ) : void | Promise < void > ;
84
107
}
85
108
86
109
export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -91,13 +114,65 @@ export class UnauthorizedError extends Error {
91
114
}
92
115
}
93
116
117
+ /**
118
+ * Parses an OAuth error response from a string or Response object.
119
+ *
120
+ * If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
121
+ * and an instance of the appropriate OAuthError subclass will be returned.
122
+ * If parsing fails, it falls back to a generic ServerError that includes
123
+ * the response status (if available) and original content.
124
+ *
125
+ * @param input - A Response object or string containing the error response
126
+ * @returns A Promise that resolves to an OAuthError instance
127
+ */
128
+ export async function parseErrorResponse ( input : Response | string ) : Promise < OAuthError > {
129
+ const statusCode = input instanceof Response ? input . status : undefined ;
130
+ const body = input instanceof Response ? await input . text ( ) : input ;
131
+
132
+ try {
133
+ const result = OAuthErrorResponseSchema . parse ( JSON . parse ( body ) ) ;
134
+ const { error, error_description, error_uri } = result ;
135
+ const errorClass = OAUTH_ERRORS [ error ] || ServerError ;
136
+ return new errorClass ( error_description || '' , error_uri ) ;
137
+ } catch ( error ) {
138
+ // Not a valid OAuth error response, but try to inform the user of the raw data anyway
139
+ const errorMessage = `${ statusCode ? `HTTP ${ statusCode } : ` : '' } Invalid OAuth error response: ${ error } . Raw body: ${ body } ` ;
140
+ return new ServerError ( errorMessage ) ;
141
+ }
142
+ }
143
+
94
144
/**
95
145
* Orchestrates the full auth flow with a server.
96
146
*
97
147
* This can be used as a single entry point for all authorization functionality,
98
148
* instead of linking together the other lower-level functions in this module.
99
149
*/
100
150
export async function auth (
151
+ provider : OAuthClientProvider ,
152
+ options : {
153
+ serverUrl : string | URL ;
154
+ authorizationCode ?: string ;
155
+ scope ?: string ;
156
+ resourceMetadataUrl ?: URL } ) : Promise < AuthResult > {
157
+
158
+ try {
159
+ return await authInternal ( provider , options ) ;
160
+ } catch ( error ) {
161
+ // Handle recoverable error types by invalidating credentials and retrying
162
+ if ( error instanceof InvalidClientError || error instanceof UnauthorizedClientError ) {
163
+ await provider . invalidateCredentials ?.( 'all' ) ;
164
+ return await authInternal ( provider , options ) ;
165
+ } else if ( error instanceof InvalidGrantError ) {
166
+ await provider . invalidateCredentials ?.( 'tokens' ) ;
167
+ return await authInternal ( provider , options ) ;
168
+ }
169
+
170
+ // Throw otherwise
171
+ throw error
172
+ }
173
+ }
174
+
175
+ async function authInternal (
101
176
provider : OAuthClientProvider ,
102
177
{ serverUrl,
103
178
authorizationCode,
@@ -157,7 +232,7 @@ export async function auth(
157
232
} ) ;
158
233
159
234
await provider . saveTokens ( tokens ) ;
160
- return "AUTHORIZED" ;
235
+ return "AUTHORIZED"
161
236
}
162
237
163
238
const tokens = await provider . tokens ( ) ;
@@ -174,9 +249,15 @@ export async function auth(
174
249
} ) ;
175
250
176
251
await provider . saveTokens ( newTokens ) ;
177
- return "AUTHORIZED" ;
178
- } catch {
179
- // Could not refresh OAuth tokens
252
+ return "AUTHORIZED"
253
+ } catch ( error ) {
254
+ // 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.
255
+ if ( ! ( error instanceof OAuthError ) || error instanceof ServerError ) {
256
+ // Could not refresh OAuth tokens
257
+ } else {
258
+ // Refresh failed for another reason, re-throw
259
+ throw error ;
260
+ }
180
261
}
181
262
}
182
263
@@ -194,7 +275,7 @@ export async function auth(
194
275
195
276
await provider . saveCodeVerifier ( codeVerifier ) ;
196
277
await provider . redirectToAuthorization ( authorizationUrl ) ;
197
- return "REDIRECT" ;
278
+ return "REDIRECT"
198
279
}
199
280
200
281
export async function selectResourceURL ( serverUrl : string | URL , provider : OAuthClientProvider , resourceMetadata ?: OAuthProtectedResourceMetadata ) : Promise < URL | undefined > {
@@ -523,7 +604,7 @@ export async function exchangeAuthorization(
523
604
} ) ;
524
605
525
606
if ( ! response . ok ) {
526
- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
607
+ throw await parseErrorResponse ( response ) ;
527
608
}
528
609
529
610
return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -587,7 +668,7 @@ export async function refreshAuthorization(
587
668
body : params ,
588
669
} ) ;
589
670
if ( ! response . ok ) {
590
- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
671
+ throw await parseErrorResponse ( response ) ;
591
672
}
592
673
593
674
return OAuthTokensSchema . parse ( { refresh_token : refreshToken , ...( await response . json ( ) ) } ) ;
@@ -627,7 +708,7 @@ export async function registerClient(
627
708
} ) ;
628
709
629
710
if ( ! response . ok ) {
630
- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
711
+ throw await parseErrorResponse ( response ) ;
631
712
}
632
713
633
714
return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments