Skip to content

Commit f7ba2a3

Browse files
committed
mcp-server: added servlet context path to resource_metadata header
1 parent 46c28bb commit f7ba2a3

File tree

4 files changed

+149
-2
lines changed

4 files changed

+149
-2
lines changed

mcp-server-security/src/main/java/org/springaicommunity/mcp/security/server/oauth2/authentication/BearerResourceMetadataTokenAuthenticationEntryPoint.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ public void commence(HttpServletRequest request, HttpServletResponse response,
6363

6464
private String buildResourceMetadataPath(HttpServletRequest request, ResourceIdentifier resourceIdentifier) {
6565
return UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
66-
.replacePath("/.well-known/oauth-protected-resource" + resourceIdentifier.getPath())
66+
.replacePath(
67+
request.getContextPath() + "/.well-known/oauth-protected-resource" + resourceIdentifier.getPath())
6768
.replaceQuery(null)
6869
.fragment(null)
6970
.toUriString();

mcp-server-security/src/main/java/org/springaicommunity/mcp/security/server/oauth2/metadata/ResourceIdentifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public String getResource() {
4343
var request = requestAttributes.getRequest();
4444

4545
return UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
46-
.replacePath(this.getPath())
46+
.replacePath(request.getContextPath() + this.getPath())
4747
.replaceQuery(null)
4848
.fragment(null)
4949
.toUriString();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.springaicommunity.mcp.security.server.oauth2.authentication;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.eq;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.ArgumentCaptor;
12+
import org.springaicommunity.mcp.security.server.oauth2.metadata.ResourceIdentifier;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.security.core.AuthenticationException;
15+
16+
import jakarta.servlet.http.HttpServletRequest;
17+
import jakarta.servlet.http.HttpServletResponse;
18+
19+
class BearerResourceMetadataTokenAuthenticationEntryPointTest {
20+
21+
private HttpServletRequest request;
22+
23+
private HttpServletResponse response;
24+
25+
private ResourceIdentifier resourceIdentifier;
26+
27+
private BearerResourceMetadataTokenAuthenticationEntryPoint entryPoint;
28+
29+
@BeforeEach
30+
void setUp() {
31+
request = mock(HttpServletRequest.class);
32+
response = mock(HttpServletResponse.class);
33+
resourceIdentifier = mock(ResourceIdentifier.class);
34+
when(resourceIdentifier.getPath()).thenReturn("/mcp");
35+
entryPoint = new BearerResourceMetadataTokenAuthenticationEntryPoint(resourceIdentifier);
36+
}
37+
38+
@Test
39+
void commence_ShouldAddCustomContextPath() throws Exception {
40+
when(request.getContextPath()).thenReturn("/foo");
41+
when(request.getScheme()).thenReturn("https");
42+
when(request.getServerName()).thenReturn("my.host.com");
43+
when(request.getServerPort()).thenReturn(443);
44+
when(request.getRequestURI()).thenReturn("/foo/some/endpoint");
45+
when(request.getQueryString()).thenReturn(null);
46+
47+
when(response.getHeader(HttpHeaders.WWW_AUTHENTICATE)).thenReturn("Bearer");
48+
49+
AuthenticationException authException = mock(AuthenticationException.class);
50+
51+
entryPoint.commence(request, response, authException);
52+
53+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
54+
verify(response).setHeader(eq(HttpHeaders.WWW_AUTHENTICATE), captor.capture());
55+
56+
String headerValue = captor.getValue();
57+
assertThat(headerValue)
58+
.contains("Bearer resource_metadata=https://my.host.com/foo/.well-known/oauth-protected-resource/mcp");
59+
}
60+
61+
@Test
62+
void commence_ShouldAddDefaultContextPath() throws Exception {
63+
when(request.getContextPath()).thenReturn("/");
64+
when(request.getScheme()).thenReturn("https");
65+
when(request.getServerName()).thenReturn("my.host.com");
66+
when(request.getServerPort()).thenReturn(443);
67+
when(request.getRequestURI()).thenReturn("/some/endpoint");
68+
when(request.getQueryString()).thenReturn(null);
69+
70+
when(response.getHeader(HttpHeaders.WWW_AUTHENTICATE)).thenReturn("Bearer");
71+
72+
AuthenticationException authException = mock(AuthenticationException.class);
73+
74+
entryPoint.commence(request, response, authException);
75+
76+
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
77+
verify(response).setHeader(eq(HttpHeaders.WWW_AUTHENTICATE), captor.capture());
78+
79+
String headerValue = captor.getValue();
80+
assertThat(headerValue)
81+
.contains("Bearer resource_metadata=https://my.host.com/.well-known/oauth-protected-resource/mcp");
82+
}
83+
84+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.springaicommunity.mcp.security.server.oauth2.metadata;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.BDDAssertions.catchThrowable;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import org.junit.jupiter.api.AfterEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.web.context.request.RequestContextHolder;
11+
import org.springframework.web.context.request.ServletRequestAttributes;
12+
13+
import jakarta.servlet.http.HttpServletRequest;
14+
15+
class ResourceIdentifierTest {
16+
17+
@AfterEach
18+
void tearDown() {
19+
RequestContextHolder.resetRequestAttributes();
20+
}
21+
22+
@Test
23+
void constructor_ShouldThrowException_WhenPathIsEmpty() {
24+
// when
25+
Throwable thrown = catchThrowable(() -> new ResourceIdentifier(""));
26+
27+
// then
28+
assertThat(thrown).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("path cannot be empty");
29+
}
30+
31+
@Test
32+
void getPath_ShouldReturnGivenPath() {
33+
// given
34+
var identifier = new ResourceIdentifier("/my-resource");
35+
36+
// then
37+
assertThat(identifier.getPath()).isEqualTo("/my-resource");
38+
}
39+
40+
@Test
41+
void getResource_ShouldBuildFullUrlBasedOnCurrentRequest() {
42+
// given
43+
HttpServletRequest request = mock(HttpServletRequest.class);
44+
when(request.getScheme()).thenReturn("https");
45+
when(request.getServerName()).thenReturn("my.host.com");
46+
when(request.getServerPort()).thenReturn(8443);
47+
when(request.getContextPath()).thenReturn("/foo");
48+
when(request.getRequestURI()).thenReturn("/foo/other/path");
49+
when(request.getQueryString()).thenReturn(null);
50+
51+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
52+
53+
var identifier = new ResourceIdentifier("/mcp");
54+
55+
// when
56+
String result = identifier.getResource();
57+
58+
// then
59+
assertThat(result).isEqualTo("https://my.host.com:8443/foo/mcp");
60+
}
61+
62+
}

0 commit comments

Comments
 (0)