diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java index 0cab4f5fd7..166135e2fe 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java @@ -94,7 +94,17 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin } } + /** + * Redirects to the login page. If HTTP sessions are disabled, the redirect URL is saved in a + * cookie now, to be retrieved by the {@link KeycloakAuthenticationSuccessHandler} or the + * {@link KeycloakAuthenticationFailureHandler} when the login sequence completes. + */ protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { + if (request.getSession(false) == null && KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) == null) { + // If no session exists yet at this point, then apparently the redirect URL is not + // stored in a session. We'll store it in a cookie instead. + response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(request.getRequestURI())); + } String contextAwareLoginUri = request.getContextPath() + loginUri; log.debug("Redirecting to login URI {}", contextAwareLoginUri); response.sendRedirect(contextAwareLoginUri); diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java index 5bdd3e18f6..fdb6cb84c1 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationFailureHandler.java @@ -38,6 +38,9 @@ public class KeycloakAuthenticationFailureHandler implements AuthenticationFailu // Check that the response was not committed yet (this may happen when another // part of the Keycloak adapter sends a challenge or a redirect). if (!response.isCommitted()) { + if (KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) != null) { + response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null)); + } response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unable to authenticate using the Authorization header"); } else { if (200 <= response.getStatus() && response.getStatus() < 300) { diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java new file mode 100644 index 0000000000..7a0eab9b6a --- /dev/null +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationSuccessHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springsecurity.authentication; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +/** + * Wrapper for an authentication success handler that sends a redirect if a redirect URL was set in + * a cookie. + * + * @author Sjoerd Cranen + * + * @see KeycloakCookieBasedRedirect + * @see KeycloakAuthenticationEntryPoint#commenceLoginRedirect + */ +public class KeycloakAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationSuccessHandler.class); + + private final AuthenticationSuccessHandler fallback; + + public KeycloakAuthenticationSuccessHandler(AuthenticationSuccessHandler fallback) { + this.fallback = fallback; + } + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + String location = KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request); + if (location == null) { + if (fallback != null) { + fallback.onAuthenticationSuccess(request, response, authentication); + } + } else { + try { + response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null)); + response.sendRedirect(location); + } catch (IOException e) { + LOG.warn("Unable to redirect user after login", e); + } + } + } +} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java new file mode 100644 index 0000000000..b3b8e7e5f7 --- /dev/null +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakCookieBasedRedirect.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springsecurity.authentication; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +/** + * Utility class that provides methods to create and retrieve cookies used for login redirects. + * + * @author Sjoerd Cranen + */ +public final class KeycloakCookieBasedRedirect { + + private static final String REDIRECT_COOKIE = "KC_REDIRECT"; + + private KeycloakCookieBasedRedirect() {} + + /** + * Checks if a cookie with name {@value REDIRECT_COOKIE} exists, and if so, returns its value. + * If multiple cookies of the same name exist, the value of the first cookie is returned. + * + * @param request the request to retrieve the cookie from. + * @return the value of the cookie, if it exists, or else {@code null}. + */ + public static String getRedirectUrlFromCookie(HttpServletRequest request) { + if (request.getCookies() == null) { + return null; + } + for (Cookie cookie : request.getCookies()) { + if (REDIRECT_COOKIE.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } + + /** + * Creates a cookie with name {@value REDIRECT_COOKIE} and the given URL as value. + * + * @param url the value that the cookie should have. If {@code null}, a cookie is created that + * expires immediately and has an empty string as value. + * @return a cookie that can be added to a response. + */ + public static Cookie createCookieFromRedirectUrl(String url) { + Cookie cookie = new Cookie(REDIRECT_COOKIE, url == null ? "" : url); + cookie.setHttpOnly(true); + cookie.setPath("/"); + if (url == null) { + cookie.setMaxAge(0); + } + return cookie; + } +} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java index 935987afc1..213788655f 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakLogoutHandler.java @@ -22,7 +22,9 @@ import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; +import org.keycloak.adapters.springsecurity.token.AdapterTokenStoreFactory; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.keycloak.adapters.springsecurity.token.SpringSecurityAdapterTokenStoreFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; @@ -43,12 +45,17 @@ public class KeycloakLogoutHandler implements LogoutHandler { private static final Logger log = LoggerFactory.getLogger(KeycloakLogoutHandler.class); private AdapterDeploymentContext adapterDeploymentContext; + private AdapterTokenStoreFactory adapterTokenStoreFactory = new SpringSecurityAdapterTokenStoreFactory(); public KeycloakLogoutHandler(AdapterDeploymentContext adapterDeploymentContext) { Assert.notNull(adapterDeploymentContext); this.adapterDeploymentContext = adapterDeploymentContext; } + public void setAdapterTokenStoreFactory(AdapterTokenStoreFactory adapterTokenStoreFactory) { + this.adapterTokenStoreFactory = adapterTokenStoreFactory; + } + @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { if (authentication == null) { @@ -66,6 +73,7 @@ public class KeycloakLogoutHandler implements LogoutHandler { protected void handleSingleSignOut(HttpServletRequest request, HttpServletResponse response, KeycloakAuthenticationToken authenticationToken) { HttpFacade facade = new SimpleHttpFacade(request, response); KeycloakDeployment deployment = adapterDeploymentContext.resolveDeployment(facade); + adapterTokenStoreFactory.createAdapterTokenStore(deployment, request, response).logout(); RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) authenticationToken.getAccount().getKeycloakSecurityContext(); session.logout(deployment); } diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java index 9d344971da..034189db1f 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java @@ -42,7 +42,6 @@ import org.springframework.security.config.annotation.web.servlet.configuration. import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; /** @@ -119,7 +118,7 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo .sessionAuthenticationStrategy(sessionAuthenticationStrategy()) .and() .addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class) - .addFilterBefore(keycloakAuthenticationProcessingFilter(), BasicAuthenticationFilter.class) + .addFilterBefore(keycloakAuthenticationProcessingFilter(), LogoutFilter.class) .addFilterAfter(keycloakSecurityContextRequestFilter(), SecurityContextHolderAwareRequestFilter.class) .addFilterAfter(keycloakAuthenticatedActionsRequestFilter(), KeycloakSecurityContextRequestFilter.class) .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java new file mode 100644 index 0000000000..bee9d15000 --- /dev/null +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/AdapterStateCookieRequestMatcher.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springsecurity.filter; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import org.keycloak.constants.AdapterConstants; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * Matches a request if it contains a {@value AdapterConstants#KEYCLOAK_ADAPTER_STATE_COOKIE} + * cookie. + * + * @author Sjoerd Cranen + */ +public class AdapterStateCookieRequestMatcher implements RequestMatcher { + + @Override + public boolean matches(HttpServletRequest request) { + if (request.getCookies() == null) { + return false; + } + for (Cookie cookie: request.getCookies()) { + if (AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE.equals(cookie.getName())) { + return true; + } + } + return false; + } +} diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java index 09e6a513fd..930867efd6 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilter.java @@ -33,7 +33,9 @@ import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler; +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationSuccessHandler; import org.keycloak.adapters.springsecurity.authentication.RequestAuthenticatorFactory; import org.keycloak.adapters.springsecurity.authentication.SpringSecurityRequestAuthenticatorFactory; import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; @@ -52,6 +54,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; @@ -65,18 +68,18 @@ import org.springframework.util.Assert; * @version $Revision: 1 $ */ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter implements ApplicationContextAware { - public static final String DEFAULT_LOGIN_URL = "/sso/login"; 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. + * and any request with a Authorization header or an {@link AdapterStateCookieRequestMatcher adapter state cookie}. */ public static final RequestMatcher DEFAULT_REQUEST_MATCHER = new OrRequestMatcher( - new AntPathRequestMatcher(DEFAULT_LOGIN_URL), + new AntPathRequestMatcher(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI), new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER), - new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN) + new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN), + new AdapterStateCookieRequestMatcher() ); private static final Logger log = LoggerFactory.getLogger(KeycloakAuthenticationProcessingFilter.class); @@ -97,6 +100,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati public KeycloakAuthenticationProcessingFilter(AuthenticationManager authenticationManager) { this(authenticationManager, DEFAULT_REQUEST_MATCHER); setAuthenticationFailureHandler(new KeycloakAuthenticationFailureHandler()); + setAuthenticationSuccessHandler(new KeycloakAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler())); } /** @@ -143,7 +147,7 @@ public class KeycloakAuthenticationProcessingFilter extends AbstractAuthenticati // using Spring authenticationFailureHandler deployment.setDelegateBearerErrorResponseSending(true); - AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request); + AdapterTokenStore tokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, request, response); RequestAuthenticator authenticator = requestAuthenticatorFactory.createRequestAuthenticator(facade, request, deployment, tokenStore, -1); diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java index cd8cc7f534..4e17183ad3 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakSecurityContextRequestFilter.java @@ -73,7 +73,8 @@ public class KeycloakSecurityContextRequestFilter extends GenericFilterBean impl // just in case session got serialized if (refreshableSecurityContext.getDeployment()==null) { log.trace("Recreating missing deployment and related fields in deserialized context"); - AdapterTokenStore adapterTokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, (HttpServletRequest) request); + AdapterTokenStore adapterTokenStore = adapterTokenStoreFactory.createAdapterTokenStore(deployment, (HttpServletRequest) request, + (HttpServletResponse) response); refreshableSecurityContext.setCurrentRequestInfo(deployment, adapterTokenStore); } diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java index 2f90e5d6a7..b8a60ce7d2 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/AdapterTokenStoreFactory.java @@ -17,6 +17,7 @@ package org.keycloak.adapters.springsecurity.token; +import javax.servlet.http.HttpServletResponse; import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.KeycloakDeployment; @@ -34,10 +35,11 @@ public interface AdapterTokenStoreFactory { * * @param deployment the KeycloakDeployment (required) * @param request the current HttpServletRequest (required) + * @param response the current HttpServletResponse (required when using cookies) * - * @return a new AdapterTokenStore for the given deployment and request - * @throws IllegalArgumentException if either the deployment or request is null + * @return a new AdapterTokenStore for the given deployment, request and response + * @throws IllegalArgumentException if any required parameter is null */ - AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request); + AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request, HttpServletResponse response); } diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java index fb309c1656..321744cede 100644 --- a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactory.java @@ -19,8 +19,11 @@ package org.keycloak.adapters.springsecurity.token; import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.enums.TokenStore; +import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * {@link AdapterTokenStoreFactory} that returns a new {@link SpringSecurityTokenStore} for each request. @@ -30,7 +33,11 @@ import javax.servlet.http.HttpServletRequest; public class SpringSecurityAdapterTokenStoreFactory implements AdapterTokenStoreFactory { @Override - public AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request) { + public AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request, HttpServletResponse response) { + Assert.notNull(deployment, "KeycloakDeployment is required"); + if (deployment.getTokenStore() == TokenStore.COOKIE) { + return new SpringSecurityCookieTokenStore(deployment, request, response); + } return new SpringSecurityTokenStore(deployment, request); } } diff --git a/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java new file mode 100644 index 0000000000..92699f82b4 --- /dev/null +++ b/adapters/oidc/spring-security/src/main/java/org/keycloak/adapters/springsecurity/token/SpringSecurityCookieTokenStore.java @@ -0,0 +1,141 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.springsecurity.token; + +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; +import org.keycloak.adapters.springsecurity.facade.SimpleHttpFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +/** + * Extension of {@link SpringSecurityTokenStore} that stores the obtains tokens in a cookie. + * + * @author Sjoerd Cranen + */ +public class SpringSecurityCookieTokenStore extends SpringSecurityTokenStore { + + private final Logger logger = LoggerFactory.getLogger(SpringSecurityCookieTokenStore.class); + + private final KeycloakDeployment deployment; + private final HttpFacade facade; + private volatile boolean cookieChecked = false; + + public SpringSecurityCookieTokenStore( + KeycloakDeployment deployment, + HttpServletRequest request, + HttpServletResponse response) { + super(deployment, request); + Assert.notNull(response, "HttpServletResponse is required"); + this.deployment = deployment; + this.facade = new SimpleHttpFacade(request, response); + } + + @Override + public void checkCurrentToken() { + final KeycloakPrincipal principal = + checkPrincipalFromCookie(); + if (principal != null) { + final RefreshableKeycloakSecurityContext securityContext = + principal.getKeycloakSecurityContext(); + KeycloakSecurityContext current = ((OIDCHttpFacade) facade).getSecurityContext(); + if (current != null) { + securityContext.setAuthorizationContext(current.getAuthorizationContext()); + } + final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + final OidcKeycloakAccount account = + new SimpleKeycloakAccount(principal, roles, securityContext); + SecurityContextHolder.getContext() + .setAuthentication(new KeycloakAuthenticationToken(account, false)); + } else { + super.checkCurrentToken(); + } + cookieChecked = true; + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + if (!cookieChecked) { + checkCurrentToken(); + } + return super.isCached(authenticator); + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + super.refreshCallback(securityContext); + CookieTokenStore.setTokenCookie(deployment, facade, securityContext); + } + + @Override + public void saveAccountInfo(OidcKeycloakAccount account) { + super.saveAccountInfo(account); + RefreshableKeycloakSecurityContext securityContext = + (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); + CookieTokenStore.setTokenCookie(deployment, facade, securityContext); + } + + @Override + public void logout() { + CookieTokenStore.removeCookie(deployment, facade); + super.logout(); + } + + /** + * Verify if we already have authenticated and active principal in cookie. Perform refresh if + * it's not active + * + * @return valid principal + */ + private KeycloakPrincipal checkPrincipalFromCookie() { + KeycloakPrincipal principal = + CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); + if (principal == null) { + logger.debug("Account was not in cookie or was invalid"); + return null; + } + + RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext(); + + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal; + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) { + refreshCallback(session); + return principal; + } + + logger.debug( + "Cleanup and expire cookie for user {} after failed refresh", principal.getName()); + CookieTokenStore.removeCookie(deployment, facade); + return null; + } +} diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java index b8452190a4..c0a69c1268 100755 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java +++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakAuthenticationProcessingFilterTest.java @@ -27,6 +27,7 @@ import org.keycloak.adapters.OidcKeycloakAccount; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.springsecurity.KeycloakAuthenticationException; import org.keycloak.adapters.springsecurity.account.KeycloakRole; +import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint; import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationFailureHandler; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.keycloak.common.enums.SslRequired; @@ -50,8 +51,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; @@ -107,6 +106,7 @@ public class KeycloakAuthenticationProcessingFilterTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); request = spy(new MockHttpServletRequest()); + request.setRequestURI("http://host"); filter = new KeycloakAuthenticationProcessingFilter(authenticationManager); keycloakFailureHandler = new KeycloakAuthenticationFailureHandler(); @@ -151,6 +151,7 @@ public class KeycloakAuthenticationProcessingFilterTest { @Test public void testSuccessfulAuthenticationInteractive() throws Exception { + request.setRequestURI("http://host" + KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI + "?query"); Authentication authentication = new KeycloakAuthenticationToken(keycloakAccount, true, authorities); filter.successfulAuthentication(request, response, chain, authentication); diff --git a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java index d074fd2570..6984cd5fa5 100755 --- a/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java +++ b/adapters/oidc/spring-security/src/test/java/org/keycloak/adapters/springsecurity/token/SpringSecurityAdapterTokenStoreFactoryTest.java @@ -21,13 +21,15 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.spi.AdapterSessionStore; +import org.keycloak.enums.TokenStore; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; /** * Spring Security adapter token store factory tests. @@ -42,6 +44,9 @@ public class SpringSecurityAdapterTokenStoreFactoryTest { @Mock private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -49,18 +54,37 @@ public class SpringSecurityAdapterTokenStoreFactoryTest { @Test public void testCreateAdapterTokenStore() throws Exception { - AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request); - assertNotNull(store); + when(deployment.getTokenStore()).thenReturn(TokenStore.SESSION); + AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request, response); assertTrue(store instanceof SpringSecurityTokenStore); } + @Test + public void testCreateAdapterTokenStoreUsingCookies() throws Exception { + when(deployment.getTokenStore()).thenReturn(TokenStore.COOKIE); + AdapterSessionStore store = factory.createAdapterTokenStore(deployment, request, response); + assertTrue(store instanceof SpringSecurityCookieTokenStore); + } + @Test(expected = IllegalArgumentException.class) public void testCreateAdapterTokenStoreNullDeployment() throws Exception { - factory.createAdapterTokenStore(null, request); + factory.createAdapterTokenStore(null, request, response); } @Test(expected = IllegalArgumentException.class) public void testCreateAdapterTokenStoreNullRequest() throws Exception { - factory.createAdapterTokenStore(deployment, null); + factory.createAdapterTokenStore(deployment, null, response); + } + + @Test + public void testCreateAdapterTokenStoreNullResponse() throws Exception { + when(deployment.getTokenStore()).thenReturn(TokenStore.SESSION); + factory.createAdapterTokenStore(deployment, request, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateAdapterTokenStoreNullResponseUsingCookies() throws Exception { + when(deployment.getTokenStore()).thenReturn(TokenStore.COOKIE); + factory.createAdapterTokenStore(deployment, request, null); } }