Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion apiml/src/main/java/org/zowe/apiml/util/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public Mono<String> getBearerTokenFromHeaderReactive(ServerWebExchange exchange)
public Mono<String> getCookieValue(ServerWebExchange exchange, String cookieName) {
return Mono.justOrEmpty(exchange)
.mapNotNull(ex -> ex.getRequest().getCookies().getFirst(cookieName))
.map(HttpCookie::getValue);
.map(HttpCookie::getValue)
.filter(value -> !value.isEmpty());
}
}
13 changes: 13 additions & 0 deletions apiml/src/test/java/org/zowe/apiml/util/HttpUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ void shouldExtractTokenFromCookie() {
.verifyComplete();
}

@Test
void shouldReturnEmptyForEmptyCookieValue() {
var cookie = new HttpCookie("apimlAuthenticationToken", "");
var request = MockServerHttpRequest.get("/logout")
.cookie(cookie)
.build();

var exchange = MockServerWebExchange.from(request);

StepVerifier.create(httpUtils.getTokenFromRequest(exchange))
.verifyComplete();
}

@Test
void shouldExtractTokenFromAuthorizationHeader() {
var request = MockServerHttpRequest.get("/logout")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ public String passTicket(String jwtToken, String applicationId) throws ZaasClien

@Override
public void logout(String jwtToken) throws ZaasConfigurationException, ZaasClientException {
if (jwtToken == null || jwtToken.isEmpty()) {
throw new ZaasClientException(ZaasClientErrorCodes.TOKEN_NOT_PROVIDED);
}
tokens.logout(jwtToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ void givenValidToken_whenLogoutIsCalled_thenSuccessLogout() {
assertDoesNotThrow(() -> underTest.logout("apimlAuthenticationToken=" + VALID_TOKEN));
}

@Test
void givenEmptyToken_whenLogout_thenThrowsException() {
assertThrows(ZaasClientException.class, () -> underTest.logout(""));
}

@Test
void givenNullToken_whenLogout_thenThrowsException() {
assertThrows(ZaasClientException.class, () -> underTest.logout(null));
}

@Test
void givenNullKeyStorePath_whenTheClientIsConstructed_thenExceptionIsThrown() {
ConfigProperties config = new ConfigProperties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@
*/
public Boolean invalidateJwtToken(String jwtToken, boolean distribute) {
log.debug("Invalidating JWT: ...{}", StringUtils.right(jwtToken, 15));
if (jwtToken == null || jwtToken.isEmpty()) {
throw new TokenNotProvidedException("No JWT token provided for invalidation");
}
if (jwtToken != null && isInvalidated(jwtToken)) {
return Boolean.TRUE;
}
Expand All @@ -210,6 +213,15 @@
}

private Boolean doInvalidate(String jwtToken, boolean distribute, Application app) {
// Validate token format FIRST — before any peer calls
// If token is unparseable, throw immediately with descriptive exception
final QueryResponse queryResponse;
try {
queryResponse = parseJwtToken(jwtToken).getQueryResponse();
} catch (TokenNotValidException | TokenExpireException | TokenFormatNotValidException e) {
throw e; // Always propagate — never suppress based on HA state

Check warning on line 222 in zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add logic to this catch clause or eliminate it and rethrow the exception automatically.

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZ7_7aH-F6k1SnWjCnee&open=AZ7_7aH-F6k1SnWjCnee&pullRequest=4757
}

boolean isInvalidatedOnAnotherInstance = false;
if (distribute) {
isInvalidatedOnAnotherInstance = invalidateTokenOnAnotherInstance(jwtToken, app);
Expand All @@ -218,7 +230,6 @@
}
}

final QueryResponse queryResponse = parseJwtToken(jwtToken).getQueryResponse();
try {
switch (queryResponse.getSource()) {
case ZOWE:
Expand Down Expand Up @@ -264,7 +275,10 @@
* @return state of invalidate (true - token was invalidated)
*/
public Boolean invalidateJwtTokenGateway(String jwtToken, boolean distribute, Application app) {
if (jwtToken == null || jwtToken.isEmpty()) {
throw new TokenNotProvidedException("No JWT token provided for invalidation");
}
if (jwtToken != null && isInvalidated(jwtToken)) {

Check warning on line 281 in zaas-service/src/main/java/org/zowe/apiml/zaas/security/service/AuthenticationService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this expression which always evaluates to "true"

See more on https://sonarcloud.io/project/issues?id=zowe_api-layer&issues=AZ7_7aH-F6k1SnWjCned&open=AZ7_7aH-F6k1SnWjCned&pullRequest=4757
return Boolean.TRUE;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.zowe.apiml.security.common.token.TokenAuthentication;
import org.zowe.apiml.security.common.token.TokenExpireException;
import org.zowe.apiml.security.common.token.TokenNotValidException;
import org.zowe.apiml.security.common.token.TokenNotProvidedException;
import org.zowe.apiml.security.common.util.JWTTestUtils;
import org.zowe.apiml.security.common.util.JwtUtils;
import org.zowe.apiml.util.CacheUtils;
Expand Down Expand Up @@ -449,8 +450,11 @@ class GivenInvalidateZosmfTokenTest {

@Test
void givenNoInstancesAvailable_thenReturnFalse() {
stubJWTSecurityForSign();
authConfigurationProperties.getTokenProperties().setIssuer(ZOSMF);
String token = authService.createJwtToken("user", "dom", null);
when(eurekaClient.getApplication(CoreService.ZAAS.getServiceId())).thenReturn(null);
assertFalse(authService.invalidateJwtToken(JWT_TOKEN, true));
assertFalse(authService.invalidateJwtToken(token, true));
}

@Test
Expand Down Expand Up @@ -737,7 +741,8 @@ void whenInstancesAvailable_thenReturnSuccess() {

@Test
void givenHttpClientErrorOnInvalidateAnotherInstance_thenReturnFalse() {
String token = "jwtToken";
stubJWTSecurityForSign();
String token = authService.createJwtToken("user", "dom", null);

Application application = mock(Application.class);
ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class);
Expand Down Expand Up @@ -768,4 +773,69 @@ void givenHttpClientErrorOnInvalidateAnotherInstance_thenReturnFalse() {

}
}

@Nested
class GivenTokenInvalidationTokenValidationTest {

@Test
void whenEmptyToken_thenThrowTokenNotProvidedException() {
assertThrows(TokenNotProvidedException.class, () ->
authService.invalidateJwtToken("", true));
}

@Test
void whenNullToken_thenThrowTokenNotProvidedException() {
assertThrows(TokenNotProvidedException.class, () ->
authService.invalidateJwtToken(null, true));
}

@Test
void whenEmptyTokenGateway_thenThrowTokenNotProvidedException() {
Application app = mock(Application.class);
assertThrows(TokenNotProvidedException.class, () ->
authService.invalidateJwtTokenGateway("", true, app));
}

@Test
void whenNullTokenGateway_thenThrowTokenNotProvidedException() {
Application app = mock(Application.class);
assertThrows(TokenNotProvidedException.class, () ->
authService.invalidateJwtTokenGateway(null, true, app));
}

@Test
void whenUnparseableToken_thenThrowBeforeDistribution() {
// A non-empty, non-null token that parseJwtToken cannot parse
// Verification: with distribute=true, the distribution should never happen
// because parseJwtToken throws TokenNotValidException first
assertThrows(TokenNotValidException.class, () ->
authService.invalidateJwtToken("not_a_valid_jwt_token", true));
}

@Test
void whenValidToken_thenDistributionStillHappens() {
Application application = mock(Application.class);
ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class);
InstanceInfo instanceInfo = mock(InstanceInfo.class);
InstanceInfo otherInstance = mock(InstanceInfo.class);

stubJWTSecurityForSign();
String token = authService.createJwtToken("user", "dom", null);

when(eurekaClient.getApplication(CoreService.ZAAS.getServiceId())).thenReturn(application);
when(eurekaClient.getApplicationInfoManager()).thenReturn(applicationInfoManager);
when(applicationInfoManager.getInfo()).thenReturn(instanceInfo);
when(instanceInfo.getInstanceId()).thenReturn("self");
when(application.getInstances()).thenReturn(List.of(instanceInfo, otherInstance));
when(otherInstance.getInstanceId()).thenReturn("peer");
when(otherInstance.getSecurePort()).thenReturn(100);
when(otherInstance.getHostName()).thenReturn("localhost");
doNothing().when(restTemplate).delete(anyString());

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

// Verify that peer distribution happened (parseJwtToken succeeded first)
verify(restTemplate, times(1)).delete(anyString());
}
}
}
Loading