Skip to content

Commit 3c69e5d

Browse files
author
itsuki
committed
change auth flow to use authroization server and
add function to discover protected resource metadata
1 parent 0ce2da8 commit 3c69e5d

File tree

1 file changed

+113
-22
lines changed

1 file changed

+113
-22
lines changed

src/client/auth.ts

Lines changed: 113 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import pkceChallenge from "pkce-challenge";
22
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";
55

66
/**
77
* Implements an end-to-end OAuth client to be used with one MCP server.
8-
*
8+
*
99
* This client relies upon a concept of an authorized "session," the exact
1010
* meaning of which is application-defined. Tokens, authorization codes, and
1111
* code verifiers should not cross different sessions.
@@ -32,7 +32,7 @@ export interface OAuthClientProvider {
3232
* If implemented, this permits the OAuth client to dynamically register with
3333
* the server. Client information saved this way should later be read via
3434
* `clientInformation()`.
35-
*
35+
*
3636
* This method is not required to be implemented if client information is
3737
* statically known (e.g., pre-registered).
3838
*/
@@ -78,14 +78,29 @@ export class UnauthorizedError extends Error {
7878

7979
/**
8080
* Orchestrates the full auth flow with a server.
81-
*
81+
*
8282
* This can be used as a single entry point for all authorization functionality,
8383
* instead of linking together the other lower-level functions in this module.
8484
*/
8585
export async function auth(
8686
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);
89104

90105
// Handle client registration if needed
91106
let clientInformation = await Promise.resolve(provider.clientInformation());
@@ -98,7 +113,7 @@ export async function auth(
98113
throw new Error("OAuth client information must be saveable for dynamic registration");
99114
}
100115

101-
const fullInformation = await registerClient(serverUrl, {
116+
const fullInformation = await registerClient(authorizationServerUrl, {
102117
metadata,
103118
clientMetadata: provider.clientMetadata,
104119
});
@@ -110,7 +125,7 @@ export async function auth(
110125
// Exchange authorization code for tokens
111126
if (authorizationCode !== undefined) {
112127
const codeVerifier = await provider.codeVerifier();
113-
const tokens = await exchangeAuthorization(serverUrl, {
128+
const tokens = await exchangeAuthorization(authorizationServerUrl, {
114129
metadata,
115130
clientInformation,
116131
authorizationCode,
@@ -128,7 +143,7 @@ export async function auth(
128143
if (tokens?.refresh_token) {
129144
try {
130145
// Attempt to refresh the token
131-
const newTokens = await refreshAuthorization(serverUrl, {
146+
const newTokens = await refreshAuthorization(authorizationServerUrl, {
132147
metadata,
133148
clientInformation,
134149
refreshToken: tokens.refresh_token,
@@ -142,7 +157,7 @@ export async function auth(
142157
}
143158

144159
// Start new authorization flow
145-
const { authorizationUrl, codeVerifier } = await startAuthorization(serverUrl, {
160+
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
146161
metadata,
147162
clientInformation,
148163
redirectUrl: provider.redirectUrl
@@ -153,17 +168,93 @@ export async function auth(
153168
return "REDIRECT";
154169
}
155170

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 = /resource_metadata="([^"]*)"/;
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+
156247
/**
157248
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
158249
*
159250
* If the server returns a 404 for the well-known endpoint, this function will
160251
* return `undefined`. Any other errors will be thrown as exceptions.
161252
*/
162253
export async function discoverOAuthMetadata(
163-
serverUrl: string | URL,
254+
authorizationServerUrl: string | URL,
164255
opts?: { protocolVersion?: string },
165256
): 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);
167258
let response: Response;
168259
try {
169260
response = await fetch(url, {
@@ -197,7 +288,7 @@ export async function discoverOAuthMetadata(
197288
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
198289
*/
199290
export async function startAuthorization(
200-
serverUrl: string | URL,
291+
authorizationServerUrl: string | URL,
201292
{
202293
metadata,
203294
clientInformation,
@@ -230,7 +321,7 @@ export async function startAuthorization(
230321
);
231322
}
232323
} else {
233-
authorizationUrl = new URL("/authorize", serverUrl);
324+
authorizationUrl = new URL("/authorize", authorizationServerUrl);
234325
}
235326

236327
// Generate PKCE challenge
@@ -254,7 +345,7 @@ export async function startAuthorization(
254345
* Exchanges an authorization code for an access token with the given server.
255346
*/
256347
export async function exchangeAuthorization(
257-
serverUrl: string | URL,
348+
authorizationServerUrl: string | URL,
258349
{
259350
metadata,
260351
clientInformation,
@@ -284,7 +375,7 @@ export async function exchangeAuthorization(
284375
);
285376
}
286377
} else {
287-
tokenUrl = new URL("/token", serverUrl);
378+
tokenUrl = new URL("/token", authorizationServerUrl);
288379
}
289380

290381
// Exchange code for tokens
@@ -319,7 +410,7 @@ export async function exchangeAuthorization(
319410
* Exchange a refresh token for an updated access token.
320411
*/
321412
export async function refreshAuthorization(
322-
serverUrl: string | URL,
413+
authorizationServerUrl: string | URL,
323414
{
324415
metadata,
325416
clientInformation,
@@ -345,7 +436,7 @@ export async function refreshAuthorization(
345436
);
346437
}
347438
} else {
348-
tokenUrl = new URL("/token", serverUrl);
439+
tokenUrl = new URL("/token", authorizationServerUrl);
349440
}
350441

351442
// Exchange refresh token
@@ -366,7 +457,7 @@ export async function refreshAuthorization(
366457
},
367458
body: params,
368459
});
369-
460+
console.log(response)
370461
if (!response.ok) {
371462
throw new Error(`Token refresh failed: HTTP ${response.status}`);
372463
}
@@ -378,7 +469,7 @@ export async function refreshAuthorization(
378469
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
379470
*/
380471
export async function registerClient(
381-
serverUrl: string | URL,
472+
authorizationServerUrl: string | URL,
382473
{
383474
metadata,
384475
clientMetadata,
@@ -396,7 +487,7 @@ export async function registerClient(
396487

397488
registrationUrl = new URL(metadata.registration_endpoint);
398489
} else {
399-
registrationUrl = new URL("/register", serverUrl);
490+
registrationUrl = new URL("/register", authorizationServerUrl);
400491
}
401492

402493
const response = await fetch(registrationUrl, {

0 commit comments

Comments
 (0)