@@ -64,12 +64,12 @@ static class MyTableResponse {
6464
6565 // Feign client for testing REST endpoints
6666 interface RestClient {
67- @ RequestLine ("GET /v1/rest/queries/MyTable ?limit={limit}" )
68- MyTableResponse getMyTable (@ Param ("limit" ) int limit );
67+ @ RequestLine ("GET /v1/rest/queries/AuthMyTable ?limit={limit}" )
68+ MyTableResponse getAuthMyTable (@ Param ("limit" ) int limit );
6969
70- @ RequestLine ("GET /v1/rest/queries/MyTable ?limit={limit}" )
70+ @ RequestLine ("GET /v1/rest/queries/AuthMyTable ?limit={limit}" )
7171 @ Headers ("Authorization: Bearer {token}" )
72- MyTableResponse getMyTableWithAuth (@ Param ("token" ) String token , @ Param ("limit" ) int limit );
72+ MyTableResponse getAuthMyTableWithAuth (@ Param ("token" ) String token , @ Param ("limit" ) int limit );
7373 }
7474
7575 private PostgreSQLContainer <?> postgresql ;
@@ -142,18 +142,24 @@ protected void commonTearDown() {
142142 void givenJwt_whenUnauthenticatedGraphQL_thenReturns401 () {
143143 compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
144144
145- var response = executeGraphQLQuery ("{\" query\" :\" query { __typename }\" }" );
145+ var response = executeGraphQLQuery ("{\" query\" :\" query { AuthMyTable(limit: 5) { val } }\" }" );
146146
147147 assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (401 );
148148 }
149149
150150 @ Test
151151 @ SneakyThrows
152152 void givenJwt_whenAuthenticatedGraphQL_thenSucceeds () {
153- compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
153+ compileAndStartServerWithDatabase ("jwt-authorized-base.sqrl" , testDir );
154154
155- var response = executeGraphQLQuery ("{\" query\" :\" query { __typename }\" }" , generateJwtToken ());
156- validateBasicGraphQLResponse (response );
155+ var response =
156+ executeGraphQLQuery (
157+ "{\" query\" :\" query { AuthMyTable(limit: 5) { val } }\" }" , generateJwtToken ());
158+
159+ assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (200 );
160+ var responseBody = EntityUtils .toString (response .getEntity ());
161+ assertThat (responseBody ).contains ("\" data\" " );
162+ assertThat (responseBody ).contains ("\" AuthMyTable\" " );
157163 }
158164
159165 @ Test
@@ -163,7 +169,8 @@ void givenJwt_whenBadToken_thenReturns401() {
163169
164170 // Generate token with RS256 algorithm while server expects HS256
165171 var response =
166- executeGraphQLQuery ("{\" query\" :\" query { __typename }\" }" , generateRS256JwtToken ());
172+ executeGraphQLQuery (
173+ "{\" query\" :\" query { AuthMyTable(limit: 5) { val } }\" }" , generateRS256JwtToken ());
167174
168175 assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (401 );
169176
@@ -181,7 +188,9 @@ void givenJwt_whenCorruptedToken_thenReturns401() {
181188 compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
182189
183190 // Generate token with RS256 algorithm while server expects HS256
184- var response = executeGraphQLQuery ("{\" query\" :\" query { __typename }\" }" , "dummy-invalid-jwt" );
191+ var response =
192+ executeGraphQLQuery (
193+ "{\" query\" :\" query { AuthMyTable(limit: 5) { val } }\" }" , "dummy-invalid-jwt" );
185194
186195 assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (401 );
187196
@@ -194,6 +203,29 @@ void givenJwt_whenCorruptedToken_thenReturns401() {
194203 assertThat (responseBody ).contains ("\" IllegalArgumentException: Invalid format for JWT\" " );
195204 }
196205
206+ @ Test
207+ @ SneakyThrows
208+ void givenJwt_whenMissingRequiredClaims_thenReturns403 () {
209+ compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
210+
211+ try {
212+ // Generate valid JWT token but with empty claims (missing required claims)
213+ var response =
214+ executeGraphQLQuery (
215+ "{\" query\" :\" query { AuthMyTable(limit: 5) { val } }\" }" ,
216+ generateJwtToken (Map .of ()));
217+
218+ // Valid token but missing required claims should return 403 Forbidden
219+ var responseBody = EntityUtils .toString (response .getEntity ());
220+ assertThat (response .getStatusLine ().getStatusCode ()).isEqualTo (403 );
221+ assertThat (responseBody ).contains ("\" errors\" " );
222+ } finally {
223+ var logs = serverContainer .getLogs ();
224+ log .info ("Detailed server logs:" );
225+ System .out .println (logs );
226+ }
227+ }
228+
197229 @ Test
198230 @ SneakyThrows
199231 void givenJwt_whenUnauthenticatedMcp_thenFails () {
@@ -251,7 +283,7 @@ void givenJwt_whenAuthenticatedMcp_thenSucceeds() {
251283 // Test calling a tool that doesn't require auth metadata
252284 var myTableTool =
253285 tools .tools ().stream ()
254- .filter (tool -> "GetMyTable " .equals (tool .name ()))
286+ .filter (tool -> "GetAuthMyTable " .equals (tool .name ()))
255287 .findFirst ()
256288 .orElseThrow (() -> new RuntimeException ("GetMyTable tool not found" ));
257289 log .info ("Testing tool call: {}" , myTableTool .name ());
@@ -274,10 +306,60 @@ void givenJwt_whenAuthenticatedMcp_thenSucceeds() {
274306 }
275307 }
276308
309+ @ Test
310+ @ SneakyThrows
311+ void givenJwt_whenMissingRequiredClaimsMcp_thenFails () {
312+ compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
313+
314+ var mcpUrl =
315+ String .format (
316+ "http://localhost:%d/v1/mcp" , serverContainer .getMappedPort (HTTP_SERVER_PORT ));
317+ var jwtToken = generateJwtToken (Map .of ());
318+ log .info ("Testing MCP endpoint with JWT missing claims: {}" , mcpUrl );
319+
320+ // Create MCP client with JWT authentication but missing required claims
321+ var transport =
322+ HttpClientStreamableHttpTransport .builder (mcpUrl )
323+ .customizeRequest (r -> r .header ("Authorization" , "Bearer " + jwtToken ))
324+ .endpoint ("/v1/mcp" )
325+ .build ();
326+
327+ try (var client = McpClient .sync (transport ).requestTimeout (Duration .ofSeconds (10 )).build (); ) {
328+ // Should succeed with proper JWT - authentication passes
329+ client .initialize ();
330+
331+ var tools = client .listTools ();
332+
333+ log .info ("Successfully listed {} tools" , tools .tools ().size ());
334+ assertThat (tools ).isNotNull ();
335+ assertThat (tools .tools ()).isNotNull ();
336+ assertThat (tools .tools ()).isNotEmpty ();
337+
338+ // Test calling a tool that requires auth metadata - should fail with Forbidden
339+ var myTableTool =
340+ tools .tools ().stream ()
341+ .filter (tool -> "GetAuthMyTable" .equals (tool .name ()))
342+ .findFirst ()
343+ .orElseThrow (() -> new RuntimeException ("GetAuthMyTable tool not found" ));
344+ log .info ("Testing tool call: {}" , myTableTool .name ());
345+
346+ var callRequest =
347+ McpSchema .CallToolRequest .builder ()
348+ .name (myTableTool .name ())
349+ .arguments (Map .of ("limit" , 5 ))
350+ .build ();
351+
352+ // Should throw exception when calling tool with missing required claims
353+ assertThatThrownBy (() -> client .callTool (callRequest ))
354+ .isInstanceOf (RuntimeException .class )
355+ .hasMessageContaining ("Forbidden" );
356+ }
357+ }
358+
277359 @ Test
278360 @ SneakyThrows
279361 void givenJwt_whenUnauthenticatedRest_thenReturns401 () {
280- compileAndStartServerWithDatabase ("jwt-authorized-base.sqrl" , testDir );
362+ compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
281363
282364 var restUrl =
283365 String .format ("http://localhost:%d" , serverContainer .getMappedPort (HTTP_SERVER_PORT ));
@@ -289,8 +371,8 @@ void givenJwt_whenUnauthenticatedRest_thenReturns401() {
289371 .decoder (new JacksonDecoder ())
290372 .target (RestClient .class , restUrl );
291373
292- // When JWT is enabled, all REST endpoints require authentication
293- assertThatThrownBy (() -> restClient .getMyTable (5 ))
374+ // AuthMyTable endpoint requires authentication
375+ assertThatThrownBy (() -> restClient .getAuthMyTable (5 ))
294376 .isInstanceOf (FeignException .Unauthorized .class )
295377 .hasMessageContaining ("JWT auth failed" );
296378 }
@@ -312,7 +394,7 @@ void givenJwt_whenAuthenticatedRest_thenSucceeds() {
312394 .target (RestClient .class , restUrl );
313395
314396 // REST endpoint should work with valid JWT
315- var response = restClient .getMyTableWithAuth (jwtToken , 5 );
397+ var response = restClient .getAuthMyTableWithAuth (jwtToken , 5 );
316398 log .info ("REST endpoint response with JWT: {}" , response );
317399
318400 assertThat (response ).isNotNull ();
@@ -321,19 +403,53 @@ void givenJwt_whenAuthenticatedRest_thenSucceeds() {
321403 assertThat (response .data ).allSatisfy (item -> assertThat (item .val ).isPositive ());
322404 }
323405
406+ @ Test
407+ @ SneakyThrows
408+ void givenJwt_whenMissingRequiredClaimsRest_thenReturns403 () {
409+ compileAndStartServer ("jwt-authorized-base.sqrl" , testDir );
410+
411+ var restUrl =
412+ String .format ("http://localhost:%d" , serverContainer .getMappedPort (HTTP_SERVER_PORT ));
413+ var jwtToken = generateJwtToken (Map .of ());
414+ log .info ("Testing REST endpoint with JWT missing claims: {}" , restUrl );
415+
416+ try {
417+ var restClient =
418+ Feign .builder ()
419+ .encoder (new JacksonEncoder ())
420+ .decoder (new JacksonDecoder ())
421+ .target (RestClient .class , restUrl );
422+
423+ // Valid token but missing required claims should return 403 Forbidden
424+ assertThatThrownBy (() -> restClient .getAuthMyTableWithAuth (jwtToken , 5 ))
425+ .isInstanceOf (FeignException .Forbidden .class );
426+ } finally {
427+ var logs = serverContainer .getLogs ();
428+ log .info ("Detailed MCP Validation Results:" );
429+ System .out .println (logs );
430+ }
431+ }
432+
324433 private String generateJwtToken () {
434+ return generateJwtToken (Map .of ("val" , 1 , "values" , List .of (1 , 2 , 3 )));
435+ }
436+
437+ private String generateJwtToken (Map <String , Object > claims ) {
325438 var now = Instant .now ();
326439 var expiration = now .plus (1 , ChronoUnit .HOURS );
327440
328- return Jwts .builder ()
329- .issuer ("my-test-issuer" )
330- .audience ()
331- .add ("my-test-audience" )
332- .and ()
333- .issuedAt (Date .from (now ))
334- .expiration (Date .from (expiration ))
335- .claim ("val" , 1 )
336- .claim ("values" , List .of (1 , 2 , 3 ))
441+ var builder =
442+ Jwts .builder ()
443+ .issuer ("my-test-issuer" )
444+ .audience ()
445+ .add ("my-test-audience" )
446+ .and ()
447+ .issuedAt (Date .from (now ))
448+ .expiration (Date .from (expiration ));
449+
450+ claims .forEach (builder ::claim );
451+
452+ return builder
337453 .signWith (
338454 new SecretKeySpec (
339455 "testSecretThatIsAtLeast256BitsLong32Chars" .getBytes (UTF_8 ), "HmacSHA256" ))
0 commit comments