Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7ecd57c
Initial commit - use a specific rather than a generic message.
iansergeant Jul 13, 2026
8e8fefa
Merge branch 'v3.x.x' into reboot/specific-message-for-provider-usage
iansergeant42 Jul 13, 2026
20c0e3c
Apply suggestions from code review
janan07 Jul 14, 2026
b64e4f8
Update common-service-core/src/main/resources/core-log-messages.yml
iansergeant42 Jul 14, 2026
b595bde
Apply suggestion from @iansergeant42
iansergeant42 Jul 14, 2026
e6fd29a
Update zaas-service/src/main/java/org/zowe/apiml/zaas/security/config…
iansergeant42 Jul 14, 2026
ff38151
Update zaas-service/src/main/java/org/zowe/apiml/zaas/security/config…
iansergeant42 Jul 14, 2026
60dd373
Merge branch 'v3.x.x' into reboot/specific-message-for-provider-usage
iansergeant42 Jul 14, 2026
b801d89
Merge branch 'v3.x.x' into reboot/specific-message-for-provider-usage
janan07 Jul 14, 2026
3dedd00
Merge branch 'v3.x.x' into reboot/specific-message-for-provider-usage
iansergeant42 Jul 14, 2026
21114a1
Code review.
iansergeant Jul 14, 2026
25e749e
Merge remote-tracking branch 'origin/v3.x.x' into reboot/jwt-tokens-p…
iansergeant Jul 15, 2026
b504e92
Initial commit
iansergeant Jul 15, 2026
5ac0eea
Checkstyle fixes
iansergeant Jul 15, 2026
01272d4
Checkstyle fixes
iansergeant Jul 15, 2026
1b683ca
Failing tests
iansergeant Jul 15, 2026
64d1179
Merge branch 'v3.x.x' into reboot/jwt-tokens-passed-in-headers
iansergeant42 Jul 15, 2026
cfea024
Test coverage
iansergeant Jul 16, 2026
0496d8c
Merge branch 'reboot/jwt-tokens-passed-in-headers' of https://github.…
iansergeant Jul 16, 2026
32530e4
Merge remote-tracking branch 'origin/v3.x.x' into reboot/jwt-tokens-p…
iansergeant Jul 16, 2026
00f0066
Code review changes.
iansergeant Jul 20, 2026
5aa251a
Changes to improve coverage
iansergeant Jul 21, 2026
ae882a1
Checkstyle
iansergeant Jul 21, 2026
9540e6f
Checkstyle
iansergeant Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,18 @@ Logout handler is implemented in Spring Security (see WebSecurityConfig)
/**
* Invalidate JWT, hidden endpoint undocumented
*
* @param token The JWT token to invalidate
* @return
*/
@Hidden
@DeleteMapping(path = "/invalidate/{token}")
public Mono<ResponseEntity<Void>> invalidateJwtToken(@PathVariable String token) {
@DeleteMapping(path = "/invalidate")
public Mono<ResponseEntity<Void>> invalidateJwtToken(@RequestHeader("Authorization") String authHeader) {
try {
var app = peerAwareInstanceRegistry.getApplications().getRegisteredApplications(CoreService.GATEWAY.getServiceId());
var invalidated = authenticationService.invalidateJwtTokenGateway(token, false, app);
if (!authHeader.startsWith("Bearer ")) {
return Mono.just(ResponseEntity.status(SC_BAD_REQUEST).build());
}
final var jwtToken = authHeader.substring(7).trim(); //Starts with "Bearer "
var invalidated = authenticationService.invalidateJwtTokenGateway(jwtToken, false, app);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the token is missing?

@iansergeant42 iansergeant42 Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an isEmpty() check. The token cannot be null at this stage as "Bearer ".substring(7) is empty. I'll add a test for this.

EDIT - use of @RequestHeader("Authorization") means that you see SC_BAD_REQUEST automatically. There's a test for this.

return Mono.just(ResponseEntity.status(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE).build());
} catch (TokenNotValidException e) {
return Mono.just(ResponseEntity.status(SC_BAD_REQUEST).build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import java.net.URI;

import static io.restassured.RestAssured.given;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
import static org.apache.http.HttpStatus.SC_OK;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
Expand All @@ -33,6 +36,7 @@
@Import(ReactiveAuthenticationControllerTests.MockRegisterToApiLayerConfig.class)
class ReactiveAuthenticationControllerTests extends AcceptanceTestWithMockServices {

private static final String BEARER = "Bearer ";
private static final String REFRESH_ENDPOINT = "/gateway/api/v1/auth/refresh";
private static final String LOGIN_ENDPOINT = "/gateway/api/v1/auth/login";
private static final String AUTH_COOKIE = "apimlAuthenticationToken";
Expand Down Expand Up @@ -150,11 +154,12 @@ void whenInvalidateJwt_thenRequireCertificateAuthentication() {

@Test
void whenInvalidate_wrongMethod_thenFail() {
var token = login();

var token = login();
given()
.header(AUTHORIZATION, BEARER + token)
.when()
.get(URI.create(basePath + INVALIDATE_JWT_ENDPOINT + "/" + token))
.get(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given statement should have .header(AUTHORIZATION, "Baerer " + token)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added .header(AUTHORIZATION, "Bearer " + token)

.then()
.statusCode(SC_METHOD_NOT_ALLOWED);
}
Expand All @@ -166,10 +171,57 @@ void whenInvalidateJwt_withCert_thenSuccess() {
given()
.config(SslContext.clientCertApiml)
.when()
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT + "/" + token))
.header(AUTHORIZATION, BEARER + token)
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))
.then()
.statusCode(200);
.statusCode(SC_OK);
given()
.config(SslContext.clientCertApiml)
.when()
.header(AUTHORIZATION, "Bearer " + token) //this will work as the token has its extra space trimmed
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))
.then()
.statusCode(SC_OK);
}

@Test
void whenNoHeader_withCert_then401() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in name 401 -> 400


given()
.config(SslContext.clientCertApiml)
.when()
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))
.then()
.statusCode(SC_BAD_REQUEST);

}

@Test
void whenInvalidHeader_withCert_then400() {

given()
.config(SslContext.clientCertApiml)
.when()
.header(AUTHORIZATION, "wibble")
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))
.then()
.statusCode(SC_BAD_REQUEST);

}

@Test
void whenEmptyToken_withCert_then400() {

given()
.config(SslContext.clientCertApiml)
.when()
.header(AUTHORIZATION, BEARER)
.delete(URI.create(basePath + INVALIDATE_JWT_ENDPOINT))
.then()
.statusCode(SC_BAD_REQUEST);

}

Comment thread
richard-salac marked this conversation as resolved.
@TestConfiguration
public static class MockRegisterToApiLayerConfig {
@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@ExtendWith(MockitoExtension.class)
class ReactiveAuthenticationControllerTest {

private static final String BEARER = "Bearer ";
@Mock private AuthenticationService authenticationService;
@Mock private PeerAwareInstanceRegistryImpl peerAwareInstanceRegistry;
@Mock private HttpUtils httpUtils;
Expand Down Expand Up @@ -91,7 +92,7 @@ void invalidateJwtToken_success() {
when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(mockApplication);
when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class))).thenReturn(true);

var result = controller.invalidateJwtToken(jwtToInvalidate);
var result = controller.invalidateJwtToken(BEARER + jwtToInvalidate);

StepVerifier.create(result)
.expectNextMatches(responseEntity -> HttpStatus.OK.equals(responseEntity.getStatusCode()))
Expand All @@ -107,7 +108,7 @@ void invalidateJwtToken_serviceUnavailable() {
when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(mockApplication);
when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class))).thenReturn(false);

var result = controller.invalidateJwtToken(jwtToInvalidate);
var result = controller.invalidateJwtToken(BEARER + jwtToInvalidate);

StepVerifier.create(result)
.expectNextMatches(responseEntity -> HttpStatus.SERVICE_UNAVAILABLE.equals(responseEntity.getStatusCode()))
Expand All @@ -124,7 +125,7 @@ void invalidateJwtToken_tokenNotValidException() {
when(authenticationService.invalidateJwtTokenGateway(eq(jwtToInvalidate), eq(false), any(Application.class)))
.thenThrow(new TokenNotValidException("Token is not valid"));

var result = controller.invalidateJwtToken(jwtToInvalidate);
var result = controller.invalidateJwtToken(BEARER + jwtToInvalidate);

StepVerifier.create(result)
.expectNextMatches(responseEntity -> HttpStatus.BAD_REQUEST.equals(responseEntity.getStatusCode()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.zowe.apiml.util.http.HttpRequestUtils;

import static io.restassured.RestAssured.given;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import static org.hamcrest.CoreMatchers.equalTo;
Expand Down Expand Up @@ -53,7 +54,7 @@ void thenAuthenticate(String endpoint) {
String token = SecurityUtils.gatewayToken(USERNAME, PASSWORD);
// Gateway request to url
given()
.header("Authorization", "Bearer " + token)
.header(AUTHORIZATION, "Bearer " + token)
.when()
.get(HttpRequestUtils.getUriFromGateway(endpoint))
.then()
Expand All @@ -77,7 +78,7 @@ void thenReturnUnauthorized(String endpoint) {
String expectedMessage = "The request has not been applied because it lacks valid authentication credentials.";
// Gateway request to url
given()
.header("Authorization", "Bearer invalidToken")
.header(AUTHORIZATION, "Bearer invalidToken")
.when()
.get(HttpRequestUtils.getUriFromGateway(endpoint))
.then()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public class AuthController {
private static final ObjectWriter writer = new ObjectMapper().writer();

public static final String CONTROLLER_PATH = "/zaas/api/v1/auth"; // NOSONAR: URL is always using / to separate path segments
public static final String INVALIDATE_PATH = "/invalidate/**"; // NOSONAR
public static final String INVALIDATE_PATH = "/invalidate"; // NOSONAR
public static final String DISTRIBUTE_PATH = "/distribute/**"; // NOSONAR
public static final String PUBLIC_KEYS_PATH = "/keys/public"; // NOSONAR
public static final String ACCESS_TOKEN_REVOKE = "/access-token/revoke"; // NOSONAR
Expand All @@ -114,12 +114,17 @@ public class AuthController {
@ApiResponse(responseCode = "400", description = "Invalid token"),
@ApiResponse(responseCode = "503", description = "Authentication service is not available")
})
public void invalidateJwtToken(HttpServletRequest request, HttpServletResponse response) {
final String endpoint = "/auth/invalidate/";
final String uri = request.getRequestURI();
final int index = uri.indexOf(endpoint);
public void invalidateJwtToken(@RequestHeader("Authorization") String authHeader, HttpServletResponse response) {
if (!authHeader.startsWith("Bearer ")) {
response.setStatus(SC_BAD_REQUEST);
return;
}
final var jwtToken = authHeader.substring(7).trim();
if (jwtToken.isEmpty()) { //it cannot be null, as "Bearer".substring(7) is empty, not null
response.setStatus(SC_BAD_REQUEST);
return;
Comment on lines +123 to +125

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rely on @RequestHeader like in modulith?

}

final String jwtToken = uri.substring(index + endpoint.length());
try {
final boolean invalidated = authenticationService.invalidateJwtToken(jwtToken, false);
response.setStatus(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
Expand Down Expand Up @@ -275,11 +277,10 @@ public Boolean invalidateJwtTokenGateway(String jwtToken, boolean distribute, Ap
* Obtain URL to use to invalidate a JWT
*
* @param instanceInfo Registration data for the authentication service used
* @param jwtToken JWT token to invalidate
* @return
* @return the URL
*/
protected String getInvalidateUrl(InstanceInfo instanceInfo, String jwtToken) {
return EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate/" + jwtToken;
protected String getInvalidateUrl(InstanceInfo instanceInfo) {
return EurekaUtils.getUrl(instanceInfo) + AuthController.CONTROLLER_PATH + "/invalidate";
}

private boolean invalidateTokenOnAnotherInstance(String jwtToken, Application application) {
Expand All @@ -295,9 +296,17 @@ private boolean invalidateTokenOnAnotherInstance(String jwtToken, Application ap
continue;
}

final String url = getInvalidateUrl(instanceInfo, jwtToken);
final String url = getInvalidateUrl(instanceInfo);
try {
restTemplate.delete(url);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + jwtToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use org.apache.http.HttpHeaders.AUTHORIZATION whenever possible, not only in tests

HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
restTemplate.exchange(
url,
HttpMethod.DELETE,
requestEntity,
Void.class
);
} catch (HttpClientErrorException e) {
log.debug("Problem invalidating token on another instance url {}", url, e);
returnValue = Boolean.FALSE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ public ModulithAuthenticationService(ApplicationContext applicationContext,
}

@Override
protected String getInvalidateUrl(InstanceInfo instanceInfo, String jwtToken) {
return EurekaUtils.getUrl(instanceInfo) + "/gateway/api/v1/auth/invalidate/" + jwtToken;
protected String getInvalidateUrl(InstanceInfo instanceInfo) {
return EurekaUtils.getUrl(instanceInfo) + "/gateway/api/v1/auth/invalidate/";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.zowe.apiml.message.yaml.YamlMessageService;
import org.zowe.apiml.security.common.token.AccessTokenProvider;
import org.zowe.apiml.security.common.token.TokenAuthentication;
import org.zowe.apiml.security.common.token.TokenNotValidException;
import org.zowe.apiml.zaas.security.service.AuthenticationService;
import org.zowe.apiml.zaas.security.service.JwtSecurity;
import org.zowe.apiml.zaas.security.service.token.OIDCTokenProvider;
Expand All @@ -60,6 +61,9 @@
@ExtendWith(SpringExtension.class)
class AuthControllerTest {

private static final String INVALIDATE = "/zaas/api/v1/auth/invalidate";
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer ";
private AuthController authController;
private MockMvc mockMvc;

Expand Down Expand Up @@ -102,12 +106,33 @@ void setUp() throws JSONException, JoseException {
@Test
void invalidateJwtToken() throws Exception {
when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE);
this.mockMvc.perform(delete("/zaas/api/v1/auth/invalidate/a/b")).andExpect(status().is(SC_OK));
mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, BEARER + "a/b"))
.andExpect(status().is(SC_OK));

when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE);
this.mockMvc.perform(delete("/zaas/api/v1/auth/invalidate/abcde")).andExpect(status().is(SC_OK));
mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, BEARER + "abcde"))
.andExpect(status().is(SC_OK));

this.mockMvc.perform(delete("/zaas/api/v1/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE));
when(authenticationService.invalidateJwtToken("fghij", false)).thenThrow(new TokenNotValidException("invalid"));
mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, BEARER + "fghij"))
.andExpect(status().is(SC_BAD_REQUEST));

mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, BEARER + "xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE));

mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, "wibble")).andExpect(status().is(SC_BAD_REQUEST));

mockMvc.perform(delete(INVALIDATE)).andExpect(status().is(SC_BAD_REQUEST));

mockMvc.perform(delete(INVALIDATE)
.header(AUTHORIZATION, BEARER)).andExpect(status().is(SC_BAD_REQUEST));

mockMvc.perform(get(INVALIDATE)
.header(AUTHORIZATION, BEARER + "xyz")).andExpect(status().is(SC_METHOD_NOT_ALLOWED));

verify(authenticationService, times(1)).invalidateJwtToken("abcde", false);
verify(authenticationService, times(1)).invalidateJwtToken("a/b", false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.test.context.ContextConfiguration;
Expand Down Expand Up @@ -489,7 +492,14 @@ void givenTokenWasAlreadyInvalidateOnAnotherInstance_thenReturnInvalidatedTrue()
stubJWTSecurityForSign();
authConfigurationProperties.getTokenProperties().setIssuer(ZOSMF);
String token = authService.createJwtToken("user", DOMAIN, null);
doNothing().when(restTemplate).delete("http://localhost:0/zaas/api/v1/auth/invalidate/" + token);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Void> responseEntity = ResponseEntity.ok().build();
when(restTemplate.exchange("http://localhost:0/zaas/api/v1/auth/invalidate",
HttpMethod.DELETE,
requestEntity,
Void.class)).thenReturn(responseEntity);
doThrow(new BadCredentialsException("Invalid Credentials")).when(zosmfService).invalidate(ZosmfService.TokenType.JWT, token);

assertTrue(authService.invalidateJwtToken(token, true));
Expand Down Expand Up @@ -763,7 +773,7 @@ void givenHttpClientErrorOnInvalidateAnotherInstance_thenReturnFalse() {

doThrow(HttpClientErrorException.BadRequest.class)
.when(restTemplate)
.delete(anyString());
.exchange(anyString(),any(),any(),(Class<Object>)any());

assertFalse(authService.invalidateJwtToken(token, true));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.zaas.security.service;

import com.netflix.appinfo.InstanceInfo;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class ModulithAuthenticationServiceTest {
@Test
void getInvalidateUrl() {
ModulithAuthenticationService service = new ModulithAuthenticationService(null, null, null, null, null, null, null, null);
InstanceInfo instanceInfo = mock(InstanceInfo.class);
when(instanceInfo.getHostName()).thenReturn("localhost");
when(instanceInfo.getSecurePort()).thenReturn(443);
when(instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE)).thenReturn(true);
String invalidateUrl = service.getInvalidateUrl(instanceInfo);
assertEquals("https://localhost:443/gateway/api/v1/auth/invalidate/", invalidateUrl);
}
}
Loading