Skip to content

Commit 224a2e2

Browse files
committed
update resource to be a url
1 parent 36f338a commit 224a2e2

File tree

8 files changed

+33
-92
lines changed

8 files changed

+33
-92
lines changed

src/client/auth.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ export async function auth(
100100
authorizationCode?: string;
101101
scope?: string;
102102
resourceMetadataUrl?: URL;
103-
resource?: string }): Promise<AuthResult> {
103+
resource?: URL }): Promise<AuthResult> {
104104

105105
// Remove fragment from resource parameter if provided
106-
let canonicalResource: string | undefined;
106+
let canonicalResource: URL | undefined;
107107
if (resource) {
108-
canonicalResource = resourceUrlFromServerUrl(resource);
108+
canonicalResource = resourceUrlFromServerUrl(new URL(resource));
109109
}
110110

111111
let authorizationServerUrl = serverUrl;
@@ -329,7 +329,7 @@ export async function startAuthorization(
329329
redirectUrl: string | URL;
330330
scope?: string;
331331
state?: string;
332-
resource?: string;
332+
resource?: URL;
333333
},
334334
): Promise<{ authorizationUrl: URL; codeVerifier: string }> {
335335
const responseType = "code";
@@ -380,7 +380,7 @@ export async function startAuthorization(
380380
}
381381

382382
if (resource) {
383-
authorizationUrl.searchParams.set("resource", resource);
383+
authorizationUrl.searchParams.set("resource", resource.href);
384384
}
385385

386386
return { authorizationUrl, codeVerifier };
@@ -404,7 +404,7 @@ export async function exchangeAuthorization(
404404
authorizationCode: string;
405405
codeVerifier: string;
406406
redirectUri: string | URL;
407-
resource?: string;
407+
resource?: URL;
408408
},
409409
): Promise<OAuthTokens> {
410410
const grantType = "authorization_code";
@@ -439,7 +439,7 @@ export async function exchangeAuthorization(
439439
}
440440

441441
if (resource) {
442-
params.set("resource", resource);
442+
params.set("resource", resource.href);
443443
}
444444

445445
const response = await fetch(tokenUrl, {
@@ -471,7 +471,7 @@ export async function refreshAuthorization(
471471
metadata?: OAuthMetadata;
472472
clientInformation: OAuthClientInformation;
473473
refreshToken: string;
474-
resource?: string;
474+
resource?: URL;
475475
},
476476
): Promise<OAuthTokens> {
477477
const grantType = "refresh_token";
@@ -504,7 +504,7 @@ export async function refreshAuthorization(
504504
}
505505

506506
if (resource) {
507-
params.set("resource", resource);
507+
params.set("resource", resource.href);
508508
}
509509

510510
const response = await fetch(tokenUrl, {

src/examples/server/demoInMemoryOAuthProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class DemoInMemoryAuthProvider implements OAuthServerProvider {
122122
client: OAuthClientInformationFull,
123123
_refreshToken: string,
124124
_scopes?: string[],
125-
resource?: string
125+
resource?: URL
126126
): Promise<OAuthTokens> {
127127
throw new Error('Refresh tokens not implemented for example demo');
128128
}

src/server/auth/handlers/authorize.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ describe('Authorization Handler', () => {
295295
expect(mockProviderWithResource).toHaveBeenCalledWith(
296296
validClient,
297297
expect.objectContaining({
298-
resource: 'https://api.example.com/resource',
298+
resource: new URL('https://api.example.com/resource'),
299299
redirectUri: 'https://example.com/callback',
300300
codeChallenge: 'challenge123'
301301
}),
@@ -365,7 +365,7 @@ describe('Authorization Handler', () => {
365365
expect(mockProviderWithResources).toHaveBeenCalledWith(
366366
validClient,
367367
expect.objectContaining({
368-
resource: 'https://api1.example.com/resource',
368+
resource: new URL('https://api1.example.com/resource'),
369369
state: 'test-state'
370370
}),
371371
expect.any(Object)
@@ -391,7 +391,7 @@ describe('Authorization Handler', () => {
391391
expect(mockProviderPost).toHaveBeenCalledWith(
392392
validClient,
393393
expect.objectContaining({
394-
resource: 'https://api.example.com/resource'
394+
resource: new URL('https://api.example.com/resource')
395395
}),
396396
expect.any(Object)
397397
);

src/server/auth/handlers/authorize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
142142
scopes: requestedScopes,
143143
redirectUri: redirect_uri,
144144
codeChallenge: code_challenge,
145-
resource,
145+
resource: resource ? new URL(resource) : undefined,
146146
}, res);
147147
} catch (error) {
148148
// Post-redirect errors - redirect with error parameters

src/server/auth/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ export interface OAuthServerProvider {
4242
authorizationCode: string,
4343
codeVerifier?: string,
4444
redirectUri?: string,
45-
resource?: string
45+
resource?: URL
4646
): Promise<OAuthTokens>;
4747

4848
/**
4949
* Exchanges a refresh token for an access token.
5050
*/
51-
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[], resource?: string): Promise<OAuthTokens>;
51+
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[], resource?: URL): Promise<OAuthTokens>;
5252

5353
/**
5454
* Verifies an access token and returns information about it.

src/server/auth/providers/proxyProvider.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe("Proxy OAuth Server Provider", () => {
112112
codeChallenge: 'test-challenge',
113113
state: 'test-state',
114114
scopes: ['read', 'write'],
115-
resource: 'https://api.example.com/resource'
115+
resource: new URL('https://api.example.com/resource')
116116
},
117117
mockResponse
118118
);

src/server/auth/providers/proxyProvider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
134134
// Add optional standard OAuth parameters
135135
if (params.state) searchParams.set("state", params.state);
136136
if (params.scopes?.length) searchParams.set("scope", params.scopes.join(" "));
137-
if (params.resource) searchParams.set("resource", params.resource);
137+
if (params.resource) searchParams.set("resource", params.resource.href);
138138

139139
targetUrl.search = searchParams.toString();
140140
res.redirect(targetUrl.toString());
@@ -154,7 +154,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
154154
authorizationCode: string,
155155
codeVerifier?: string,
156156
redirectUri?: string,
157-
resource?: string
157+
resource?: URL
158158
): Promise<OAuthTokens> {
159159
const params = new URLSearchParams({
160160
grant_type: "authorization_code",
@@ -199,7 +199,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
199199
client: OAuthClientInformationFull,
200200
refreshToken: string,
201201
scopes?: string[],
202-
resource?: string
202+
resource?: URL
203203
): Promise<OAuthTokens> {
204204

205205
const params = new URLSearchParams({

src/shared/auth-utils.test.ts

Lines changed: 13 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,30 @@
1-
import { validateResourceUri, extractResourceUri, resourceUrlFromServerUrl } from './auth-utils.js';
1+
import { resourceUrlFromServerUrl } from './auth-utils.js';
22

33
describe('auth-utils', () => {
44
describe('resourceUrlFromServerUrl', () => {
55
it('should remove fragments', () => {
6-
expect(resourceUrlFromServerUrl('https://example.com/path#fragment')).toBe('https://example.com/path');
7-
expect(resourceUrlFromServerUrl('https://example.com#fragment')).toBe('https://example.com');
8-
expect(resourceUrlFromServerUrl('https://example.com/path?query=1#fragment')).toBe('https://example.com/path?query=1');
6+
expect(resourceUrlFromServerUrl(new URL('https://example.com/path#fragment')).href).toBe('https://example.com/path');
7+
expect(resourceUrlFromServerUrl(new URL('https://example.com#fragment')).href).toBe('https://example.com/');
8+
expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1#fragment')).href).toBe('https://example.com/path?query=1');
99
});
1010

1111
it('should return URL unchanged if no fragment', () => {
12-
expect(resourceUrlFromServerUrl('https://example.com')).toBe('https://example.com');
13-
expect(resourceUrlFromServerUrl('https://example.com/path')).toBe('https://example.com/path');
14-
expect(resourceUrlFromServerUrl('https://example.com/path?query=1')).toBe('https://example.com/path?query=1');
12+
expect(resourceUrlFromServerUrl(new URL('https://example.com')).href).toBe('https://example.com/');
13+
expect(resourceUrlFromServerUrl(new URL('https://example.com/path')).href).toBe('https://example.com/path');
14+
expect(resourceUrlFromServerUrl(new URL('https://example.com/path?query=1')).href).toBe('https://example.com/path?query=1');
1515
});
1616

1717
it('should keep everything else unchanged', () => {
1818
// Case sensitivity preserved
19-
expect(resourceUrlFromServerUrl('HTTPS://EXAMPLE.COM/PATH')).toBe('HTTPS://EXAMPLE.COM/PATH');
19+
expect(resourceUrlFromServerUrl(new URL('https://EXAMPLE.COM/PATH')).href).toBe('https://example.com/PATH');
2020
// Ports preserved
21-
expect(resourceUrlFromServerUrl('https://example.com:443/path')).toBe('https://example.com:443/path');
22-
expect(resourceUrlFromServerUrl('https://example.com:8080/path')).toBe('https://example.com:8080/path');
21+
expect(resourceUrlFromServerUrl(new URL('https://example.com:443/path')).href).toBe('https://example.com/path');
22+
expect(resourceUrlFromServerUrl(new URL('https://example.com:8080/path')).href).toBe('https://example.com:8080/path');
2323
// Query parameters preserved
24-
expect(resourceUrlFromServerUrl('https://example.com?foo=bar&baz=qux')).toBe('https://example.com?foo=bar&baz=qux');
24+
expect(resourceUrlFromServerUrl(new URL('https://example.com?foo=bar&baz=qux')).href).toBe('https://example.com/?foo=bar&baz=qux');
2525
// Trailing slashes preserved
26-
expect(resourceUrlFromServerUrl('https://example.com/')).toBe('https://example.com/');
27-
expect(resourceUrlFromServerUrl('https://example.com/path/')).toBe('https://example.com/path/');
28-
});
29-
});
30-
31-
32-
describe('validateResourceUri', () => {
33-
it('should accept valid resource URIs without fragments', () => {
34-
expect(() => validateResourceUri('https://example.com')).not.toThrow();
35-
expect(() => validateResourceUri('https://example.com/path')).not.toThrow();
36-
expect(() => validateResourceUri('http://example.com:8080')).not.toThrow();
37-
expect(() => validateResourceUri('https://example.com?query=1')).not.toThrow();
38-
expect(() => validateResourceUri('ftp://example.com')).not.toThrow(); // Only fragment check now
39-
});
40-
41-
it('should reject URIs with fragments', () => {
42-
expect(() => validateResourceUri('https://example.com#fragment')).toThrow('must not contain a fragment');
43-
expect(() => validateResourceUri('https://example.com/path#section')).toThrow('must not contain a fragment');
44-
expect(() => validateResourceUri('https://example.com?query=1#anchor')).toThrow('must not contain a fragment');
45-
});
46-
47-
it('should accept any URI without fragment', () => {
48-
// These are all valid now since we only check for fragments
49-
expect(() => validateResourceUri('//example.com')).not.toThrow();
50-
expect(() => validateResourceUri('https://user:[email protected]')).not.toThrow();
51-
expect(() => validateResourceUri('/path')).not.toThrow();
52-
expect(() => validateResourceUri('path')).not.toThrow();
53-
});
54-
});
55-
56-
describe('extractResourceUri', () => {
57-
it('should remove fragments from URLs', () => {
58-
expect(extractResourceUri('https://example.com/path#fragment')).toBe('https://example.com/path');
59-
expect(extractResourceUri('https://example.com/path?query=1#fragment')).toBe('https://example.com/path?query=1');
60-
});
61-
62-
it('should handle URL object', () => {
63-
const url = new URL('https://example.com:8443/path?query=1#fragment');
64-
expect(extractResourceUri(url)).toBe('https://example.com:8443/path?query=1');
65-
});
66-
67-
it('should keep everything else unchanged', () => {
68-
// Preserves case
69-
expect(extractResourceUri('HTTPS://EXAMPLE.COM/path')).toBe('HTTPS://EXAMPLE.COM/path');
70-
// Preserves all ports
71-
expect(extractResourceUri('https://example.com:443/path')).toBe('https://example.com:443/path');
72-
expect(extractResourceUri('http://example.com:80/path')).toBe('http://example.com:80/path');
73-
// Preserves query parameters
74-
expect(extractResourceUri('https://example.com/path?query=1')).toBe('https://example.com/path?query=1');
75-
// Preserves trailing slashes
76-
expect(extractResourceUri('https://example.com/')).toBe('https://example.com/');
77-
expect(extractResourceUri('https://example.com/app1/')).toBe('https://example.com/app1/');
78-
});
79-
80-
it('should distinguish between different paths on same domain', () => {
81-
// This is the key test for the security concern mentioned
82-
const app1 = extractResourceUri('https://api.example.com/mcp-server-1');
83-
const app2 = extractResourceUri('https://api.example.com/mcp-server-2');
84-
expect(app1).not.toBe(app2);
85-
expect(app1).toBe('https://api.example.com/mcp-server-1');
86-
expect(app2).toBe('https://api.example.com/mcp-server-2');
26+
expect(resourceUrlFromServerUrl(new URL('https://example.com/')).href).toBe('https://example.com/');
27+
expect(resourceUrlFromServerUrl(new URL('https://example.com/path/')).href).toBe('https://example.com/path/');
8728
});
8829
});
8930
});

0 commit comments

Comments
 (0)