From b05da425b989ebb8dc8b22b948651d97f91f535b Mon Sep 17 00:00:00 2001 From: Scott Rossillo Date: Wed, 6 May 2015 16:29:43 -0400 Subject: [PATCH] Improve Spring Security adapter client to client authorization --- .../SpringSecurityRequestAuthenticator.java | 2 +- .../client/KeycloakClientRequestFactory.java | 33 +++- .../client/KeycloakRestTemplate.java | 25 ++- ...eycloakAuthenticationProcessingFilter.java | 102 +++++++++++- .../token/AdapterTokenStoreFactory.java | 26 +++ .../token/KeycloakAuthenticationToken.java | 2 + ...pringSecurityAdapterTokenStoreFactory.java | 19 +++ .../token/SpringSecurityTokenStore.java | 5 +- .../KeycloakClientRequestFactoryTest.java | 81 ++++++++++ ...oakAuthenticationProcessingFilterTest.java | 152 ++++++++++++++++++ ...gSecurityAdapterTokenStoreFactoryTest.java | 49 ++++++ .../token/SpringSecurityTokenStoreTest.java | 94 +++++++++++ 12 files changed, 575 insertions(+), 15 deletions(-) create mode 100644 integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java create mode 100644 integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java index 8b229eafe0..b224ccfa70 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/SpringSecurityRequestAuthenticator.java @@ -77,7 +77,7 @@ public class SpringSecurityRequestAuthenticator extends RequestAuthenticator { Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, securityContext); - logger.warn("Completing bearer authentication. Bearer roles: {} ",roles); + logger.debug("Completing bearer authentication. Bearer roles: {} ",roles); SecurityContextHolder.getContext().setAuthentication(new KeycloakAuthenticationToken(account)); request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java index 2dea07195a..72f32b718e 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactory.java @@ -36,10 +36,35 @@ public class KeycloakClientRequestFactory extends HttpComponentsClientHttpReques @Override protected void postProcessHttpRequest(HttpUriRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication; - KeycloakSecurityContext context = token.getAccount().getKeycloakSecurityContext(); - + KeycloakSecurityContext context = this.getKeycloakSecurityContext(); request.setHeader(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString()); } + + /** + * Returns the {@link KeycloakSecurityContext} from the Spring {@link SecurityContextHolder}'s {@link Authentication}. + * + * @return the current KeycloakSecurityContext + */ + protected KeycloakSecurityContext getKeycloakSecurityContext() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + KeycloakAuthenticationToken token; + KeycloakSecurityContext context; + + if (authentication == null) { + throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal"); + } + + if (!KeycloakAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { + throw new IllegalStateException( + String.format( + "Cannot set authorization header because Authentication is of type %s but %s is required", + authentication.getClass(), KeycloakAuthenticationToken.class) + ); + } + + token = (KeycloakAuthenticationToken) authentication; + context = token.getAccount().getKeycloakSecurityContext(); + + return context; + } } diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java index 3451717620..fdf56d25c3 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/client/KeycloakRestTemplate.java @@ -1,15 +1,32 @@ package org.keycloak.adapters.springsecurity.client; -import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; /** - * Created by scott on 4/22/15. + * Extends Spring's central class for client-side HTTP access, {@link RestTemplate}, adding + * automatic authentication for service to service calls using the currently authenticated Keycloak principal. + * This class is designed to work with other services secured by Keycloak. + * + *

+ * The main advantage to using this class over Spring's RestTemplate is that authentication + * is handled automatically when both the service making the API call and the service being called are + * protected by Keycloak authentication. + *

+ * + * @see RestOperations + * @see RestTemplate + * + * @author Scott Rossillo + * @version $Revision: 1 $ */ -//@Service -//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class KeycloakRestTemplate extends RestTemplate implements RestOperations { + + /** + * Create a new instance based on the given {@link KeycloakClientRequestFactory}. + * + * @param factory the KeycloakClientRequestFactory to use when creating new requests + */ public KeycloakRestTemplate(KeycloakClientRequestFactory factory) { super(factory); } diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java index 55cbd92a26..cf5f373fcf 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java @@ -1,19 +1,23 @@ package org.keycloak.adapters.springsecurity.filter; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RequestAuthenticator; import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticator; import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; -import org.keycloak.adapters.springsecurity.token.SpringSecurityTokenStore; +import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory; +import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; @@ -24,6 +28,7 @@ import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,6 +42,8 @@ import java.io.IOException; */ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware { + public static final String AUTHORIZATION_HEADER = "Authorization"; + /** * Request matcher that matches requests to the {@link KeycloakAuthenticationEntryPoint#DEFAULT_LOGIN_URI default login URI} * and any request with a Authorization header. @@ -48,6 +55,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati private ApplicationContext applicationContext; private AdapterDeploymentContextBean adapterDeploymentContextBean; + private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); private AuthenticationManager authenticationManager; /** @@ -83,11 +91,13 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati Assert.notNull(authenticationManager, "authenticationManager cannot be null"); this.authenticationManager = authenticationManager; super.setAuthenticationManager(authenticationManager); + super.setAllowSessionCreation(false); + super.setContinueChainBeforeSuccessfulAuthentication(false); } @Override public void afterPropertiesSet() { - adapterDeploymentContextBean= applicationContext.getBean(AdapterDeploymentContextBean.class); + adapterDeploymentContextBean = applicationContext.getBean(AdapterDeploymentContextBean.class); super.afterPropertiesSet(); } @@ -99,8 +109,8 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati KeycloakDeployment deployment = adapterDeploymentContextBean.getDeployment(); SimpleHttpFacade facade = new SimpleHttpFacade(request, response); - SpringSecurityTokenStore tokenStore = new SpringSecurityTokenStore(deployment, request); - SpringSecurityRequestAuthenticator authenticator + AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request); + RequestAuthenticator authenticator = new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, -1); AuthOutcome result = authenticator.authenticate(); @@ -121,8 +131,92 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati return null; } + /** + * Returns true if the request was made with a bearer token authorization header. + * + * @param request the current HttpServletRequest + * + * @return true if the request was made with a bearer token authorization header; + * false otherwise. + */ + protected boolean isBearerTokenRequest(HttpServletRequest request) { + String authValue = request.getHeader(AUTHORIZATION_HEADER); + return authValue != null && authValue.startsWith("Bearer"); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + + if (!this.isBearerTokenRequest(request)) { + super.successfulAuthentication(request, response, chain, authResult); + return; + } + + if (log.isDebugEnabled()) { + log.debug("Authentication success using bearer token. Updating SecurityContextHolder to contain: {}", authResult); + } + + SecurityContextHolder.getContext().setAuthentication(authResult); + + // Fire event + if (this.eventPublisher != null) { + eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); + } + + try { + chain.doFilter(request, response); + } finally { + SecurityContextHolder.clearContext(); + } + + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + + if (this.isBearerTokenRequest(request)) { + SecurityContextHolder.clearContext(); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unable to authenticate bearer token"); + return; + } + + super.unsuccessfulAuthentication(request, response, failed); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + + /** + * Sets the adapter token store factory to use when creating per-request adapter token stores. + * + * @param adapterTokenStoreFactory the AdapterTokenStoreFactory to use + */ + public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory adapterTokenStoreFactory) { + Assert.notNull(adapterTokenStoreFactory, "AdapterTokenStoreFactory cannot be null"); + this.adapterTokenStoreFactory = adapterTokenStoreFactory; + } + + /** + * This filter does not support explicitly enabling session creation. + * + * @throws UnsupportedOperationException this filter does not support explicitly enabling session creation. + */ + @Override + public final void setAllowSessionCreation(boolean allowSessionCreation) { + throw new UnsupportedOperationException("This filter does not support explicitly setting a session creation policy"); + } + + /** + * This filter does not support explicitly setting a continue chain before success policy + * + * @throws UnsupportedOperationException this filter does not support explicitly setting a continue chain before success policy + */ + @Override + public final void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) { + throw new UnsupportedOperationException("This filter does not support explicitly setting a continue chain before success policy"); + } } diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java new file mode 100644 index 0000000000..a43c5ae3ab --- /dev/null +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java @@ -0,0 +1,26 @@ +package org.keycloak.adapters.springsecurity.token; + +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakDeployment; + +import javax.servlet.http.HttpServletRequest; + +/** + * Creates a per-request adapter token store. + * + * @author Scott Rossillo + */ +public interface AdapterTokenStoreFactory { + + /** + * Returns a new {@link AdapterTokenStore} for the given {@link KeycloakDeployment} and {@link HttpServletRequest request}. + * + * @param deployment the KeycloakDeployment (required) + * @param request the current HttpServletRequest (required) + * + * @return a new AdapterTokenStore for the given deployment and request + * @throws IllegalArgumentException if either the deployment or request is null + */ + AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request); + +} diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java index c96ae7e28c..da4c1922ef 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/KeycloakAuthenticationToken.java @@ -34,6 +34,8 @@ public class KeycloakAuthenticationToken extends AbstractAuthenticationToken imp public KeycloakAuthenticationToken(KeycloakAccount account, Collection authorities) { super(authorities); + Assert.notNull(account, "KeycloakAccount cannot be null"); + Assert.notNull(account.getPrincipal(), "KeycloakAccount.getPrincipal() cannot be null"); this.principal = account.getPrincipal(); this.setDetails(account); setAuthenticated(true); diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java new file mode 100644 index 0000000000..f443f72486 --- /dev/null +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java @@ -0,0 +1,19 @@ +package org.keycloak.adapters.springsecurity.token; + +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakDeployment; + +import javax.servlet.http.HttpServletRequest; + +/** + * {@link AdapterTokenStoreFactory} that returns a new {@link SpringSecurityTokenStore} for each request. + * + * @author Scott Rossillo + */ +public class SpringSecurityAdapterTokenStoreFactory implements AdapterTokenStoreFactory { + + @Override + public AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request) { + return new SpringSecurityTokenStore(deployment, request); + } +} diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java index 3c6af11ee5..c16d73f107 100644 --- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java +++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStore.java @@ -53,7 +53,7 @@ public class SpringSecurityTokenStore implements AdapterTokenStore { return false; } - if (KeycloakAuthenticationToken.class.isAssignableFrom(context.getAuthentication().getClass())) { + if (!KeycloakAuthenticationToken.class.isAssignableFrom(context.getAuthentication().getClass())) { logger.warn("Expected a KeycloakAuthenticationToken, but found {}", context.getAuthentication()); return false; } @@ -84,7 +84,7 @@ public class SpringSecurityTokenStore implements AdapterTokenStore { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { - throw new IllegalStateException("Went to save Keycloak account {}, but already have {}"); + throw new IllegalStateException(String.format("Went to save Keycloak account %s, but already have %s", account, authentication)); } logger.debug("Saving account info {}", account); @@ -99,6 +99,7 @@ public class SpringSecurityTokenStore implements AdapterTokenStore { if (session != null) { session.setAttribute(KeycloakSecurityContext.class.getName(), null); + session.invalidate(); } SecurityContextHolder.clearContext(); diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java new file mode 100644 index 0000000000..e35efe622a --- /dev/null +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/client/KeycloakClientRequestFactoryTest.java @@ -0,0 +1,81 @@ +package org.keycloak.adapters.springsecurity.client; + +import org.apache.http.client.methods.HttpUriRequest; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.springsecurity.account.KeycloakRole; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import java.util.Collections; +import java.util.UUID; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Keycloak client request factory tests. + */ +public class KeycloakClientRequestFactoryTest { + + @Spy + private KeycloakClientRequestFactory factory; + + @Mock + private KeycloakAccount account; + + @Mock + private KeycloakAuthenticationToken keycloakAuthenticationToken; + + @Mock + private KeycloakSecurityContext keycloakSecurityContext; + + @Mock + private HttpUriRequest request; + + private String bearerTokenString; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + bearerTokenString = UUID.randomUUID().toString(); + + SecurityContextHolder.getContext().setAuthentication(keycloakAuthenticationToken); + when(keycloakAuthenticationToken.getAccount()).thenReturn(account); + when(account.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext); + when(keycloakSecurityContext.getTokenString()).thenReturn(bearerTokenString); + } + + @Test + public void testPostProcessHttpRequest() throws Exception { + factory.postProcessHttpRequest(request); + verify(factory).getKeycloakSecurityContext(); + verify(request).setHeader(eq(KeycloakClientRequestFactory.AUTHORIZATION_HEADER), eq("Bearer " + bearerTokenString)); + } + + @Test + public void testGetKeycloakSecurityContext() throws Exception { + KeycloakSecurityContext context = factory.getKeycloakSecurityContext(); + assertNotNull(context); + assertEquals(keycloakSecurityContext, context); + } + + @Test(expected = IllegalStateException.class) + public void testGetKeycloakSecurityContextInvalidAuthentication() throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("baz")))); + factory.getKeycloakSecurityContext(); + } + + @Test(expected = IllegalStateException.class) + public void testGetKeycloakSecurityContextNullAuthentication() throws Exception { + SecurityContextHolder.clearContext(); + factory.getKeycloakSecurityContext(); + } +} diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java new file mode 100644 index 0000000000..45420339db --- /dev/null +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java @@ -0,0 +1,152 @@ +package org.keycloak.adapters.springsecurity.filter; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean; +import org.keycloak.adapters.springsecurity.account.KeycloakRole; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Keycloak authentication process filter test cases. + */ +public class KeycloakAuthenticationProcessingFilterTest { + + private KeycloakAuthenticationProcessingFilter filter; + + @Mock + private AuthenticationManager authenticationManager; + + @Mock + private AdapterDeploymentContextBean adapterDeploymentContextBean; + + @Mock + private FilterChain chain; + + private MockHttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private ApplicationContext applicationContext; + + @Mock + private AuthenticationSuccessHandler successHandler; + + @Mock + private AuthenticationFailureHandler failureHandler; + + @Mock + private KeycloakAccount keycloakAccount; + + @Mock + private KeycloakDeployment keycloakDeployment; + + @Mock + private KeycloakSecurityContext keycloakSecurityContext; + + private final List authorities = Collections.singletonList(new KeycloakRole("ROLE_USER")); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + request = spy(new MockHttpServletRequest()); + filter = new KeycloakAuthenticationProcessingFilter(authenticationManager); + + filter.setApplicationContext(applicationContext); + filter.setAuthenticationSuccessHandler(successHandler); + filter.setAuthenticationFailureHandler(failureHandler); + + when(applicationContext.getBean(eq(AdapterDeploymentContextBean.class))).thenReturn(adapterDeploymentContextBean); + when(adapterDeploymentContextBean.getDeployment()).thenReturn(keycloakDeployment); + when(keycloakAccount.getPrincipal()).thenReturn( + new KeycloakPrincipal(UUID.randomUUID().toString(), keycloakSecurityContext)); + + + filter.afterPropertiesSet(); + } + + @Test + public void testIsBearerTokenRequest() throws Exception { + assertFalse(filter.isBearerTokenRequest(request)); + this.setAuthHeader(request); + assertTrue(filter.isBearerTokenRequest(request)); + } + + @Test + public void testSuccessfulAuthenticationInteractive() throws Exception { + Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities); + filter.successfulAuthentication(request, response, chain, authentication); + + verify(successHandler).onAuthenticationSuccess(eq(request), eq(response), eq(authentication)); + verify(chain, never()).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void testSuccessfulAuthenticationBearer() throws Exception { + Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, authorities); + this.setAuthHeader(request); + filter.successfulAuthentication(request, response, chain, authentication); + + verify(chain).doFilter(eq(request), eq(response)); + verify(successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(Authentication.class)); + } + + @Test + public void testUnsuccessfulAuthenticationInteractive() throws Exception { + AuthenticationException exception = new BadCredentialsException("OOPS"); + filter.unsuccessfulAuthentication(request, response, exception); + verify(failureHandler).onAuthenticationFailure(eq(request), eq(response), eq(exception)); + } + + @Test + public void testUnsuccessfulAuthenticatioBearer() throws Exception { + AuthenticationException exception = new BadCredentialsException("OOPS"); + this.setAuthHeader(request); + filter.unsuccessfulAuthentication(request, response, exception); + verify(response).sendError(eq(HttpServletResponse.SC_FORBIDDEN), anyString()); + verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), + any(AuthenticationException.class)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSetAllowSessionCreation() throws Exception { + filter.setAllowSessionCreation(true); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSetContinueChainBeforeSuccessfulAuthentication() throws Exception { + filter.setContinueChainBeforeSuccessfulAuthentication(true); + } + + private void setAuthHeader(MockHttpServletRequest request) { + request.addHeader(KeycloakAuthenticationProcessingFilter.AUTHORIZATION_HEADER, "Bearer " + UUID.randomUUID().toString()); + } + +} diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java new file mode 100644 index 0000000000..602f8caaca --- /dev/null +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java @@ -0,0 +1,49 @@ +package org.keycloak.adapters.springsecurity.token; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakDeployment; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.*; + +/** + * Spring Security adapter token store factory tests. + */ +public class SpringSecurityAdapterTokenStoreFactoryTest { + + private AdapterTokenStoreFactory factory = new SpringSecurityAdapterTokenStoreFactory(); + + @Mock + private KeycloakDeployment deployment; + + @Mock + private HttpServletRequest request; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testCreateAdapterTokenStore() throws Exception { + AdapterTokenStore store = factory.createAdapterTokenStore(deployment, request); + assertNotNull(store); + assertTrue(store instanceof SpringSecurityTokenStore); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAdapterTokenStoreNullDeployment() throws Exception { + factory.createAdapterTokenStore(null, request); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAdapterTokenStoreNullRequest() throws Exception { + factory.createAdapterTokenStore(deployment, null); + } +} diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java new file mode 100644 index 0000000000..7088951d5e --- /dev/null +++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityTokenStoreTest.java @@ -0,0 +1,94 @@ +package org.keycloak.adapters.springsecurity.token; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.adapters.springsecurity.account.KeycloakRole; +import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import javax.servlet.http.HttpSession; + +import java.security.Principal; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * Spring Security token store tests. + */ +public class SpringSecurityTokenStoreTest { + + private SpringSecurityTokenStore store; + + @Mock + private KeycloakDeployment deployment; + + @Mock + private Principal principal; + + @Mock + private RequestAuthenticator requestAuthenticator; + + @Mock + private RefreshableKeycloakSecurityContext keycloakSecurityContext; + + private MockHttpServletRequest request; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + request = new MockHttpServletRequest(); + store = new SpringSecurityTokenStore(deployment, request); + } + + @After + public void tearDown() throws Exception { + SecurityContextHolder.clearContext(); + } + + @Test + public void testIsCached() throws Exception { + Authentication authentication = new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("ROLE_FOO"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + assertFalse(store.isCached(requestAuthenticator)); + } + + @Test + public void testSaveAccountInfo() throws Exception { + KeycloakAccount account = new SimpleKeycloakAccount(principal, Collections.singleton("FOO"), keycloakSecurityContext); + Authentication authentication; + + store.saveAccountInfo(account); + authentication = SecurityContextHolder.getContext().getAuthentication(); + + assertNotNull(authentication); + assertTrue(authentication instanceof KeycloakAuthenticationToken); + } + + @Test(expected = IllegalStateException.class) + public void testSaveAccountInfoInvalidAuthenticationType() throws Exception { + KeycloakAccount account = new SimpleKeycloakAccount(principal, Collections.singleton("FOO"), keycloakSecurityContext); + Authentication authentication = new PreAuthenticatedAuthenticationToken("foo", "bar", Collections.singleton(new KeycloakRole("ROLE_FOO"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + store.saveAccountInfo(account); + } + + @Test + public void testLogout() throws Exception { + MockHttpSession session = (MockHttpSession) request.getSession(true); + assertFalse(session.isInvalid()); + store.logout(); + assertTrue(session.isInvalid()); + } +}