Skip to content
Open
8 changes: 8 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ jobs:
APIML_SECURITY_AUTH_PROVIDER: saf
APIML_DISCOVERY_ALLPEERSURLS: https://apiml-2:10011/eureka,https://apiml:10011/eureka
APIML_SERVICE_HOSTNAME: apiml
CACHING_STORAGE_INFINISPAN_INITIALHOSTS: "apiml[7600],apiml-2[7600]"
logbackService: ZWEAGW1
JGROUPS_BIND_PORT: 7600
JGROUPS_BIND_ADDRESS: apiml
JGROUPS_KEYEXCHANGE_PORT: 7601
apiml-2:
image: ghcr.io/balhar-jakub/apiml:${{ github.run_id }}-${{ github.run_number }}
env:
Expand All @@ -223,7 +227,11 @@ jobs:
APIML_SECURITY_AUTH_PROVIDER: saf
APIML_DISCOVERY_ALLPEERSURLS: https://apiml:10011/eureka,https://apiml-2:10011/eureka
APIML_SERVICE_HOSTNAME: apiml-2
CACHING_STORAGE_INFINISPAN_INITIALHOSTS: "apiml[7600],apiml-2[7600]"
logbackService: ZWEAGW2
JGROUPS_BIND_PORT: 7600
JGROUPS_BIND_ADDRESS: apiml-2
JGROUPS_KEYEXCHANGE_PORT: 7601
discoverable-client:
image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }}
env:
Expand Down
20 changes: 15 additions & 5 deletions apiml/src/main/java/org/zowe/apiml/filter/LogoutHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
package org.zowe.apiml.filter;

import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
Expand All @@ -38,6 +40,14 @@ public class LogoutHandler implements ServerLogoutHandler {
private final FailedAuthenticationWebHandler failure;
private final PeerAwareInstanceRegistryImpl peerAwareInstanceRegistry;
private final HttpUtils httpUtils;
private final ApplicationContext applicationContext;

private boolean distribute;

@PostConstruct
void init() {
distribute = !applicationContext.containsBean("infinispanConfig");
}

@Override
public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
Expand All @@ -51,21 +61,21 @@ private Mono<Void> invalidateJwtToken(String token, WebFilterExchange exchange)
);
}

if (Boolean.TRUE.equals(authenticationService.isInvalidated(token))) {
return failure.onAuthenticationFailure(exchange,new TokenNotValidException("The token you are trying to logout is not valid"));
if (authenticationService.isInvalidated(token)) {
return failure.onAuthenticationFailure(exchange, new TokenNotValidException("The token you are trying to logout is not valid"));
} else {
try {
var app = peerAwareInstanceRegistry.getApplications().getRegisteredApplications(CoreService.GATEWAY.getServiceId());
authenticationService.invalidateJwtTokenGateway(token, true, app);
authenticationService.invalidateJwtTokenGateway(token, distribute, app);
} catch (TokenNotValidException e) {
// TokenNotValidException thrown in cases where the format is not valid
return failure.onAuthenticationFailure(exchange,new TokenFormatNotValidException(e.getMessage()));
return failure.onAuthenticationFailure(exchange, new TokenFormatNotValidException(e.getMessage()));
} catch (AuthenticationException e) {
return failure.onAuthenticationFailure(exchange, e);
} catch (Exception e) {
// Catch any issue like ServiceNotAccessibleException, throw TokenNotValidException
// so a 401 is returned. Returning 500 gives information about the system and is thus avoided.
return failure.onAuthenticationFailure(exchange, new TokenNotValidException("Error while logging out token"));
return failure.onAuthenticationFailure(exchange, new TokenNotValidException("Error while logging out token"));
}
return Mono.empty();
}
Expand Down
37 changes: 34 additions & 3 deletions apiml/src/test/java/org/zowe/apiml/filter/LogoutHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

package org.zowe.apiml.filter;

import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
Expand All @@ -25,6 +27,7 @@
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.web.server.WebFilterChain;
import org.zowe.apiml.handler.FailedAuthenticationWebHandler;
import org.zowe.apiml.product.constants.CoreService;
import org.zowe.apiml.security.common.config.AuthConfigurationProperties;
import org.zowe.apiml.security.common.token.TokenFormatNotValidException;
import org.zowe.apiml.security.common.token.TokenNotValidException;
Expand All @@ -46,6 +49,7 @@ class LogoutHandlerTest {
@Mock private AuthenticationService authenticationService;
@Mock private FailedAuthenticationWebHandler failureHandler;
@Mock private PeerAwareInstanceRegistryImpl registry;
@Mock private ApplicationContext applicationContext;
private HttpUtils httpUtils = new HttpUtils(new AuthConfigurationProperties()) {
{
readConfig();
Expand All @@ -56,7 +60,7 @@ class LogoutHandlerTest {

@BeforeEach
void setUp() {
logoutHandler = new LogoutHandler(authenticationService, failureHandler, registry, httpUtils);
logoutHandler = new LogoutHandler(authenticationService, failureHandler, registry, httpUtils, applicationContext);
}

@Test
Expand Down Expand Up @@ -119,22 +123,49 @@ void shouldCallFailureHandlerOnUnexpectedException() {
}

@Test
void shouldInvalidateValidTokenSuccessfully() {
void shouldInvalidateValidTokenSuccessfully_WithInfinispan() {
var request = MockServerHttpRequest.get("/logout")
.header(HttpHeaders.AUTHORIZATION, "Bearer token123")
.build();
var exchange = MockServerWebExchange.from(request);
WebFilterChain mockChain = mock(WebFilterChain.class);
var webFilterExchange = new WebFilterExchange(exchange, mockChain);

when(applicationContext.containsBean("infinispanConfig")).thenReturn(true);
logoutHandler.init();

when(authenticationService.isInvalidated("token123")).thenReturn(false);
Applications mockApplications = mock(Applications.class);
when(registry.getApplications()).thenReturn(mockApplications);
var application = mock(Application.class);
when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(application);
StepVerifier.create(logoutHandler.logout(webFilterExchange, mock(Authentication.class)))
.verifyComplete();

verify(authenticationService).invalidateJwtTokenGateway(eq("token123"), eq(false), eq(application));
}

@Test
void shouldInvalidateValidTokenSuccessfully_WithoutInfinispan() {
var request = MockServerHttpRequest.get("/logout")
.header(HttpHeaders.AUTHORIZATION, "Bearer token123")
.build();
var exchange = MockServerWebExchange.from(request);
WebFilterChain mockChain = mock(WebFilterChain.class);
var webFilterExchange = new WebFilterExchange(exchange, mockChain);

when(applicationContext.containsBean("infinispanConfig")).thenReturn(false);
logoutHandler.init();

when(authenticationService.isInvalidated("token123")).thenReturn(false);
Applications mockApplications = mock(Applications.class);
when(registry.getApplications()).thenReturn(mockApplications);
var application = mock(Application.class);
when(mockApplications.getRegisteredApplications(CoreService.GATEWAY.getServiceId())).thenReturn(application);
StepVerifier.create(logoutHandler.logout(webFilterExchange, mock(Authentication.class)))
.verifyComplete();

verify(authenticationService).invalidateJwtTokenGateway(eq("token123"), eq(true), any());
verify(authenticationService).invalidateJwtTokenGateway(eq("token123"), eq(true), eq(application));
}

@Test
Expand Down
Loading