From f9106191fd7fd2144d0125e80bf68e038211016e Mon Sep 17 00:00:00 2001 From: anthonyraymond Date: Mon, 9 Apr 2018 23:51:19 +0200 Subject: [PATCH] WebSocketAuthenticatorService & AuthChannelInterceptorAdapter tests --- .../joal/ApplicationClosingListener.java | 1 - .../araymond/joal/web/config/BeanConfig.java | 1 - ...WebSocketAuthenticationSecurityConfig.java | 9 +- .../AuthChannelInterceptorAdapter.java | 11 +- .../WebSocketAuthenticatorService.java | 2 +- ...ocketAuthenticationSecurityConfigTest.java | 5 +- ...thChannelInterceptorAdapterWebAppTest.java | 100 ++++++++++++++++++ .../WebSocketAuthenticatorServiceTest.java | 85 +++++++++++++++ 8 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapterWebAppTest.java create mode 100644 src/test/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorServiceTest.java diff --git a/src/main/java/org/araymond/joal/ApplicationClosingListener.java b/src/main/java/org/araymond/joal/ApplicationClosingListener.java index 3206caf9..03e4ac9b 100644 --- a/src/main/java/org/araymond/joal/ApplicationClosingListener.java +++ b/src/main/java/org/araymond/joal/ApplicationClosingListener.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Component; import javax.inject.Inject; -import java.io.IOException; /** * Created by raymo on 08/07/2017. diff --git a/src/main/java/org/araymond/joal/web/config/BeanConfig.java b/src/main/java/org/araymond/joal/web/config/BeanConfig.java index de4c55af..40d67f3b 100644 --- a/src/main/java/org/araymond/joal/web/config/BeanConfig.java +++ b/src/main/java/org/araymond/joal/web/config/BeanConfig.java @@ -6,7 +6,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import java.io.IOException; diff --git a/src/main/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfig.java b/src/main/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfig.java index 2e67d4d7..aacc6b30 100644 --- a/src/main/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfig.java +++ b/src/main/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfig.java @@ -3,7 +3,6 @@ import com.google.common.annotations.VisibleForTesting; import org.araymond.joal.web.annotations.ConditionalOnWebUi; import org.araymond.joal.web.config.security.websocket.interceptor.AuthChannelInterceptorAdapter; -import org.araymond.joal.web.config.security.websocket.services.WebSocketAuthenticatorService; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -21,11 +20,11 @@ @Configuration @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class WebSocketAuthenticationSecurityConfig extends AbstractWebSocketMessageBrokerConfigurer { - private final WebSocketAuthenticatorService webSocketAuthenticatorService; + private final AuthChannelInterceptorAdapter authChannelInterceptorAdapter; @Inject - public WebSocketAuthenticationSecurityConfig(final WebSocketAuthenticatorService webSocketAuthenticatorService) { - this.webSocketAuthenticatorService = webSocketAuthenticatorService; + public WebSocketAuthenticationSecurityConfig(final AuthChannelInterceptorAdapter authChannelInterceptorAdapter) { + this.authChannelInterceptorAdapter = authChannelInterceptorAdapter; } @Override @@ -40,7 +39,7 @@ public void configureClientInboundChannel(final ChannelRegistration registration @VisibleForTesting ChannelInterceptor[] createChannelInterceptors() { - return new ChannelInterceptor[]{new AuthChannelInterceptorAdapter(this.webSocketAuthenticatorService)}; + return new ChannelInterceptor[]{this.authChannelInterceptorAdapter}; } } diff --git a/src/main/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapter.java b/src/main/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapter.java index 95cc967b..483becfe 100644 --- a/src/main/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapter.java +++ b/src/main/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapter.java @@ -1,5 +1,6 @@ package org.araymond.joal.web.config.security.websocket.interceptor; +import org.araymond.joal.web.annotations.ConditionalOnWebUi; import org.araymond.joal.web.config.security.websocket.services.WebSocketAuthenticatorService; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -9,16 +10,22 @@ import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; /** * Created by raymo on 30/07/2017. */ +@ConditionalOnWebUi +@Component public class AuthChannelInterceptorAdapter extends ChannelInterceptorAdapter { - private static final String USERNAME_HEADER = "X-Joal-Username"; - private static final String TOKEN_HEADER = "X-Joal-Auth-Token"; + static final String USERNAME_HEADER = "X-Joal-Username"; + static final String TOKEN_HEADER = "X-Joal-Auth-Token"; private final WebSocketAuthenticatorService webSocketAuthenticatorService; + @Inject public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) { this.webSocketAuthenticatorService = webSocketAuthenticatorService; } diff --git a/src/main/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorService.java b/src/main/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorService.java index 635c4c53..a17fc4da 100644 --- a/src/main/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorService.java +++ b/src/main/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorService.java @@ -33,7 +33,7 @@ public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final CharSequ if (StringUtils.isBlank(authToken)) { throw new AuthenticationCredentialsNotFoundException("Authentication token was null or empty."); } - if (!appSecretToken.equals(authToken)) { + if (!appSecretToken.contentEquals(authToken)) { throw new BadCredentialsException("Authentication token does not match the expected token"); } diff --git a/src/test/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfigTest.java b/src/test/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfigTest.java index 3775a135..2b993723 100644 --- a/src/test/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfigTest.java +++ b/src/test/java/org/araymond/joal/web/config/security/WebSocketAuthenticationSecurityConfigTest.java @@ -1,5 +1,6 @@ package org.araymond.joal.web.config.security; +import org.araymond.joal.web.config.security.websocket.interceptor.AuthChannelInterceptorAdapter; import org.araymond.joal.web.config.security.websocket.services.WebSocketAuthenticatorService; import org.junit.Test; import org.junit.runner.RunWith; @@ -16,8 +17,8 @@ public class WebSocketAuthenticationSecurityConfigTest { @Test public void shouldRegisterInterceptors() { - final WebSocketAuthenticatorService authService = mock(WebSocketAuthenticatorService.class); - final WebSocketAuthenticationSecurityConfig webSocketAuthenticationSecurityConfig = spy(new WebSocketAuthenticationSecurityConfig(authService)); + final AuthChannelInterceptorAdapter authAdaptor = mock(AuthChannelInterceptorAdapter.class); + final WebSocketAuthenticationSecurityConfig webSocketAuthenticationSecurityConfig = spy(new WebSocketAuthenticationSecurityConfig(authAdaptor)); final ChannelRegistration registration = mock(ChannelRegistration.class); webSocketAuthenticationSecurityConfig.configureClientInboundChannel(registration); diff --git a/src/test/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapterWebAppTest.java b/src/test/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapterWebAppTest.java new file mode 100644 index 00000000..575e0b2f --- /dev/null +++ b/src/test/java/org/araymond/joal/web/config/security/websocket/interceptor/AuthChannelInterceptorAdapterWebAppTest.java @@ -0,0 +1,100 @@ +package org.araymond.joal.web.config.security.websocket.interceptor; + +import org.araymond.joal.TestConstant; +import org.araymond.joal.web.config.WebSocketConfig; +import org.araymond.joal.web.config.security.WebSecurityConfig; +import org.araymond.joal.web.config.security.websocket.services.WebSocketAuthenticatorService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.messaging.WebSocketStompClient; + +import javax.inject.Inject; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = { + AuthChannelInterceptorAdapter.class, + WebSocketConfig.class, + org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration.class, + org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration.class, + org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration.class, + org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.class, + org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration.class, + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.main.web-environment=true", + "joal.ui.path.prefix=" + TestConstant.UI_PATH_PREFIX, + "joal.ui.secret-token=" + TestConstant.UI_SECRET_TOKEN + } +) +@Import(AuthChannelInterceptorAdapterWebAppTest.TestWebSocketAuthenticationConfig.class) +public class AuthChannelInterceptorAdapterWebAppTest { + + @LocalServerPort + private int port; + + @MockBean + private WebSocketAuthenticatorService authenticatorService; + + @TestConfiguration + public static class TestWebSocketAuthenticationConfig extends AbstractWebSocketMessageBrokerConfigurer { + private final AuthChannelInterceptorAdapter authChannelInterceptorAdapter; + @Inject + public TestWebSocketAuthenticationConfig(final AuthChannelInterceptorAdapter authChannelInterceptorAdapter) { + this.authChannelInterceptorAdapter = authChannelInterceptorAdapter; + } + @Override + public void registerStompEndpoints(final StompEndpointRegistry registry) { + // Endpoints are already registered on WebSocketConfig, no need to add more. + } + @Override + public void configureClientInboundChannel(final ChannelRegistration registration) { + registration.interceptors(this.authChannelInterceptorAdapter); + } + } + + @Test + public void shouldCallAuthServiceWhenUserTriesToConnect() throws InterruptedException, ExecutionException, TimeoutException { + final WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient()); + + final StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.add(AuthChannelInterceptorAdapter.USERNAME_HEADER, "john"); + stompHeaders.add(AuthChannelInterceptorAdapter.TOKEN_HEADER, TestConstant.UI_SECRET_TOKEN); + + stompClient.connect("ws://localhost:" + port + "/" + TestConstant.UI_PATH_PREFIX, new WebSocketHttpHeaders(), stompHeaders, new StompSessionHandlerAdapter() { + }).get(10, TimeUnit.SECONDS); + + verify(authenticatorService, times(1)).getAuthenticatedOrFail("john", TestConstant.UI_SECRET_TOKEN); + } +} diff --git a/src/test/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorServiceTest.java b/src/test/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorServiceTest.java new file mode 100644 index 00000000..80039332 --- /dev/null +++ b/src/test/java/org/araymond/joal/web/config/security/websocket/services/WebSocketAuthenticatorServiceTest.java @@ -0,0 +1,85 @@ +package org.araymond.joal.web.config.security.websocket.services; + +import org.araymond.joal.TestConstant; +import org.assertj.core.api.Condition; +import org.junit.Test; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class WebSocketAuthenticatorServiceTest { + + @Test + public void shouldThrowExceptionOnNullOrEmptyUsername() { + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + assertThatThrownBy(() -> authService.getAuthenticatedOrFail(" ", TestConstant.UI_SECRET_TOKEN)) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Username"); + + assertThatThrownBy(() -> authService.getAuthenticatedOrFail("", TestConstant.UI_SECRET_TOKEN)) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Username"); + + assertThatThrownBy(() -> authService.getAuthenticatedOrFail(null, TestConstant.UI_SECRET_TOKEN)) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Username"); + } + + @Test + public void shouldThrowExceptionOnNullOrEmptyToken() { + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + assertThatThrownBy(() -> authService.getAuthenticatedOrFail("john", " ")) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Authentication token"); + + assertThatThrownBy(() -> authService.getAuthenticatedOrFail("john", "")) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Authentication token"); + + assertThatThrownBy(() -> authService.getAuthenticatedOrFail("john", null)) + .isInstanceOf(AuthenticationCredentialsNotFoundException.class) + .hasMessageContaining("Authentication token"); + } + + @Test + public void shouldThrowExceptionIfTokenDoesNotMatches() { + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + assertThatThrownBy(() -> authService.getAuthenticatedOrFail("john", "nop")) + .isInstanceOf(BadCredentialsException.class) + .hasMessageContaining("Authentication token does not match"); + } + + @Test + public void shouldReturnAuthenticationTokenOnSuccess() { + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + + final UsernamePasswordAuthenticationToken authToken = authService.getAuthenticatedOrFail("john", TestConstant.UI_SECRET_TOKEN); + + assertThat(authToken.getName()).isEqualTo("john"); + } + + @Test + public void shouldReturnInstanceOfUsernamePasswordAuthenticationTokenOnSuccess() { + // This is not a useless test, Spring security chain test if the instance of the returned AuthToken is UsernamePasswordAuthenticationToken + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + + final UsernamePasswordAuthenticationToken authToken = authService.getAuthenticatedOrFail("john", TestConstant.UI_SECRET_TOKEN); + + assertThat(authToken).isInstanceOf(UsernamePasswordAuthenticationToken.class); + } + + @Test + public void shouldDefineAtLeastOneGrantedAuthorityOnSuccess() { + // This is not a useless test, Spring security chain test if there is at least one granted authority, if there is none, we are considered as non authenticated + final WebSocketAuthenticatorService authService = new WebSocketAuthenticatorService(TestConstant.UI_SECRET_TOKEN); + + final UsernamePasswordAuthenticationToken authToken = authService.getAuthenticatedOrFail("john", TestConstant.UI_SECRET_TOKEN); + + assertThat(authToken.getAuthorities().size()).isGreaterThanOrEqualTo(1); + } + +}