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);
}
}