Cookie token store not working in Spring Security adapter
Co-authored-by: scranen <scranen@gmail.com> Co-authored-by: rainerfrey <frey.rainer@gmail.com> Co-authored-by: pedroigor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
25511d4dbf
commit
2d3f771b70
14 changed files with 399 additions and 19 deletions
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 <a href="mailto:scranen@gmail.com">Sjoerd Cranen</a>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:scranen@gmail.com">Sjoerd Cranen</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 <a href="mailto:scranen@gmail.com">Sjoerd Cranen</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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 <code>Authorization</code> header.
|
||||
* and any request with a <code>Authorization</code> 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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <code>KeycloakDeployment</code> (required)
|
||||
* @param request the current <code>HttpServletRequest</code> (required)
|
||||
* @param response the current <code>HttpServletResponse</code> (required when using cookies)
|
||||
*
|
||||
* @return a new <code>AdapterTokenStore</code> for the given <code>deployment</code> and <code>request</code>
|
||||
* @throws IllegalArgumentException if either the <code>deployment</code> or <code>request</code> is <code>null</code>
|
||||
* @return a new <code>AdapterTokenStore</code> for the given <code>deployment</code>, <code>request</code> and <code>response</code>
|
||||
* @throws IllegalArgumentException if any required parameter is <code>null</code>
|
||||
*/
|
||||
AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request);
|
||||
AdapterTokenStore createAdapterTokenStore(KeycloakDeployment deployment, HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:scranen@gmail.com">Sjoerd Cranen</a>
|
||||
*/
|
||||
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<RefreshableKeycloakSecurityContext> principal =
|
||||
checkPrincipalFromCookie();
|
||||
if (principal != null) {
|
||||
final RefreshableKeycloakSecurityContext securityContext =
|
||||
principal.getKeycloakSecurityContext();
|
||||
KeycloakSecurityContext current = ((OIDCHttpFacade) facade).getSecurityContext();
|
||||
if (current != null) {
|
||||
securityContext.setAuthorizationContext(current.getAuthorizationContext());
|
||||
}
|
||||
final Set<String> 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<RefreshableKeycloakSecurityContext> checkPrincipalFromCookie() {
|
||||
KeycloakPrincipal<RefreshableKeycloakSecurityContext> 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue