11import pkceChallenge from "pkce-challenge" ;
22import { 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" ;
524
625/**
726 * Implements an end-to-end OAuth client to be used with one MCP server.
@@ -66,6 +85,13 @@ export interface OAuthClientProvider {
6685 * the authorization result.
6786 */
6887 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 > ;
6995}
7096
7197export type AuthResult = "AUTHORIZED" | "REDIRECT" ;
@@ -76,6 +102,33 @@ export class UnauthorizedError extends Error {
76102 }
77103}
78104
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+
79132/**
80133 * Orchestrates the full auth flow with a server.
81134 *
@@ -84,74 +137,102 @@ export class UnauthorizedError extends Error {
84137 */
85138export async function auth (
86139 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 ) ;
105146
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+ }
109153
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+ }
120157
121- await provider . saveTokens ( tokens ) ;
122- return "AUTHORIZED" ;
123- }
158+ const fullInformation = await registerClient ( serverUrl , {
159+ metadata,
160+ clientMetadata : provider . clientMetadata ,
161+ } ) ;
124162
125- const tokens = await provider . tokens ( ) ;
163+ await provider . saveClientInformation ( fullInformation ) ;
164+ clientInformation = fullInformation ;
165+ }
126166
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 , {
132171 metadata,
133172 clientInformation,
134- refreshToken : tokens . refresh_token ,
173+ authorizationCode,
174+ codeVerifier,
175+ redirectUri : provider . redirectUrl ,
135176 } ) ;
136177
137- await provider . saveTokens ( newTokens ) ;
178+ await provider . saveTokens ( tokens ) ;
138179 return "AUTHORIZED" ;
139- } catch ( error ) {
140- console . error ( "Could not refresh OAuth tokens:" , error ) ;
141180 }
142- }
143181
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+ }
151206
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+ }
155236}
156237
157238/**
@@ -316,7 +397,7 @@ export async function exchangeAuthorization(
316397 } ) ;
317398
318399 if ( ! response . ok ) {
319- throw new Error ( `Token exchange failed: HTTP ${ response . status } ` ) ;
400+ throw await parseErrorResponse ( response ) ;
320401 }
321402
322403 return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -375,7 +456,7 @@ export async function refreshAuthorization(
375456 } ) ;
376457
377458 if ( ! response . ok ) {
378- throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
459+ throw await parseErrorResponse ( response ) ;
379460 }
380461
381462 return OAuthTokensSchema . parse ( await response . json ( ) ) ;
@@ -415,7 +496,7 @@ export async function registerClient(
415496 } ) ;
416497
417498 if ( ! response . ok ) {
418- throw new Error ( `Dynamic client registration failed: HTTP ${ response . status } ` ) ;
499+ throw await parseErrorResponse ( response ) ;
419500 }
420501
421502 return OAuthClientInformationFullSchema . parse ( await response . json ( ) ) ;
0 commit comments