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 { OAuthClientMetadata , OAuthClientInformation , OAuthTokens , OAuthMetadata , OAuthClientInformationFull , OAuthProtectedResourceMetadata } from "../shared/auth.js" ;
4
+ import { OAuthClientInformationFullSchema , OAuthMetadataSchema , OAuthProtectedResourceMetadataSchema , OAuthTokensSchema } from "../shared/auth.js" ;
5
5
6
6
/**
7
7
* Implements an end-to-end OAuth client to be used with one MCP server.
8
- *
8
+ *
9
9
* This client relies upon a concept of an authorized "session," the exact
10
10
* meaning of which is application-defined. Tokens, authorization codes, and
11
11
* code verifiers should not cross different sessions.
@@ -32,7 +32,7 @@ export interface OAuthClientProvider {
32
32
* If implemented, this permits the OAuth client to dynamically register with
33
33
* the server. Client information saved this way should later be read via
34
34
* `clientInformation()`.
35
- *
35
+ *
36
36
* This method is not required to be implemented if client information is
37
37
* statically known (e.g., pre-registered).
38
38
*/
@@ -78,14 +78,29 @@ export class UnauthorizedError extends Error {
78
78
79
79
/**
80
80
* Orchestrates the full auth flow with a server.
81
- *
81
+ *
82
82
* This can be used as a single entry point for all authorization functionality,
83
83
* instead of linking together the other lower-level functions in this module.
84
84
*/
85
85
export async function auth (
86
86
provider : OAuthClientProvider ,
87
- { serverUrl, authorizationCode } : { serverUrl : string | URL , authorizationCode ?: string } ) : Promise < AuthResult > {
88
- const metadata = await discoverOAuthMetadata ( serverUrl ) ;
87
+ { resourceServerUrl, authorizationCode, authServerUrl } : { resourceServerUrl : string | URL , authorizationCode ?: string , authServerUrl ?: string | URL } ) : Promise < AuthResult > {
88
+
89
+ let authorizationServerUrl = authServerUrl
90
+
91
+ if ( ! authorizationServerUrl ) {
92
+ const protectedResourceMetadata = await discoverOAuthProtectedResourceMetadata ( resourceServerUrl ) ;
93
+ if ( protectedResourceMetadata . authorization_servers === undefined || protectedResourceMetadata . authorization_servers . length === 0 ) {
94
+ throw new Error ( "Server does not speicify any authorization servers." ) ;
95
+ }
96
+ authorizationServerUrl = protectedResourceMetadata . authorization_servers [ 0 ] ;
97
+ }
98
+
99
+ if ( ! authorizationServerUrl ) {
100
+ throw new Error ( "Server does not speicify any authorization servers." ) ;
101
+ }
102
+
103
+ const metadata = await discoverOAuthMetadata ( authorizationServerUrl ) ;
89
104
90
105
// Handle client registration if needed
91
106
let clientInformation = await Promise . resolve ( provider . clientInformation ( ) ) ;
@@ -98,7 +113,7 @@ export async function auth(
98
113
throw new Error ( "OAuth client information must be saveable for dynamic registration" ) ;
99
114
}
100
115
101
- const fullInformation = await registerClient ( serverUrl , {
116
+ const fullInformation = await registerClient ( authorizationServerUrl , {
102
117
metadata,
103
118
clientMetadata : provider . clientMetadata ,
104
119
} ) ;
@@ -110,7 +125,7 @@ export async function auth(
110
125
// Exchange authorization code for tokens
111
126
if ( authorizationCode !== undefined ) {
112
127
const codeVerifier = await provider . codeVerifier ( ) ;
113
- const tokens = await exchangeAuthorization ( serverUrl , {
128
+ const tokens = await exchangeAuthorization ( authorizationServerUrl , {
114
129
metadata,
115
130
clientInformation,
116
131
authorizationCode,
@@ -128,7 +143,7 @@ export async function auth(
128
143
if ( tokens ?. refresh_token ) {
129
144
try {
130
145
// Attempt to refresh the token
131
- const newTokens = await refreshAuthorization ( serverUrl , {
146
+ const newTokens = await refreshAuthorization ( authorizationServerUrl , {
132
147
metadata,
133
148
clientInformation,
134
149
refreshToken : tokens . refresh_token ,
@@ -142,7 +157,7 @@ export async function auth(
142
157
}
143
158
144
159
// Start new authorization flow
145
- const { authorizationUrl, codeVerifier } = await startAuthorization ( serverUrl , {
160
+ const { authorizationUrl, codeVerifier } = await startAuthorization ( authorizationServerUrl , {
146
161
metadata,
147
162
clientInformation,
148
163
redirectUrl : provider . redirectUrl
@@ -153,17 +168,93 @@ export async function auth(
153
168
return "REDIRECT" ;
154
169
}
155
170
171
+ /**
172
+ * Extract resource_metadata from response header.
173
+ */
174
+ export function extractResourceMetadataUrl ( res : Response ) : URL | undefined {
175
+
176
+ const authenticateHeader = res . headers . get ( "WWW-Authenticate" ) ;
177
+ if ( ! authenticateHeader ) {
178
+ return undefined ;
179
+ }
180
+
181
+ const [ type , scheme ] = authenticateHeader . split ( ' ' ) ;
182
+ if ( type . toLowerCase ( ) !== 'bearer' || ! scheme ) {
183
+ console . log ( "Invalid WWW-Authenticate header format, expected 'Bearer'" ) ;
184
+ return undefined ;
185
+ }
186
+ const regex = / r e s o u r c e _ m e t a d a t a = " ( [ ^ " ] * ) " / ;
187
+ const match = regex . exec ( authenticateHeader ) ;
188
+
189
+ if ( ! match ) {
190
+ return undefined ;
191
+ }
192
+
193
+ try {
194
+ return new URL ( match [ 1 ] ) ;
195
+ } catch ( error ) {
196
+ console . log ( "Invalid resource metadata url." ) ;
197
+ return undefined ;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Looks up RFC 9728 OAuth 2.0 Protected Resource Metadata.
203
+ *
204
+ * If the server returns a 404 for the well-known endpoint, this function will
205
+ * return `undefined`. Any other errors will be thrown as exceptions.
206
+ */
207
+ export async function discoverOAuthProtectedResourceMetadata (
208
+ resourceServerUrl : string | URL ,
209
+ opts ?: { protocolVersion ?: string , resourceMetadataUrl ?: string | URL } ,
210
+ ) : Promise < OAuthProtectedResourceMetadata > {
211
+
212
+ let url : URL
213
+ if ( opts ?. resourceMetadataUrl ) {
214
+ url = new URL ( opts ?. resourceMetadataUrl ) ;
215
+ } else {
216
+ url = new URL ( "/.well-known/oauth-protected-resource" , resourceServerUrl ) ;
217
+ }
218
+
219
+ let response : Response ;
220
+ try {
221
+ response = await fetch ( url , {
222
+ headers : {
223
+ "MCP-Protocol-Version" : opts ?. protocolVersion ?? LATEST_PROTOCOL_VERSION
224
+ }
225
+ } ) ;
226
+ } catch ( error ) {
227
+ // CORS errors come back as TypeError
228
+ if ( error instanceof TypeError ) {
229
+ response = await fetch ( url ) ;
230
+ } else {
231
+ throw error ;
232
+ }
233
+ }
234
+
235
+ if ( response . status === 404 ) {
236
+ throw new Error ( `Resource server does not implement OAuth 2.0 Protected Resource Metadata.` ) ;
237
+ }
238
+
239
+ if ( ! response . ok ) {
240
+ throw new Error (
241
+ `HTTP ${ response . status } trying to load well-known OAuth protected resource metadata.` ,
242
+ ) ;
243
+ }
244
+ return OAuthProtectedResourceMetadataSchema . parse ( await response . json ( ) ) ;
245
+ }
246
+
156
247
/**
157
248
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
158
249
*
159
250
* If the server returns a 404 for the well-known endpoint, this function will
160
251
* return `undefined`. Any other errors will be thrown as exceptions.
161
252
*/
162
253
export async function discoverOAuthMetadata (
163
- serverUrl : string | URL ,
254
+ authorizationServerUrl : string | URL ,
164
255
opts ?: { protocolVersion ?: string } ,
165
256
) : Promise < OAuthMetadata | undefined > {
166
- const url = new URL ( "/.well-known/oauth-authorization-server" , serverUrl ) ;
257
+ const url = new URL ( "/.well-known/oauth-authorization-server" , authorizationServerUrl ) ;
167
258
let response : Response ;
168
259
try {
169
260
response = await fetch ( url , {
@@ -197,7 +288,7 @@ export async function discoverOAuthMetadata(
197
288
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
198
289
*/
199
290
export async function startAuthorization (
200
- serverUrl : string | URL ,
291
+ authorizationServerUrl : string | URL ,
201
292
{
202
293
metadata,
203
294
clientInformation,
@@ -230,7 +321,7 @@ export async function startAuthorization(
230
321
) ;
231
322
}
232
323
} else {
233
- authorizationUrl = new URL ( "/authorize" , serverUrl ) ;
324
+ authorizationUrl = new URL ( "/authorize" , authorizationServerUrl ) ;
234
325
}
235
326
236
327
// Generate PKCE challenge
@@ -254,7 +345,7 @@ export async function startAuthorization(
254
345
* Exchanges an authorization code for an access token with the given server.
255
346
*/
256
347
export async function exchangeAuthorization (
257
- serverUrl : string | URL ,
348
+ authorizationServerUrl : string | URL ,
258
349
{
259
350
metadata,
260
351
clientInformation,
@@ -284,7 +375,7 @@ export async function exchangeAuthorization(
284
375
) ;
285
376
}
286
377
} else {
287
- tokenUrl = new URL ( "/token" , serverUrl ) ;
378
+ tokenUrl = new URL ( "/token" , authorizationServerUrl ) ;
288
379
}
289
380
290
381
// Exchange code for tokens
@@ -319,7 +410,7 @@ export async function exchangeAuthorization(
319
410
* Exchange a refresh token for an updated access token.
320
411
*/
321
412
export async function refreshAuthorization (
322
- serverUrl : string | URL ,
413
+ authorizationServerUrl : string | URL ,
323
414
{
324
415
metadata,
325
416
clientInformation,
@@ -345,7 +436,7 @@ export async function refreshAuthorization(
345
436
) ;
346
437
}
347
438
} else {
348
- tokenUrl = new URL ( "/token" , serverUrl ) ;
439
+ tokenUrl = new URL ( "/token" , authorizationServerUrl ) ;
349
440
}
350
441
351
442
// Exchange refresh token
@@ -366,7 +457,7 @@ export async function refreshAuthorization(
366
457
} ,
367
458
body : params ,
368
459
} ) ;
369
-
460
+ console . log ( response )
370
461
if ( ! response . ok ) {
371
462
throw new Error ( `Token refresh failed: HTTP ${ response . status } ` ) ;
372
463
}
@@ -378,7 +469,7 @@ export async function refreshAuthorization(
378
469
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
379
470
*/
380
471
export async function registerClient (
381
- serverUrl : string | URL ,
472
+ authorizationServerUrl : string | URL ,
382
473
{
383
474
metadata,
384
475
clientMetadata,
@@ -396,7 +487,7 @@ export async function registerClient(
396
487
397
488
registrationUrl = new URL ( metadata . registration_endpoint ) ;
398
489
} else {
399
- registrationUrl = new URL ( "/register" , serverUrl ) ;
490
+ registrationUrl = new URL ( "/register" , authorizationServerUrl ) ;
400
491
}
401
492
402
493
const response = await fetch ( registrationUrl , {
0 commit comments