diff --git a/core/src/conformance/java/ai/authplane/sdk/core/conformance/Rfc9728ConformanceTest.java b/core/src/conformance/java/ai/authplane/sdk/core/conformance/Rfc9728ConformanceTest.java index cbb0569..9e67815 100644 --- a/core/src/conformance/java/ai/authplane/sdk/core/conformance/Rfc9728ConformanceTest.java +++ b/core/src/conformance/java/ai/authplane/sdk/core/conformance/Rfc9728ConformanceTest.java @@ -148,5 +148,15 @@ void rfc9728_well_known_path_must_derive_from_resource_uri() { ProtectedResourceMetadata.wellKnownPath( URI.create("https://api.example.com/v2/mcp"))) .isEqualTo("/.well-known/oauth-protected-resource/v2/mcp"); + + // Trailing slashes on the resource path are dropped — identifiers differing + // only by a trailing slash resolve to the same metadata document (RFC 9728 §3). + assertThat( + ProtectedResourceMetadata.wellKnownPath( + URI.create("https://api.example.com/mcp/"))) + .isEqualTo("/.well-known/oauth-protected-resource/mcp"); + + assertThat(ProtectedResourceMetadata.wellKnownPath(URI.create("https://api.example.com/"))) + .isEqualTo("/.well-known/oauth-protected-resource"); } } diff --git a/core/src/main/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadata.java b/core/src/main/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadata.java index ac979e3..a0e2f80 100644 --- a/core/src/main/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadata.java +++ b/core/src/main/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadata.java @@ -56,11 +56,15 @@ private ProtectedResourceMetadata( * Computes the URL path at which this resource server should serve its PRM document. * *
The path is derived from the resource URI by inserting {@code - * /.well-known/oauth-protected-resource} after the authority: + * /.well-known/oauth-protected-resource} after the authority. Trailing slashes on the resource + * path are dropped, so identifiers differing only by a trailing slash resolve to the same + * metadata document (RFC 9728 §3): * *
* "https://api.example.com" → "/.well-known/oauth-protected-resource"
+ * "https://api.example.com/" → "/.well-known/oauth-protected-resource"
* "https://api.example.com/mcp" → "/.well-known/oauth-protected-resource/mcp"
+ * "https://api.example.com/mcp/" → "/.well-known/oauth-protected-resource/mcp"
* "https://api.example.com/v2/mcp" → "/.well-known/oauth-protected-resource/v2/mcp"
*
*
@@ -69,7 +73,15 @@ private ProtectedResourceMetadata(
*/
public static String wellKnownPath(URI resourceUri) {
String path = resourceUri.getPath();
- if (path == null || path.isEmpty() || path.equals("/")) {
+ if (path == null || path.isEmpty()) {
+ return WELL_KNOWN_PREFIX;
+ }
+
+ // Drop trailing slashes — "/mcp/" and "/mcp" must map to the same document
+ while (path.endsWith("/")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ if (path.isEmpty()) {
return WELL_KNOWN_PREFIX;
}
@@ -81,15 +93,13 @@ public static String wellKnownPath(URI resourceUri) {
/**
* Computes the full URL of the PRM document for the given resource URI.
*
+ * Applies the same trailing-slash normalization as {@link #wellKnownPath(URI)}. + * * @param resourceUri the resource server URI string * @return the full PRM document URL */ public static String wellKnownUrl(String resourceUri) { - String stripped = - resourceUri.endsWith("/") - ? resourceUri.substring(0, resourceUri.length() - 1) - : resourceUri; - URI uri = URI.create(stripped); + URI uri = URI.create(resourceUri); return uri.getScheme() + "://" + uri.getAuthority() + wellKnownPath(uri); } diff --git a/core/src/test/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadataTest.java b/core/src/test/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadataTest.java index b7c5804..45f8d9e 100644 --- a/core/src/test/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadataTest.java +++ b/core/src/test/java/ai/authplane/sdk/core/prm/ProtectedResourceMetadataTest.java @@ -27,6 +27,20 @@ void wellKnownPath_resourceWithPath() { .isEqualTo("/.well-known/oauth-protected-resource/mcp"); } + @Test + void wellKnownPath_rootResourceWithTrailingSlash() { + assertThat(ProtectedResourceMetadata.wellKnownPath(URI.create("https://api.example.com/"))) + .isEqualTo("/.well-known/oauth-protected-resource"); + } + + @Test + void wellKnownPath_resourceWithTrailingSlash_dropped() { + assertThat( + ProtectedResourceMetadata.wellKnownPath( + URI.create("https://api.example.com/mcp/"))) + .isEqualTo("/.well-known/oauth-protected-resource/mcp"); + } + @Test void wellKnownPath_resourceWithDeepPath() { assertThat( @@ -117,6 +131,12 @@ void wellKnownUrl_trailingSlash_stripped() { .isEqualTo("https://api.example.com/.well-known/oauth-protected-resource"); } + @Test + void wellKnownUrl_pathWithTrailingSlash_stripped() { + assertThat(ProtectedResourceMetadata.wellKnownUrl("https://api.example.com/mcp/")) + .isEqualTo("https://api.example.com/.well-known/oauth-protected-resource/mcp"); + } + @Test void toJson_producesValidJson() { var prm =