diff --git a/src/auth/auth-metadata.ts b/src/auth/auth-metadata.ts index 99c2c6e..30add88 100644 --- a/src/auth/auth-metadata.ts +++ b/src/auth/auth-metadata.ts @@ -25,11 +25,20 @@ export function protectedResourceHandler({ authServerUrls: string[]; }) { return (req: Request) => { - const origin = new URL(req.url).origin; + const resourceUrl = new URL(req.url); + + resourceUrl.pathname = resourceUrl.pathname + .replace(/^\/\.well-known\/[^\/]+/, ""); + + // The URL class does not allow for empty `pathname` and will replace it + // with "/". Here, we correct that. + const resource = resourceUrl.pathname === '/' + ? resourceUrl.toString().replace(/\/$/, '') + : resourceUrl.toString(); const metadata = generateProtectedResourceMetadata({ authServerUrls, - resourceUrl: origin, + resourceUrl: resource, }); return new Response(JSON.stringify(metadata), { @@ -43,13 +52,15 @@ export function protectedResourceHandler({ } /** - * Generates protected resource metadata for the given auth server urls and - * resource server url. + * Generates protected resource metadata for the given auth server URLs and + * protected resource identifier. The protected resource identifier, as defined + * in RFC 9728, should be a a URL that uses the https scheme and has no fragment + * component. * * @param authServerUrls - Array of issuer URLs of the authorization servers. Each URL should * match the "issuer" field in the respective authorization server's * OAuth metadata (RFC 8414). - * @param resourceUrl - URL of the resource server + * @param resourceUrl - the protected resource identifier * @param additionalMetadata - Additional metadata fields to include in the response * @returns Protected resource metadata, serializable to JSON */ diff --git a/tests/auth.test.ts b/tests/auth.test.ts new file mode 100644 index 0000000..685e785 --- /dev/null +++ b/tests/auth.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { protectedResourceHandler } from "../src/index"; + +describe("auth", () => { + describe("resource metadata URL to resource identifier mapping", () => { + const handler = protectedResourceHandler({ + authServerUrls: ["https://auth-server.com"], + }); + + const testCases = [ + // Default well-known URI suffix (oauth-protected-resource) + { + resourceMetadata: 'https://resource-server.com/.well-known/oauth-protected-resource', + resource: 'https://resource-server.com', + }, + { + resourceMetadata: 'https://resource-server.com/.well-known/oauth-protected-resource/my-resource', + resource: 'https://resource-server.com/my-resource', + }, + { + resourceMetadata: 'https://resource-server.com/.well-known/oauth-protected-resource/foo/bar', + resource: 'https://resource-server.com/foo/bar', + }, + // Ensure ports work + { + resourceMetadata: 'https://resource-server.com:8443/.well-known/oauth-protected-resource', + resource: 'https://resource-server.com:8443', + }, + // Example well-known URI suffix from RFC 9728 (example-protected-resource) + { + resourceMetadata: 'https://resource-server.com/.well-known/example-protected-resource', + resource: 'https://resource-server.com', + }, + { + resourceMetadata: 'https://resource-server.com/.well-known/example-protected-resource/my-resource', + resource: 'https://resource-server.com/my-resource', + }, + ] as const; + + testCases.forEach(testCase => { + it(`${testCase.resourceMetadata} → ${testCase.resource}`, async () => { + const req = new Request(testCase.resourceMetadata); + const res = handler(req); + const json = await res.json(); + expect(json.resource).toBe(testCase.resource); + }); + }); + }); +}); +