implementation of RFC 8707 Resource Indicators (Fixes #592, Fixes #635)

This commit is contained in:
Olivier Chafik 2025-06-16 19:29:04 +01:00
commit 7b5bfcee52
No known key found for this signature in database
4 changed files with 2311 additions and 33 deletions

View file

@ -341,6 +341,31 @@ describe("OAuth Authorization", () => {
expect(codeVerifier).toBe("test_verifier");
});
it("includes resource parameter when provided", async () => {
const { authorizationUrl } = await startAuthorization(
"https://auth.example.com",
{
clientInformation: validClientInfo,
redirectUrl: "http://localhost:3000/callback",
resource: "https://api.example.com/mcp-server",
}
);
expect(authorizationUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter when not provided", async () => {
const { authorizationUrl } = await startAuthorization(
"https://auth.example.com",
{
clientInformation: validClientInfo,
redirectUrl: "http://localhost:3000/callback",
}
);
expect(authorizationUrl.searchParams.has("resource")).toBe(false);
});
it("includes scope parameter when provided", async () => {
const { authorizationUrl } = await startAuthorization(
"https://auth.example.com",
@ -489,6 +514,45 @@ describe("OAuth Authorization", () => {
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
});
it("includes resource parameter in token exchange when provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokens,
});
const tokens = await exchangeAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
resource: "https://api.example.com/mcp-server",
});
expect(tokens).toEqual(validTokens);
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter from token exchange when not provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokens,
});
await exchangeAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
authorizationCode: "code123",
codeVerifier: "verifier123",
redirectUri: "http://localhost:3000/callback",
});
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.has("resource")).toBe(false);
});
it("validates token response schema", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@ -576,6 +640,41 @@ describe("OAuth Authorization", () => {
expect(body.get("client_secret")).toBe("secret123");
});
it("includes resource parameter in refresh token request when provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokensWithNewRefreshToken,
});
const tokens = await refreshAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
refreshToken: "refresh123",
resource: "https://api.example.com/mcp-server",
});
expect(tokens).toEqual(validTokensWithNewRefreshToken);
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("excludes resource parameter from refresh token request when not provided", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => validTokensWithNewRefreshToken,
});
await refreshAuthorization("https://auth.example.com", {
clientInformation: validClientInfo,
refreshToken: "refresh123",
});
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
expect(body.has("resource")).toBe(false);
});
it("exchanges refresh token for new tokens and keep existing refresh token if none is returned", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@ -807,5 +906,418 @@ describe("OAuth Authorization", () => {
"https://resource.example.com/.well-known/oauth-authorization-server"
);
});
it("canonicalizes resource URI by removing fragment", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call the auth function with a resource that has a fragment
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "https://api.example.com/mcp-server#fragment",
});
expect(result).toBe("REDIRECT");
// Verify redirectToAuthorization was called with the canonicalized resource
expect(mockProvider.redirectToAuthorization).toHaveBeenCalledWith(
expect.objectContaining({
searchParams: expect.any(URLSearchParams),
})
);
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("passes resource parameter through authorization flow", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods for authorization flow
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call auth without authorization code (should trigger redirect)
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "https://api.example.com/mcp-server",
});
expect(result).toBe("REDIRECT");
// Verify the authorization URL includes the resource parameter
expect(mockProvider.redirectToAuthorization).toHaveBeenCalledWith(
expect.objectContaining({
searchParams: expect.any(URLSearchParams),
})
);
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("includes resource in token exchange when authorization code is provided", async () => {
// Mock successful metadata discovery and token exchange
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
} else if (urlString.includes("/token")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
access_token: "access123",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "refresh123",
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods for token exchange
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.codeVerifier as jest.Mock).mockResolvedValue("test-verifier");
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
// Call auth with authorization code
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
authorizationCode: "auth-code-123",
resource: "https://api.example.com/mcp-server",
});
expect(result).toBe("AUTHORIZED");
// Find the token exchange call
const tokenCall = mockFetch.mock.calls.find(call =>
call[0].toString().includes("/token")
);
expect(tokenCall).toBeDefined();
const body = tokenCall![1].body as URLSearchParams;
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
expect(body.get("code")).toBe("auth-code-123");
});
it("includes resource in token refresh", async () => {
// Mock successful metadata discovery and token refresh
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
} else if (urlString.includes("/token")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
access_token: "new-access123",
token_type: "Bearer",
expires_in: 3600,
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods for token refresh
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue({
access_token: "old-access",
refresh_token: "refresh123",
});
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
// Call auth with existing tokens (should trigger refresh)
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "https://api.example.com/mcp-server",
});
expect(result).toBe("AUTHORIZED");
// Find the token refresh call
const tokenCall = mockFetch.mock.calls.find(call =>
call[0].toString().includes("/token")
);
expect(tokenCall).toBeDefined();
const body = tokenCall![1].body as URLSearchParams;
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
expect(body.get("grant_type")).toBe("refresh_token");
expect(body.get("refresh_token")).toBe("refresh123");
});
it("handles empty resource parameter", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call auth with empty resource parameter
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "",
});
expect(result).toBe("REDIRECT");
// Verify that empty resource is not included in the URL
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.has("resource")).toBe(false);
});
it("handles resource with multiple fragments", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call auth with resource containing multiple # symbols
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "https://api.example.com/mcp-server#fragment#another",
});
expect(result).toBe("REDIRECT");
// Verify the resource is properly canonicalized (everything after first # removed)
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server");
});
it("verifies resource parameter distinguishes between different paths on same domain", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Test with different resource paths on same domain
// This tests the security fix that prevents token confusion between
// multiple MCP servers on the same domain
const result1 = await auth(mockProvider, {
serverUrl: "https://api.example.com",
resource: "https://api.example.com/mcp-server-1/v1",
});
expect(result1).toBe("REDIRECT");
const redirectCall1 = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl1: URL = redirectCall1[0];
expect(authUrl1.searchParams.get("resource")).toBe("https://api.example.com/mcp-server-1/v1");
// Clear mock calls
(mockProvider.redirectToAuthorization as jest.Mock).mockClear();
// Test with different path on same domain
const result2 = await auth(mockProvider, {
serverUrl: "https://api.example.com",
resource: "https://api.example.com/mcp-server-2/v1",
});
expect(result2).toBe("REDIRECT");
const redirectCall2 = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl2: URL = redirectCall2[0];
expect(authUrl2.searchParams.get("resource")).toBe("https://api.example.com/mcp-server-2/v1");
// Verify that the two resources are different (critical for security)
expect(authUrl1.searchParams.get("resource")).not.toBe(authUrl2.searchParams.get("resource"));
});
it("preserves query parameters in resource URI", async () => {
// Mock successful metadata discovery
mockFetch.mockImplementation((url) => {
const urlString = url.toString();
if (urlString.includes("/.well-known/oauth-authorization-server")) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({
issuer: "https://auth.example.com",
authorization_endpoint: "https://auth.example.com/authorize",
token_endpoint: "https://auth.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
}),
});
}
return Promise.resolve({ ok: false, status: 404 });
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: "test-client",
client_secret: "test-secret",
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
// Call auth with resource containing query parameters
const result = await auth(mockProvider, {
serverUrl: "https://resource.example.com",
resource: "https://api.example.com/mcp-server?param=value&another=test",
});
expect(result).toBe("REDIRECT");
// Verify query parameters are preserved (only fragment is removed)
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get("resource")).toBe("https://api.example.com/mcp-server?param=value&another=test");
});
});
});

View file

@ -2,6 +2,7 @@ import pkceChallenge from "pkce-challenge";
import { LATEST_PROTOCOL_VERSION } from "../types.js";
import type { OAuthClientMetadata, OAuthClientInformation, OAuthTokens, OAuthMetadata, OAuthClientInformationFull, OAuthProtectedResourceMetadata } from "../shared/auth.js";
import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from "../shared/auth.js";
import { canonicalizeResourceUri } from "../shared/auth-utils.js";
/**
* Implements an end-to-end OAuth client to be used with one MCP server.
@ -92,12 +93,20 @@ export async function auth(
{ serverUrl,
authorizationCode,
scope,
resourceMetadataUrl
resourceMetadataUrl,
resource
}: {
serverUrl: string | URL;
authorizationCode?: string;
scope?: string;
resourceMetadataUrl?: URL }): Promise<AuthResult> {
resourceMetadataUrl?: URL;
resource?: string }): Promise<AuthResult> {
// Remove fragment from resource parameter if provided
let canonicalResource: string | undefined;
if (resource) {
canonicalResource = canonicalizeResourceUri(resource);
}
let authorizationServerUrl = serverUrl;
try {
@ -142,6 +151,7 @@ export async function auth(
authorizationCode,
codeVerifier,
redirectUri: provider.redirectUrl,
resource: canonicalResource,
});
await provider.saveTokens(tokens);
@ -158,6 +168,7 @@ export async function auth(
metadata,
clientInformation,
refreshToken: tokens.refresh_token,
resource: canonicalResource,
});
await provider.saveTokens(newTokens);
@ -176,6 +187,7 @@ export async function auth(
state,
redirectUrl: provider.redirectUrl,
scope: scope || provider.clientMetadata.scope,
resource: canonicalResource,
});
await provider.saveCodeVerifier(codeVerifier);
@ -310,12 +322,14 @@ export async function startAuthorization(
redirectUrl,
scope,
state,
resource,
}: {
metadata?: OAuthMetadata;
clientInformation: OAuthClientInformation;
redirectUrl: string | URL;
scope?: string;
state?: string;
resource?: string;
},
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
const responseType = "code";
@ -365,6 +379,10 @@ export async function startAuthorization(
authorizationUrl.searchParams.set("scope", scope);
}
if (resource) {
authorizationUrl.searchParams.set("resource", resource);
}
return { authorizationUrl, codeVerifier };
}
@ -379,12 +397,14 @@ export async function exchangeAuthorization(
authorizationCode,
codeVerifier,
redirectUri,
resource,
}: {
metadata?: OAuthMetadata;
clientInformation: OAuthClientInformation;
authorizationCode: string;
codeVerifier: string;
redirectUri: string | URL;
resource?: string;
},
): Promise<OAuthTokens> {
const grantType = "authorization_code";
@ -418,6 +438,10 @@ export async function exchangeAuthorization(
params.set("client_secret", clientInformation.client_secret);
}
if (resource) {
params.set("resource", resource);
}
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
@ -442,10 +466,12 @@ export async function refreshAuthorization(
metadata,
clientInformation,
refreshToken,
resource,
}: {
metadata?: OAuthMetadata;
clientInformation: OAuthClientInformation;
refreshToken: string;
resource?: string;
},
): Promise<OAuthTokens> {
const grantType = "refresh_token";
@ -477,6 +503,10 @@ export async function refreshAuthorization(
params.set("client_secret", clientInformation.client_secret);
}
if (resource) {
params.set("resource", resource);
}
const response = await fetch(tokenUrl, {
method: "POST",
headers: {

View file

@ -2,6 +2,7 @@ import { EventSource, type ErrorEvent, type EventSourceInit } from "eventsource"
import { Transport } from "../shared/transport.js";
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
import { extractCanonicalResourceUri } from "../shared/auth-utils.js";
export class SseError extends Error {
constructor(
@ -86,7 +87,11 @@ export class SSEClientTransport implements Transport {
let result: AuthResult;
try {
result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
result = await auth(this._authProvider, {
serverUrl: this._url,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
} catch (error) {
this.onerror?.(error as Error);
throw error;
@ -201,7 +206,12 @@ export class SSEClientTransport implements Transport {
throw new UnauthorizedError("No auth provider");
}
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl });
const result = await auth(this._authProvider, {
serverUrl: this._url,
authorizationCode,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
if (result !== "AUTHORIZED") {
throw new UnauthorizedError("Failed to authorize");
}
@ -236,7 +246,11 @@ export class SSEClientTransport implements Transport {
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
const result = await auth(this._authProvider, {
serverUrl: this._url,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
if (result !== "AUTHORIZED") {
throw new UnauthorizedError();
}

View file

@ -2,6 +2,7 @@ import { Transport } from "../shared/transport.js";
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
import { EventSourceParserStream } from "eventsource-parser/stream";
import { extractCanonicalResourceUri } from "../shared/auth-utils.js";
// Default reconnection options for StreamableHTTP connections
const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOptions = {
@ -149,7 +150,11 @@ export class StreamableHTTPClientTransport implements Transport {
let result: AuthResult;
try {
result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
result = await auth(this._authProvider, {
serverUrl: this._url,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
} catch (error) {
this.onerror?.(error as Error);
throw error;
@ -362,7 +367,12 @@ export class StreamableHTTPClientTransport implements Transport {
throw new UnauthorizedError("No auth provider");
}
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl });
const result = await auth(this._authProvider, {
serverUrl: this._url,
authorizationCode,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
if (result !== "AUTHORIZED") {
throw new UnauthorizedError("Failed to authorize");
}
@ -410,7 +420,11 @@ export class StreamableHTTPClientTransport implements Transport {
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl });
const result = await auth(this._authProvider, {
serverUrl: this._url,
resourceMetadataUrl: this._resourceMetadataUrl,
resource: extractCanonicalResourceUri(this._url)
});
if (result !== "AUTHORIZED") {
throw new UnauthorizedError();
}