From a538e25b9d9dc4c3ad34761bc9cbe973e12214b6 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 23 Oct 2014 22:58:47 +0200 Subject: [PATCH] KEYCLOAK-702 - Added AdapterTokenStore spi. Possibility to save account info to cookie as alternative to http session --- .../keycloak/adapters/AdapterConstants.java | 3 + .../java/org/keycloak/enums/TokenStore.java | 9 + .../adapters/config/AdapterConfig.java | 12 +- .../adapters/AdapterDeploymentContext.java | 11 ++ .../keycloak/adapters/AdapterTokenStore.java | 43 +++++ .../org/keycloak/adapters/AdapterUtils.java | 33 ++++ .../keycloak/adapters/CookieTokenStore.java | 89 +++++++++ .../keycloak/adapters/KeycloakDeployment.java | 10 + .../adapters/KeycloakDeploymentBuilder.java | 6 + .../adapters/OAuthRequestAuthenticator.java | 4 +- .../RefreshableKeycloakSecurityContext.java | 18 +- .../adapters/RequestAuthenticator.java | 11 +- .../as7/CatalinaCookieTokenStore.java | 109 +++++++++++ .../as7/CatalinaRequestAuthenticator.java | 89 ++++----- .../as7/CatalinaSessionTokenStore.java | 110 +++++++++++ .../as7/KeycloakAuthenticatorValve.java | 63 +++--- .../tomcat7/CatalinaCookieTokenStore.java | 107 +++++++++++ .../tomcat7/CatalinaRequestAuthenticator.java | 88 ++++----- .../tomcat7/CatalinaSessionTokenStore.java | 108 +++++++++++ .../tomcat7/KeycloakAuthenticatorValve.java | 63 +++--- .../undertow/KeycloakUndertowAccount.java | 42 ++-- .../undertow/ServletKeycloakAuthMech.java | 48 ++--- .../undertow/ServletRequestAuthenticator.java | 43 +---- .../undertow/ServletSessionTokenStore.java | 106 +++++++++++ .../undertow/UndertowCookieTokenStore.java | 79 ++++++++ .../undertow/UndertowKeycloakAuthMech.java | 35 ++-- .../UndertowRequestAuthenticator.java | 43 +---- .../undertow/UndertowSessionTokenStore.java | 90 +++++++++ .../WildflyAuthenticationMechanism.java | 4 +- .../wildfly/WildflyRequestAuthenticator.java | 5 +- .../testsuite/adapter/AdapterTest.java | 5 +- .../adapter/CookieTokenStoreAdapterTest.java | 180 ++++++++++++++++++ .../testsuite/adapter/CustomerServlet.java | 4 +- .../cust-app-cookie-keycloak.json | 12 ++ .../resources/adapter-test/demorealm.json | 9 + 35 files changed, 1351 insertions(+), 340 deletions(-) create mode 100644 core/src/main/java/org/keycloak/enums/TokenStore.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java create mode 100644 integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java create mode 100644 integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java create mode 100644 integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java create mode 100644 integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java create mode 100644 integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java create mode 100644 integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java create mode 100644 integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java create mode 100644 integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CookieTokenStoreAdapterTest.java create mode 100644 testsuite/integration/src/test/resources/adapter-test/cust-app-cookie-keycloak.json diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java index 6feb6558ef..c9399a5e1f 100755 --- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java +++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java @@ -26,4 +26,7 @@ public interface AdapterConstants { // Attribute passed in registerNode request for register new application cluster node once he joined cluster public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host"; + + // Cookie used on adapter side to store token info. Used only when tokenStore is 'COOKIE' + public static final String KEYCLOAK_ADAPTER_STATE_COOKIE = "KEYCLOAK_ADAPTER_STATE"; } diff --git a/core/src/main/java/org/keycloak/enums/TokenStore.java b/core/src/main/java/org/keycloak/enums/TokenStore.java new file mode 100644 index 0000000000..de16ecd9de --- /dev/null +++ b/core/src/main/java/org/keycloak/enums/TokenStore.java @@ -0,0 +1,9 @@ +package org.keycloak.enums; + +/** + * @author Marek Posolda + */ +public enum TokenStore { + SESSION, + COOKIE +} diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index 778f9f5b34..e2fe9fdcf9 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -18,7 +18,7 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder; "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore", "client-keystore-password", "client-key-password", "auth-server-url-for-backend-requests", "always-refresh-token", - "register-node-at-startup", "register-node-period" + "register-node-at-startup", "register-node-period", "token-store" }) public class AdapterConfig extends BaseAdapterConfig { @@ -46,6 +46,8 @@ public class AdapterConfig extends BaseAdapterConfig { protected boolean registerNodeAtStartup = false; @JsonProperty("register-node-period") protected int registerNodePeriod = -1; + @JsonProperty("token-store") + protected String tokenStore; public boolean isAllowAnyHostname() { return allowAnyHostname; @@ -142,4 +144,12 @@ public class AdapterConfig extends BaseAdapterConfig { public void setRegisterNodePeriod(int registerNodePeriod) { this.registerNodePeriod = registerNodePeriod; } + + public String getTokenStore() { + return tokenStore; + } + + public void setTokenStore(String tokenStore) { + this.tokenStore = tokenStore; + } } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index 95e31d77ef..9fff2b26c8 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -7,6 +7,7 @@ import org.apache.http.client.methods.HttpGet; import org.jboss.logging.Logger; import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.SslRequired; +import org.keycloak.enums.TokenStore; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.PublishedRealmRepresentation; import org.keycloak.util.JsonSerialization; @@ -257,6 +258,16 @@ public class AdapterDeploymentContext { delegate.setSslRequired(sslRequired); } + @Override + public TokenStore getTokenStore() { + return delegate.getTokenStore(); + } + + @Override + public void setTokenStore(TokenStore tokenStore) { + delegate.setTokenStore(tokenStore); + } + @Override public String getStateCookieName() { return delegate.getStateCookieName(); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java new file mode 100644 index 0000000000..fd2c5c3221 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterTokenStore.java @@ -0,0 +1,43 @@ +package org.keycloak.adapters; + +import org.keycloak.KeycloakSecurityContext; + +/** + * Abstraction for storing token info on adapter side. Intended to be per-request object + * + * @author Marek Posolda + */ +public interface AdapterTokenStore { + + /** + * Impl can validate if current token exists and perform refreshing if it exists and is expired + */ + void checkCurrentToken(); + + /** + * Check if we are logged already (we have already valid and successfully refreshed accessToken). Establish security context if yes + * + * @param authenticator used for actual request authentication + * @return true if we are logged-in already + */ + boolean isCached(RequestAuthenticator authenticator); + + /** + * Finish successful OAuth2 login and store validated account + * + * @param account + */ + void saveAccountInfo(KeycloakAccount account); + + /** + * Handle logout on store side and possibly propagate logout call to Keycloak + */ + void logout(); + + /** + * Callback invoked after successful token refresh + * + * @param securityContext context where refresh was performed + */ + void refreshCallback(RefreshableKeycloakSecurityContext securityContext); +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java index e41a8b5fc0..6e8b97caa1 100644 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterUtils.java @@ -1,6 +1,11 @@ package org.keycloak.adapters; +import java.util.Collections; +import java.util.Set; + +import org.jboss.logging.Logger; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.representations.AccessToken; import org.keycloak.util.UriUtils; /** @@ -8,6 +13,8 @@ import org.keycloak.util.UriUtils; */ public class AdapterUtils { + private static Logger log = Logger.getLogger(AdapterUtils.class); + public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) { if (session instanceof RefreshableKeycloakSecurityContext) { KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment(); @@ -26,4 +33,30 @@ public class AdapterUtils { return UriUtils.getOrigin(browserRequestURL); } } + + public static Set getRolesFromSecurityContext(RefreshableKeycloakSecurityContext session) { + Set roles = null; + AccessToken accessToken = session.getToken(); + if (session.getDeployment().isUseResourceRoleMappings()) { + if (log.isTraceEnabled()) { + log.trace("useResourceRoleMappings"); + } + AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName()); + if (access != null) roles = access.getRoles(); + } else { + if (log.isTraceEnabled()) { + log.trace("use realm role mappings"); + } + AccessToken.Access access = accessToken.getRealmAccess(); + if (access != null) roles = access.getRoles(); + } + if (roles == null) roles = Collections.emptySet(); + if (log.isTraceEnabled()) { + log.trace("Setting roles: "); + for (String role : roles) { + log.trace(" role: " + role); + } + } + return roles; + } } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java new file mode 100644 index 0000000000..23e017df68 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java @@ -0,0 +1,89 @@ +package org.keycloak.adapters; + +import java.io.IOException; + +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.RSATokenVerifier; +import org.keycloak.VerificationException; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; +import org.keycloak.util.KeycloakUriBuilder; + +/** + * @author Marek Posolda + */ +public class CookieTokenStore { + + private static final Logger log = Logger.getLogger(CookieTokenStore.class); + private static final String DELIM = "@"; + + public static void setTokenCookie(KeycloakDeployment deployment, HttpFacade facade, RefreshableKeycloakSecurityContext session) { + log.infof("Set new %s cookie now", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE); + String accessToken = session.getTokenString(); + String idToken = session.getIdTokenString(); + String refreshToken = session.getRefreshToken(); + String cookie = new StringBuilder(accessToken).append(DELIM) + .append(idToken).append(DELIM) + .append(refreshToken).toString(); + + String cookiePath = getContextPath(facade); + facade.getResponse().setCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookie, cookiePath, null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true); + } + + public static KeycloakPrincipal getPrincipalFromCookie(KeycloakDeployment deployment, HttpFacade facade, AdapterTokenStore tokenStore) { + HttpFacade.Cookie cookie = facade.getRequest().getCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE); + if (cookie == null) { + log.debug("Not found adapter state cookie in current request"); + return null; + } + + String cookieVal = cookie.getValue(); + + String[] tokens = cookieVal.split(DELIM); + if (tokens.length != 3) { + log.warnf("Invalid format of %s cookie. Count of tokens: %s, expected 3", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, tokens.length); + return null; + } + + String accessTokenString = tokens[0]; + String idTokenString = tokens[1]; + String refreshTokenString = tokens[2]; + + try { + // Skip check if token is active now. It's supposed to be done later by the caller + AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealm(), false); + IDToken idToken; + if (idTokenString != null && idTokenString.length() > 0) { + JWSInput input = new JWSInput(idTokenString); + try { + idToken = input.readJsonContent(IDToken.class); + } catch (IOException e) { + throw new VerificationException(e); + } + } else { + idToken = null; + } + + log.debug("Token Verification succeeded!"); + RefreshableKeycloakSecurityContext secContext = new RefreshableKeycloakSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString); + return new KeycloakPrincipal(accessToken.getSubject(), secContext); + } catch (VerificationException ve) { + log.warn("Failed verify token", ve); + return null; + } + } + + public static void removeCookie(HttpFacade facade) { + String cookiePath = getContextPath(facade); + facade.getResponse().resetCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookiePath); + } + + private static String getContextPath(HttpFacade facade) { + String uri = facade.getRequest().getURI(); + String path = KeycloakUriBuilder.fromUri(uri).getPath(); + int index = path.indexOf("/", 1); + return index == -1 ? path : path.substring(0, index); + } +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index c2d60d10f1..2380684f9e 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -5,6 +5,7 @@ import org.jboss.logging.Logger; import org.keycloak.ServiceUrlConstants; import org.keycloak.enums.RelativeUrlsUsed; import org.keycloak.enums.SslRequired; +import org.keycloak.enums.TokenStore; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.KeycloakUriBuilder; @@ -42,6 +43,7 @@ public class KeycloakDeployment { protected String scope; protected SslRequired sslRequired = SslRequired.ALL; + protected TokenStore tokenStore = TokenStore.SESSION; protected String stateCookieName = "OAuth_Token_Request_State"; protected boolean useResourceRoleMappings; protected boolean cors; @@ -236,6 +238,14 @@ public class KeycloakDeployment { this.sslRequired = sslRequired; } + public TokenStore getTokenStore() { + return tokenStore; + } + + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + public String getStateCookieName() { return stateCookieName; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 8913dd4e30..c79d90568d 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -4,6 +4,7 @@ import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.jboss.logging.Logger; import org.keycloak.enums.SslRequired; +import org.keycloak.enums.TokenStore; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.PemUtils; import org.keycloak.util.SystemPropertiesJsonParserFactory; @@ -48,6 +49,11 @@ public class KeycloakDeploymentBuilder { } else { deployment.setSslRequired(SslRequired.EXTERNAL); } + if (adapterConfig.getTokenStore() != null) { + deployment.setTokenStore(TokenStore.valueOf(adapterConfig.getTokenStore().toUpperCase())); + } else { + deployment.setTokenStore(TokenStore.SESSION); + } deployment.setResourceCredentials(adapterConfig.getCredentials()); deployment.setPublicClient(adapterConfig.isPublicClient()); deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings()); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index c3c2e6e7ba..312576e046 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -4,6 +4,7 @@ import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; +import org.keycloak.enums.TokenStore; import org.keycloak.jose.jws.JWSInput; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; @@ -255,7 +256,8 @@ public abstract class OAuthRequestAuthenticator { AccessTokenResponse tokenResponse = null; strippedOauthParametersRequestUri = stripOauthParametersFromRedirect(); try { - String httpSessionId = reqAuthenticator.getHttpSessionId(true); + // For COOKIE store we don't have httpSessionId and single sign-out won't be available + String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.getHttpSessionId(true) : null; tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId); } catch (ServerRequest.HttpFailure failure) { log.error("failed to turn code into token"); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java index 3fc5b3d6e2..3cd87cb455 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java @@ -4,6 +4,7 @@ import org.jboss.logging.Logger; import org.keycloak.KeycloakSecurityContext; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; +import org.keycloak.enums.TokenStore; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -19,14 +20,16 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext protected static Logger log = Logger.getLogger(RefreshableKeycloakSecurityContext.class); protected transient KeycloakDeployment deployment; + protected transient AdapterTokenStore tokenStore; protected String refreshToken; public RefreshableKeycloakSecurityContext() { } - public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) { + public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, AdapterTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) { super(tokenString, token, idTokenString, idToken); this.deployment = deployment; + this.tokenStore = tokenStore; this.refreshToken = refreshToken; } @@ -42,6 +45,10 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext return super.getTokenString(); } + public String getRefreshToken() { + return refreshToken; + } + public void logout(KeycloakDeployment deployment) { try { ServerRequest.invokeLogout(deployment, refreshToken); @@ -58,8 +65,9 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext return deployment; } - public void setDeployment(KeycloakDeployment deployment) { + public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { this.deployment = deployment; + this.tokenStore = tokenStore; } /** @@ -107,8 +115,14 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext this.token = token; this.refreshToken = response.getRefreshToken(); this.tokenString = tokenString; + tokenStore.refreshCallback(this); return true; } + protected void updateTokenCookie(KeycloakDeployment deployment, HttpFacade facade) { + if (deployment.getTokenStore() == TokenStore.COOKIE) { + CookieTokenStore.setTokenCookie(deployment, facade, this); + } + } } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java index 0b3123816d..a4da6a2842 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java @@ -12,12 +12,14 @@ public abstract class RequestAuthenticator { protected HttpFacade facade; protected KeycloakDeployment deployment; + protected AdapterTokenStore tokenStore; protected AuthChallenge challenge; protected int sslRedirectPort; - public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) { + public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) { this.facade = facade; this.deployment = deployment; + this.tokenStore = tokenStore; this.sslRedirectPort = sslRedirectPort; } @@ -58,7 +60,7 @@ public abstract class RequestAuthenticator { log.trace("try oauth"); } - if (isCached()) { + if (tokenStore.isCached(this)) { if (verifySSL()) return AuthOutcome.FAILED; log.debug("AUTHENTICATED: was cached"); return AuthOutcome.AUTHENTICATED; @@ -103,18 +105,17 @@ public abstract class RequestAuthenticator { } protected void completeAuthentication(OAuthRequestAuthenticator oauth) { - RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken()); + RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, tokenStore, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken()); final KeycloakPrincipal principal = new KeycloakPrincipal(oauth.getToken().getSubject(), session); completeOAuthAuthentication(principal); } protected abstract void completeOAuthAuthentication(KeycloakPrincipal principal); protected abstract void completeBearerAuthentication(KeycloakPrincipal principal); - protected abstract boolean isCached(); protected abstract String getHttpSessionId(boolean create); protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { - RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, bearer.getTokenString(), bearer.getToken(), null, null, null); + RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); final KeycloakPrincipal principal = new KeycloakPrincipal(bearer.getToken().getSubject(), session); completeBearerAuthentication(principal); } diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java new file mode 100644 index 0000000000..409f9c2c38 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java @@ -0,0 +1,109 @@ +package org.keycloak.adapters.as7; + +import java.util.Set; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * per-request object + * + * @author Marek Posolda + */ +public class CatalinaCookieTokenStore implements AdapterTokenStore { + + private static final Logger log = Logger.getLogger(CatalinaCookieTokenStore.class); + + private Request request; + private HttpFacade facade; + private KeycloakDeployment deployment; + + private KeycloakPrincipal authenticatedPrincipal; + + public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) { + this.request = request; + this.facade = facade; + this.deployment = deployment; + } + + + @Override + public void checkCurrentToken() { + this.authenticatedPrincipal = checkPrincipalFromCookie(); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + // Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request + if (authenticatedPrincipal != null) { + log.debug("remote logged in already. Establish state from cookie"); + RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext(); + securityContext.setCurrentRequestInfo(deployment, this); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext); + + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + request.setUserPrincipal(principal); + request.setAuthType("KEYCLOAK"); + return true; + } else { + return false; + } + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + CookieTokenStore.setTokenCookie(deployment, facade, securityContext); + } + + @Override + public void logout() { + CookieTokenStore.removeCookie(facade); + + KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + if (ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext secContext) { + CookieTokenStore.setTokenCookie(deployment, facade, secContext); + } + + /** + * Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active + * + * @return valid principal + */ + protected KeycloakPrincipal checkPrincipalFromCookie() { + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); + if (principal == null) { + log.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()) return principal; + + log.debugf("Cleanup and expire cookie for user %s after failed refresh", principal.getName()); + request.setUserPrincipal(null); + request.setAuthType(null); + CookieTokenStore.removeCookie(facade); + return null; + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java index a1cb95c4eb..74e0e63919 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java @@ -1,21 +1,21 @@ package org.keycloak.adapters.as7; -import org.apache.catalina.Session; import org.apache.catalina.authenticator.Constants; import org.apache.catalina.connector.Request; -import org.apache.catalina.realm.GenericPrincipal; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OAuthRequestAuthenticator; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.representations.AccessToken; +import org.keycloak.enums.TokenStore; import java.io.IOException; import java.security.Principal; -import java.util.Collections; import java.util.Set; import javax.servlet.http.HttpSession; @@ -25,18 +25,17 @@ import javax.servlet.http.HttpSession; * @version $Revision: 1 $ */ public class CatalinaRequestAuthenticator extends RequestAuthenticator { + private static final Logger log = Logger.getLogger(CatalinaRequestAuthenticator.class); protected KeycloakAuthenticatorValve valve; - protected CatalinaUserSessionManagement userSessionManagement; protected Request request; public CatalinaRequestAuthenticator(KeycloakDeployment deployment, - KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement, + KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore, CatalinaHttpFacade facade, Request request) { - super(facade, deployment, request.getConnector().getRedirectPort()); + super(facade, deployment, tokenStore, request.getConnector().getRedirectPort()); this.valve = valve; - this.userSessionManagement = userSessionManagement; this.request = request; } @@ -46,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { @Override protected void saveRequest() { try { - valve.keycloakSaveRequest(request); + // Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi? + if (deployment.getTokenStore() == TokenStore.SESSION) { + valve.keycloakSaveRequest(request); + } } catch (IOException e) { throw new RuntimeException(e); } @@ -55,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } @Override - protected void completeOAuthAuthentication(KeycloakPrincipal skp) { - RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); + protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { + final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); + final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + KeycloakAccount account = new KeycloakAccount() { + + @Override + public Principal getPrincipal() { + return skp; + } + + @Override + public Set getRoles() { + return roles; + } + + @Override + public KeycloakSecurityContext getKeycloakSecurityContext() { + return securityContext; + } + + }; + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - Set roles = getRolesFromToken(securityContext); - GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext); - Session session = request.getSessionInternal(true); - session.setPrincipal(principal); - session.setAuthType("OAUTH"); - session.setNote(KeycloakSecurityContext.class.getName(), securityContext); - String username = securityContext.getToken().getSubject(); - log.debug("userSessionManage.login: " + username); - userSessionManagement.login(session); + this.tokenStore.saveAccountInfo(account); } @Override protected void completeBearerAuthentication(KeycloakPrincipal principal) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - Set roles = getRolesFromToken(securityContext); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); if (log.isDebugEnabled()) { log.debug("Completing bearer authentication. Bearer roles: " + roles); } @@ -82,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); } - protected Set getRolesFromToken(RefreshableKeycloakSecurityContext session) { - Set roles = null; - if (deployment.isUseResourceRoleMappings()) { - AccessToken.Access access = session.getToken().getResourceAccess(deployment.getResourceName()); - if (access != null) roles = access.getRoles(); - } else { - AccessToken.Access access = session.getToken().getRealmAccess(); - if (access != null) roles = access.getRoles(); - } - if (roles == null) roles = Collections.emptySet(); - return roles; - } - - @Override - protected boolean isCached() { - if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) - return false; - log.debug("remote logged in already"); - GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK"); - Session session = request.getSessionInternal(); - if (session != null) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getNote(KeycloakSecurityContext.class.getName()); - if (securityContext != null) { - securityContext.setDeployment(deployment); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - } - restoreRequest(); - return true; - } - protected void restoreRequest() { if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) { if (valve.keycloakRestoreRequest(request)) { diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java new file mode 100644 index 0000000000..08b83adf67 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java @@ -0,0 +1,110 @@ +package org.keycloak.adapters.as7; + +import java.util.Set; + +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.jboss.logging.Logger; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * per-request object + * + * @author Marek Posolda + */ +public class CatalinaSessionTokenStore implements AdapterTokenStore { + + private static final Logger log = Logger.getLogger(CatalinaSessionTokenStore.class); + + private Request request; + private KeycloakDeployment deployment; + private CatalinaUserSessionManagement sessionManagement; + + public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment, CatalinaUserSessionManagement sessionManagement) { + this.request = request; + this.deployment = deployment; + this.sessionManagement = sessionManagement; + } + + @Override + public void checkCurrentToken() { + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return; + RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); + if (session == null) return; + // just in case session got serialized + if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) return; + + // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session + Session catalinaSession = request.getSessionInternal(); + log.debugf("Cleanup and expire session %s after failed refresh", catalinaSession.getId()); + catalinaSession.removeNote(KeycloakSecurityContext.class.getName()); + request.setUserPrincipal(null); + request.setAuthType(null); + catalinaSession.setPrincipal(null); + catalinaSession.setAuthType(null); + catalinaSession.expire(); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) + return false; + log.debug("remote logged in already. Establish state from session"); + GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); + request.setUserPrincipal(principal); + request.setAuthType("KEYCLOAK"); + + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); + if (securityContext != null) { + securityContext.setCurrentRequestInfo(deployment, this); + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + } + + ((CatalinaRequestAuthenticator)authenticator).restoreRequest(); + return true; + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + Set roles = account.getRoles(); + GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), account.getPrincipal(), roles, securityContext); + + Session session = request.getSessionInternal(true); + session.setPrincipal(principal); + session.setAuthType("OAUTH"); + session.setNote(KeycloakSecurityContext.class.getName(), securityContext); + String username = securityContext.getToken().getSubject(); + log.debug("userSessionManagement.login: " + username); + this.sessionManagement.login(session); + } + + @Override + public void logout() { + Session session = request.getSessionInternal(false); + if (session != null) { + session.removeNote(KeycloakSecurityContext.class.getName()); + KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + if (ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + // no-op + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java index 8b40abb567..93836a467d 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java @@ -13,17 +13,21 @@ import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.deploy.LoginConfig; import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.AdapterConstants; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; +import org.keycloak.adapters.CookieTokenStore; import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.enums.TokenStore; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -44,6 +48,9 @@ import java.io.InputStream; * @version $Revision: 1 $ */ public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener { + + public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; + private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected AdapterDeploymentContext deploymentContext; @@ -63,14 +70,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); if (ksc != null) { request.removeAttribute(KeycloakSecurityContext.class.getName()); - Session session = request.getSessionInternal(false); - if (session != null) { - session.removeNote(KeycloakSecurityContext.class.getName()); - if (ksc instanceof RefreshableKeycloakSecurityContext) { - CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null); - ((RefreshableKeycloakSecurityContext)ksc).logout(deploymentContext.resolveDeployment(facade)); - } - } + CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); + tokenStore.logout(); } super.logout(request); } @@ -164,10 +168,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif log.info("*** deployment isn't configured return false"); return false; } + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); nodesRegistrationManagement.tryRegister(deployment); - CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, userSessionManagement, facade, request); + CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, tokenStore, facade, request); AuthOutcome outcome = authenticator.authenticate(); if (outcome == AuthOutcome.AUTHENTICATED) { if (facade.isEnded()) { @@ -188,27 +193,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif * @param request */ protected void checkKeycloakSession(Request request, HttpFacade facade) { - if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return; - RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); - if (session == null) return; - // just in case session got serialized - if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade)); - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return; - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - Session catalinaSession = request.getSessionInternal(); - log.debugf("Cleanup and expire session %s after failed refresh", catalinaSession.getId()); - catalinaSession.removeNote(KeycloakSecurityContext.class.getName()); - request.setUserPrincipal(null); - request.setAuthType(null); - catalinaSession.setPrincipal(null); - catalinaSession.setAuthType(null); - catalinaSession.expire(); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); + tokenStore.checkCurrentToken(); } public void keycloakSaveRequest(Request request) throws IOException { @@ -223,4 +210,20 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif } } + protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { + AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE); + if (store != null) { + return store; + } + + if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) { + store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement); + } else { + store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment); + } + + request.setNote(TOKEN_STORE_NOTE, store); + return store; + } + } diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java new file mode 100644 index 0000000000..dec12b917d --- /dev/null +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java @@ -0,0 +1,107 @@ +package org.keycloak.adapters.tomcat7; + +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * @author Marek Posolda + */ +public class CatalinaCookieTokenStore implements AdapterTokenStore { + + private static final Logger log = Logger.getLogger(""+CatalinaCookieTokenStore.class); + + private Request request; + private HttpFacade facade; + private KeycloakDeployment deployment; + + private KeycloakPrincipal authenticatedPrincipal; + + public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) { + this.request = request; + this.facade = facade; + this.deployment = deployment; + } + + + @Override + public void checkCurrentToken() { + this.authenticatedPrincipal = checkPrincipalFromCookie(); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + // Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request + if (authenticatedPrincipal != null) { + log.fine("remote logged in already. Establish state from cookie"); + RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext(); + securityContext.setCurrentRequestInfo(deployment, this); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext); + + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + request.setUserPrincipal(principal); + request.setAuthType("KEYCLOAK"); + return true; + } else { + return false; + } + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + CookieTokenStore.setTokenCookie(deployment, facade, securityContext); + } + + @Override + public void logout() { + CookieTokenStore.removeCookie(facade); + + KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + if (ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext secContext) { + CookieTokenStore.setTokenCookie(deployment, facade, secContext); + } + + /** + * Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active + * + * @return valid principal + */ + protected KeycloakPrincipal checkPrincipalFromCookie() { + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); + if (principal == null) { + log.fine("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()) return principal; + + log.fine("Cleanup and expire cookie for user " + principal.getName() + " after failed refresh"); + request.setUserPrincipal(null); + request.setAuthType(null); + CookieTokenStore.removeCookie(facade); + return null; + } +} diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java index 8516b1ad8b..cc9e56abd3 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaRequestAuthenticator.java @@ -1,20 +1,20 @@ package org.keycloak.adapters.tomcat7; -import org.apache.catalina.Session; import org.apache.catalina.authenticator.Constants; import org.apache.catalina.connector.Request; -import org.apache.catalina.realm.GenericPrincipal; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OAuthRequestAuthenticator; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RequestAuthenticator; -import org.keycloak.representations.AccessToken; +import org.keycloak.enums.TokenStore; import java.io.IOException; import java.security.Principal; -import java.util.Collections; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,16 +28,14 @@ import javax.servlet.http.HttpSession; public class CatalinaRequestAuthenticator extends RequestAuthenticator { private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class); protected KeycloakAuthenticatorValve valve; - protected CatalinaUserSessionManagement userSessionManagement; protected Request request; public CatalinaRequestAuthenticator(KeycloakDeployment deployment, - KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement, + KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore, CatalinaHttpFacade facade, Request request) { - super(facade, deployment, request.getConnector().getRedirectPort()); + super(facade, deployment, tokenStore, request.getConnector().getRedirectPort()); this.valve = valve; - this.userSessionManagement = userSessionManagement; this.request = request; } @@ -47,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { @Override protected void saveRequest() { try { - valve.keycloakSaveRequest(request); + // Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi? + if (deployment.getTokenStore() == TokenStore.SESSION) { + valve.keycloakSaveRequest(request); + } } catch (IOException e) { throw new RuntimeException(e); } @@ -56,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } @Override - protected void completeOAuthAuthentication(KeycloakPrincipal skp) { - RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); + protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { + final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); + final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + KeycloakAccount account = new KeycloakAccount() { + + @Override + public Principal getPrincipal() { + return skp; + } + + @Override + public Set getRoles() { + return roles; + } + + @Override + public KeycloakSecurityContext getKeycloakSecurityContext() { + return securityContext; + } + + }; + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - Set roles = getRolesFromToken(securityContext); - GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext); - Session session = request.getSessionInternal(true); - session.setPrincipal(principal); - session.setAuthType("OAUTH"); - session.setNote(KeycloakSecurityContext.class.getName(), securityContext); - String username = securityContext.getToken().getSubject(); - log.finer("userSessionManagement.login: " + username); - userSessionManagement.login(session); + this.tokenStore.saveAccountInfo(account); } @Override protected void completeBearerAuthentication(KeycloakPrincipal principal) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); - Set roles = getRolesFromToken(securityContext); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); if (log.isLoggable(Level.FINE)) { log.fine("Completing bearer authentication. Bearer roles: " + roles); } @@ -83,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); } - protected Set getRolesFromToken(RefreshableKeycloakSecurityContext session) { - Set roles = null; - if (deployment.isUseResourceRoleMappings()) { - AccessToken.Access access = session.getToken().getResourceAccess(deployment.getResourceName()); - if (access != null) roles = access.getRoles(); - } else { - AccessToken.Access access = session.getToken().getRealmAccess(); - if (access != null) roles = access.getRoles(); - } - if (roles == null) roles = Collections.emptySet(); - return roles; - } - - @Override - protected boolean isCached() { - if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) - return false; - log.finer("remote logged in already"); - GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); - request.setUserPrincipal(principal); - request.setAuthType("KEYCLOAK"); - Session session = request.getSessionInternal(); - if (session != null) { - RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getNote(KeycloakSecurityContext.class.getName()); - if (securityContext != null) { - securityContext.setDeployment(deployment); - request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); - } - } - restoreRequest(); - return true; - } - protected void restoreRequest() { if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) { if (valve.keycloakRestoreRequest(request)) { diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java new file mode 100644 index 0000000000..81a765bb1c --- /dev/null +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java @@ -0,0 +1,108 @@ +package org.keycloak.adapters.tomcat7; + +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.realm.GenericPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * @author Marek Posolda + */ +public class CatalinaSessionTokenStore implements AdapterTokenStore { + + private static final Logger log = Logger.getLogger(""+CatalinaSessionTokenStore.class); + + private Request request; + private KeycloakDeployment deployment; + private CatalinaUserSessionManagement sessionManagement; + + public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment, CatalinaUserSessionManagement sessionManagement) { + this.request = request; + this.deployment = deployment; + this.sessionManagement = sessionManagement; + } + + @Override + public void checkCurrentToken() { + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return; + RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); + if (session == null) return; + // just in case session got serialized + if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) return; + + // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session + Session catalinaSession = request.getSessionInternal(); + log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh"); + catalinaSession.removeNote(KeycloakSecurityContext.class.getName()); + request.setUserPrincipal(null); + request.setAuthType(null); + catalinaSession.setPrincipal(null); + catalinaSession.setAuthType(null); + catalinaSession.expire(); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) + return false; + log.fine("remote logged in already. Establish state from session"); + GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); + request.setUserPrincipal(principal); + request.setAuthType("KEYCLOAK"); + + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); + if (securityContext != null) { + securityContext.setCurrentRequestInfo(deployment, this); + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + } + + ((CatalinaRequestAuthenticator)authenticator).restoreRequest(); + return true; + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + Set roles = account.getRoles(); + GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), account.getPrincipal(), roles, securityContext); + + Session session = request.getSessionInternal(true); + session.setPrincipal(principal); + session.setAuthType("OAUTH"); + session.setNote(KeycloakSecurityContext.class.getName(), securityContext); + String username = securityContext.getToken().getSubject(); + log.fine("userSessionManagement.login: " + username); + this.sessionManagement.login(session); + } + + @Override + public void logout() { + Session session = request.getSessionInternal(false); + if (session != null) { + session.removeNote(KeycloakSecurityContext.class.getName()); + KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + if (ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + // no-op + } +} diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java index 94447a1a59..0ffa42c857 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java @@ -15,6 +15,7 @@ import org.apache.catalina.deploy.LoginConfig; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.AdapterConstants; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.HttpFacade; @@ -24,6 +25,7 @@ import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.ServerRequest; +import org.keycloak.enums.TokenStore; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -45,6 +47,9 @@ import java.util.logging.Logger; * @version $Revision: 1 $ */ public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener { + + public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; + private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class); protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement(); protected AdapterDeploymentContext deploymentContext; @@ -70,16 +75,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); if (ksc != null) { request.removeAttribute(KeycloakSecurityContext.class.getName()); - Session session = request.getSessionInternal(false); - if (session != null) { - session.removeNote(KeycloakSecurityContext.class.getName()); - try { - CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null); - ServerRequest.invokeLogout(deploymentContext.resolveDeployment(facade), ksc.getToken().getSessionState()); - } catch (Exception e) { - log.severe("failed to invoke remote logout. " + e.getMessage()); - } - } + CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); + tokenStore.logout(); } super.logout(request); } @@ -164,10 +164,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif if (deployment == null || !deployment.isConfigured()) { return false; } + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); nodesRegistrationManagement.tryRegister(deployment); - CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, userSessionManagement, facade, request); + CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, tokenStore, facade, request); AuthOutcome outcome = authenticator.authenticate(); if (outcome == AuthOutcome.AUTHENTICATED) { if (facade.isEnded()) { @@ -188,27 +189,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif * @param request */ protected void checkKeycloakSession(Request request, HttpFacade facade) { - if (request.getSessionInternal(false) == null || request.getPrincipal() == null) return; - RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName()); - if (session == null) return; - // just in case session got serialized - if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade)); - if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; - - // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will - // not be updated - boolean success = session.refreshExpiredToken(false); - if (success && session.isActive()) return; - - // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session - Session catalinaSession = request.getSessionInternal(); - log.fine("Cleanup and expire session " + catalinaSession + " after failed refresh"); - catalinaSession.removeNote(KeycloakSecurityContext.class.getName()); - request.setUserPrincipal(null); - request.setAuthType(null); - catalinaSession.setPrincipal(null); - catalinaSession.setAuthType(null); - catalinaSession.expire(); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment); + tokenStore.checkCurrentToken(); } public void keycloakSaveRequest(Request request) throws IOException { @@ -223,4 +206,20 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif } } + protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { + AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE); + if (store != null) { + return store; + } + + if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) { + store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement); + } else { + store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment); + } + + request.setNote(TOKEN_STORE_NOTE, store); + return store; + } + } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java index 472bc90410..82c272733b 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java @@ -19,14 +19,15 @@ package org.keycloak.adapters.undertow; import io.undertow.security.idm.Account; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.representations.AccessToken; import java.io.Serializable; import java.security.Principal; -import java.util.Collections; import java.util.Set; /** @@ -40,33 +41,11 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA public KeycloakUndertowAccount(KeycloakPrincipal principal) { this.principal = principal; - setRoles(principal.getKeycloakSecurityContext().getToken()); + setRoles(principal.getKeycloakSecurityContext()); } - protected void setRoles(AccessToken accessToken) { - Set roles = null; - RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); - if (session.getDeployment().isUseResourceRoleMappings()) { - if (log.isTraceEnabled()) { - log.trace("useResourceRoleMappings"); - } - AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName()); - if (access != null) roles = access.getRoles(); - } else { - if (log.isTraceEnabled()) { - log.trace("use realm role mappings"); - } - AccessToken.Access access = accessToken.getRealmAccess(); - if (access != null) roles = access.getRoles(); - } - if (roles == null) roles = Collections.emptySet(); - if (log.isTraceEnabled()) { - log.trace("Setting roles: "); - for (String role : roles) { - log.trace(" role: " + role); - } - } - + protected void setRoles(RefreshableKeycloakSecurityContext session) { + Set roles = AdapterUtils.getRolesFromSecurityContext(session); this.accountRoles = roles; } @@ -85,11 +64,12 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA return principal.getKeycloakSecurityContext(); } - public void setDeployment(KeycloakDeployment deployment) { - principal.getKeycloakSecurityContext().setDeployment(deployment); + public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { + principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore); } - public boolean isActive() { + // Check if accessToken is active and try to refresh if it's not + public boolean checkActive() { // this object may have been serialized, so we need to reset realm config/metadata RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { @@ -106,7 +86,7 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA } log.debug("refresh succeeded"); - setRoles(session.getToken()); + setRoles(session); return true; } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java index 7303bda5bb..7f18e907e0 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java @@ -25,9 +25,12 @@ import io.undertow.servlet.handlers.ServletRequestContext; import org.jboss.logging.Logger; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.enums.TokenStore; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @@ -40,13 +43,11 @@ import javax.servlet.http.HttpSession; public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech { private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class); - protected UndertowUserSessionManagement userSessionManagement; protected NodesRegistrationManagement nodesRegistrationManagement; protected ConfidentialPortManager portManager; public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager) { - super(deploymentContext); - this.userSessionManagement = userSessionManagement; + super(deploymentContext, userSessionManagement); this.nodesRegistrationManagement = nodesRegistrationManagement; this.portManager = portManager; } @@ -66,40 +67,12 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech { return keycloakAuthenticate(exchange, securityContext, authenticator); } - @Override - protected void registerNotifications(SecurityContext securityContext) { - - final NotificationReceiver logoutReceiver = new NotificationReceiver() { - @Override - public void handleNotification(SecurityNotification notification) { - if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return; - final ServletRequestContext servletRequestContext = notification.getExchange().getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - req.removeAttribute(KeycloakUndertowAccount.class.getName()); - req.removeAttribute(KeycloakSecurityContext.class.getName()); - HttpSession session = req.getSession(false); - if (session == null) return; - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) return; - session.removeAttribute(KeycloakSecurityContext.class.getName()); - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - if (account.getKeycloakSecurityContext() != null) { - UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange()); - account.getKeycloakSecurityContext().logout(deploymentContext.resolveDeployment(facade)); - } - } - }; - - securityContext.registerNotificationReceiver(logoutReceiver); - } - - - protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) { int confidentialPort = getConfidentilPort(exchange); + AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext); return new ServletRequestAuthenticator(facade, deployment, - confidentialPort, securityContext, exchange, userSessionManagement); + confidentialPort, securityContext, exchange, tokenStore); } protected int getConfidentilPort(HttpServerExchange exchange) { @@ -112,4 +85,13 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech { return confidentialPort; } + @Override + protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) { + if (deployment.getTokenStore() == TokenStore.SESSION) { + return new ServletSessionTokenStore(exchange, deployment, sessionManagement, securityContext); + } else { + return new UndertowCookieTokenStore(facade, deployment, securityContext); + } + } + } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java index 5416d3c29e..ed13f5616f 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java @@ -18,13 +18,11 @@ package org.keycloak.adapters.undertow; import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpServerExchange; -import io.undertow.server.session.Session; import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.util.Sessions; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.HttpFacade; -import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; @@ -41,34 +39,8 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, SecurityContext securityContext, HttpServerExchange exchange, - UndertowUserSessionManagement userSessionManagement) { - super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement); - } - - @Override - protected boolean isCached() { - HttpSession session = getSession(false); - if (session == null) { - log.debug("session was null, returning null"); - return false; - } - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) { - log.debug("Account was not in session, returning null"); - return false; - } - account.setDeployment(deployment); - if (account.isActive()) { - log.debug("Cached account found"); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - propagateKeycloakContext( account); - return true; - } else { - log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); - session.setAttribute(KeycloakUndertowAccount.class.getName(), null); - session.invalidate(); - return false; - } + AdapterTokenStore tokenStore) { + super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore); } @Override @@ -79,15 +51,6 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { req.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); } - @Override - protected void login(KeycloakAccount account) { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpSession session = getSession(true); - session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); - - } - @Override protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal) { return new KeycloakUndertowAccount(principal); diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java new file mode 100644 index 0000000000..cbc7a0bcfe --- /dev/null +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java @@ -0,0 +1,106 @@ +package org.keycloak.adapters.undertow; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpServerExchange; +import io.undertow.servlet.handlers.ServletRequestContext; +import org.jboss.logging.Logger; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * Per-request object. Storage of tokens in servlet session. + * + * @author Marek Posolda + */ +public class ServletSessionTokenStore implements AdapterTokenStore { + + protected static Logger log = Logger.getLogger(ServletSessionTokenStore.class); + + private final HttpServerExchange exchange; + private final KeycloakDeployment deployment; + private final UndertowUserSessionManagement sessionManagement; + private final SecurityContext securityContext; + + public ServletSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement, + SecurityContext securityContext) { + this.exchange = exchange; + this.deployment = deployment; + this.sessionManagement = sessionManagement; + this.securityContext = securityContext; + } + + @Override + public void checkCurrentToken() { + // no-op on undertow + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + HttpSession session = getSession(false); + if (session == null) { + log.debug("session was null, returning null"); + return false; + } + KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); + if (account == null) { + log.debug("Account was not in session, returning null"); + return false; + } + account.setCurrentRequestInfo(deployment, this); + if (account.checkActive()) { + log.debug("Cached account found"); + securityContext.authenticationComplete(account, "KEYCLOAK", false); + ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); + return true; + } else { + log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); + session.setAttribute(KeycloakUndertowAccount.class.getName(), null); + session.invalidate(); + return false; + } + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + HttpSession session = getSession(true); + session.setAttribute(KeycloakUndertowAccount.class.getName(), account); + sessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); + } + + @Override + public void logout() { + final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); + req.removeAttribute(KeycloakUndertowAccount.class.getName()); + req.removeAttribute(KeycloakSecurityContext.class.getName()); + HttpSession session = req.getSession(false); + if (session == null) return; + KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); + if (account == null) return; + session.removeAttribute(KeycloakSecurityContext.class.getName()); + session.removeAttribute(KeycloakUndertowAccount.class.getName()); + if (account.getKeycloakSecurityContext() != null) { + account.getKeycloakSecurityContext().logout(deployment); + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + // no-op + } + + protected HttpSession getSession(boolean create) { + final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); + return req.getSession(create); + } + +} diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java new file mode 100644 index 0000000000..50859190fb --- /dev/null +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java @@ -0,0 +1,79 @@ +package org.keycloak.adapters.undertow; + +import io.undertow.security.api.SecurityContext; +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * Per-request object. Storage of tokens in cookie + * + * @author Marek Posolda + */ +public class UndertowCookieTokenStore implements AdapterTokenStore { + + protected static Logger log = Logger.getLogger(UndertowCookieTokenStore.class); + + private final HttpFacade facade; + private final KeycloakDeployment deployment; + private final SecurityContext securityContext; + + public UndertowCookieTokenStore(HttpFacade facade, KeycloakDeployment deployment, + SecurityContext securityContext) { + this.facade = facade; + this.deployment = deployment; + this.securityContext = securityContext; + } + + @Override + public void checkCurrentToken() { + // no-op on undertow + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); + if (principal == null) { + log.debug("Account was not in cookie or was invalid, returning null"); + return false; + } + KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal); + + if (account.checkActive()) { + log.debug("Cached account found"); + securityContext.authenticationComplete(account, "KEYCLOAK", false); + ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); + return true; + } else { + log.debug("Account was not active, removing cookie and returning false"); + CookieTokenStore.removeCookie(facade); + return false; + } + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + CookieTokenStore.setTokenCookie(deployment, facade, secContext); + } + + @Override + public void logout() { + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this); + if (principal == null) return; + + CookieTokenStore.removeCookie(facade); + principal.getKeycloakSecurityContext().logout(deployment); + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + CookieTokenStore.setTokenCookie(deployment, facade, securityContext); + } +} diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java index d1b9e4e4ef..1c875cbfb7 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java @@ -24,10 +24,17 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.util.AttachmentKey; import io.undertow.util.Sessions; +import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.enums.TokenStore; /** * Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism. @@ -37,9 +44,11 @@ import org.keycloak.adapters.RequestAuthenticator; public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism { public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); protected AdapterDeploymentContext deploymentContext; + protected UndertowUserSessionManagement sessionManagement; - public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext) { + public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) { this.deploymentContext = deploymentContext; + this.sessionManagement = sessionManagement; } @Override @@ -54,21 +63,17 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis return new ChallengeResult(false); } - protected void registerNotifications(SecurityContext securityContext) { + protected void registerNotifications(final SecurityContext securityContext) { final NotificationReceiver logoutReceiver = new NotificationReceiver() { @Override public void handleNotification(SecurityNotification notification) { if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return; - Session session = Sessions.getSession(notification.getExchange()); - if (session == null) return; - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) return; - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - if (account.getKeycloakSecurityContext() != null) { - UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange()); - account.getKeycloakSecurityContext().logout(deploymentContext.resolveDeployment(facade)); - } + + UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange()); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + AdapterTokenStore tokenStore = getTokenStore(notification.getExchange(), facade, deployment, securityContext); + tokenStore.logout(); } }; @@ -96,4 +101,12 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis return AuthenticationMechanismOutcome.NOT_ATTEMPTED; } + protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) { + if (deployment.getTokenStore() == TokenStore.SESSION) { + return new UndertowSessionTokenStore(exchange, deployment, sessionManagement, securityContext); + } else { + return new UndertowCookieTokenStore(facade, deployment, securityContext); + } + } + } \ No newline at end of file diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java index 671f2e7c36..6b9c3518f7 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java @@ -21,8 +21,8 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.util.Sessions; import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.HttpFacade; -import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OAuthRequestAuthenticator; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; @@ -36,16 +36,14 @@ import org.keycloak.adapters.RequestAuthenticator; public abstract class UndertowRequestAuthenticator extends RequestAuthenticator { protected SecurityContext securityContext; protected HttpServerExchange exchange; - protected UndertowUserSessionManagement userSessionManagement; public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, SecurityContext securityContext, HttpServerExchange exchange, - UndertowUserSessionManagement userSessionManagement) { - super(facade, deployment, sslRedirectPort); + AdapterTokenStore tokenStore) { + super(facade, deployment, tokenStore, sslRedirectPort); this.securityContext = securityContext; this.exchange = exchange; - this.userSessionManagement = userSessionManagement; } protected void propagateKeycloakContext(KeycloakUndertowAccount account) { @@ -67,16 +65,9 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator KeycloakUndertowAccount account = createAccount(principal); securityContext.authenticationComplete(account, "KEYCLOAK", false); propagateKeycloakContext(account); - login(account); + tokenStore.saveAccountInfo(account); } - protected void login(KeycloakAccount account) { - Session session = Sessions.getOrCreateSession(exchange); - session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - userSessionManagement.login(session.getSessionManager()); - } - - @Override protected void completeBearerAuthentication(KeycloakPrincipal principal) { KeycloakUndertowAccount account = createAccount(principal); @@ -84,32 +75,6 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator propagateKeycloakContext(account); } - @Override - protected boolean isCached() { - Session session = Sessions.getSession(exchange); - if (session == null) { - log.debug("session was null, returning null"); - return false; - } - KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); - if (account == null) { - log.debug("Account was not in session, returning null"); - return false; - } - account.setDeployment(deployment); - if (account.isActive()) { - log.debug("Cached account found"); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - propagateKeycloakContext( account); - return true; - } else { - log.debug("Account was not active, returning false"); - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - session.invalidate(exchange); - return false; - } - } - @Override protected String getHttpSessionId(boolean create) { if (create) { diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java new file mode 100644 index 0000000000..92848847ec --- /dev/null +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java @@ -0,0 +1,90 @@ +package org.keycloak.adapters.undertow; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.session.Session; +import io.undertow.util.Sessions; +import org.jboss.logging.Logger; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +/** + * Per-request object. Storage of tokens in undertow session. + * + * @author Marek Posolda + */ +public class UndertowSessionTokenStore implements AdapterTokenStore { + + protected static Logger log = Logger.getLogger(UndertowSessionTokenStore.class); + + private final HttpServerExchange exchange; + private final KeycloakDeployment deployment; + private final UndertowUserSessionManagement sessionManagement; + private final SecurityContext securityContext; + + public UndertowSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement, + SecurityContext securityContext) { + this.exchange = exchange; + this.deployment = deployment; + this.sessionManagement = sessionManagement; + this.securityContext = securityContext; + } + + @Override + public void checkCurrentToken() { + // no-op on undertow + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + Session session = Sessions.getSession(exchange); + if (session == null) { + log.debug("session was null, returning null"); + return false; + } + KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); + if (account == null) { + log.debug("Account was not in session, returning null"); + return false; + } + account.setCurrentRequestInfo(deployment, this); + if (account.checkActive()) { + log.debug("Cached account found"); + securityContext.authenticationComplete(account, "KEYCLOAK", false); + ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account); + return true; + } else { + log.debug("Account was not active, returning false"); + session.removeAttribute(KeycloakUndertowAccount.class.getName()); + session.invalidate(exchange); + return false; + } + } + + @Override + public void saveAccountInfo(KeycloakAccount account) { + Session session = Sessions.getOrCreateSession(exchange); + session.setAttribute(KeycloakUndertowAccount.class.getName(), account); + sessionManagement.login(session.getSessionManager()); + } + + @Override + public void logout() { + Session session = Sessions.getSession(exchange); + if (session == null) return; + KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); + if (account == null) return; + session.removeAttribute(KeycloakUndertowAccount.class.getName()); + if (account.getKeycloakSecurityContext() != null) { + account.getKeycloakSecurityContext().logout(deployment); + } + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + // no-op + } +} diff --git a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java index cdb5315b97..6880d669eb 100755 --- a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java +++ b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java @@ -4,6 +4,7 @@ import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.api.ConfidentialPortManager; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.undertow.ServletKeycloakAuthMech; @@ -27,7 +28,8 @@ public class WildflyAuthenticationMechanism extends ServletKeycloakAuthMech { @Override protected ServletRequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) { int confidentialPort = getConfidentilPort(exchange); + AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext); return new WildflyRequestAuthenticator(facade, deployment, - confidentialPort, securityContext, exchange, userSessionManagement); + confidentialPort, securityContext, exchange, tokenStore); } } diff --git a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java index bc8a6de787..84074d67f0 100755 --- a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java +++ b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java @@ -8,6 +8,7 @@ import org.jboss.security.SecurityConstants; import org.jboss.security.SecurityContextAssociation; import org.jboss.security.SimpleGroup; import org.jboss.security.SimplePrincipal; +import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.undertow.KeycloakUndertowAccount; @@ -31,8 +32,8 @@ public class WildflyRequestAuthenticator extends ServletRequestAuthenticator { public WildflyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort, SecurityContext securityContext, HttpServerExchange exchange, - UndertowUserSessionManagement userSessionManagement) { - super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement); + AdapterTokenStore tokenStore) { + super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore); } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java index 5443a7e742..cca4389041 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java @@ -32,7 +32,6 @@ import org.keycloak.adapters.AdapterConstants; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -112,7 +111,7 @@ public class AdapterTest { TokenManager tm = new TokenManager(); UserModel admin = session.users().getUserByUsername("admin", adminRealm); UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false); - AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession); + AccessToken token = tm.createClientAccessToken(TokenManager.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession); return tm.encodeToken(adminRealm, token); } finally { keycloakRule.stopSession(session, true); @@ -440,7 +439,7 @@ public class AdapterTest { // Open browser2 browser2.webRule.before(); try { - browser2.loginAndCheckSession(browser2.driver, browser2.loginPage); + loginAndCheckSession(browser2.driver, browser2.loginPage); // Logout in browser1 String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth")) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CookieTokenStoreAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CookieTokenStoreAdapterTest.java new file mode 100644 index 0000000000..e76d892173 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CookieTokenStoreAdapterTest.java @@ -0,0 +1,180 @@ +package org.keycloak.testsuite.adapter; + +import java.net.URL; + +import javax.ws.rs.core.UriBuilder; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.adapters.AdapterConstants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OpenIDConnectService; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.testutils.KeycloakServer; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.WebDriver; + +/** + * KEYCLOAK-702 + * + * @author Marek Posolda + */ +public class CookieTokenStoreAdapterTest { + + public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString(); + + @ClassRule + public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() { + + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class); + manager.importRealm(representation); + + URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json"); + deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user"); + url = getClass().getResource("/adapter-test/cust-app-cookie-keycloak.json"); + deployApplication("customer-cookie-portal", "/customer-cookie-portal", CustomerServlet.class, url.getPath(), "user"); + url = getClass().getResource("/adapter-test/customer-db-keycloak.json"); + deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user"); + } + }; + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected LoginPage loginPage; + + @Test + public void testTokenInCookieSSO() throws Throwable { + // Login + String tokenCookie = loginToCustomerCookiePortal(); + + // SSO to second app + driver.navigate().to("http://localhost:8081/customer-portal"); + assertLogged(); + + // return to customer-cookie-portal and assert still same cookie (accessToken didn't expire) + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + assertLogged(); + String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue(); + Assert.assertEquals(tokenCookie, tokenCookie2); + + // Logout with httpServletRequest + logoutFromCustomerCookiePortal(); + + // Also should be logged-out from the second app + driver.navigate().to("http://localhost:8081/customer-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + } + + @Test + public void testTokenInCookieRefresh() throws Throwable { + // Set token timeout 1 sec + KeycloakSession session = keycloakRule.startSession(); + RealmModel realm = session.realms().getRealmByName("demo"); + int originalTokenTimeout = realm.getAccessTokenLifespan(); + realm.setAccessTokenLifespan(1); + session.getTransaction().commit(); + session.close(); + + // login to customer-cookie-portal + String tokenCookie1 = loginToCustomerCookiePortal(); + + // wait 2 secs + Thread.sleep(2000); + + // assert cookie was refreshed + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal"); + assertLogged(); + String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue(); + Assert.assertNotEquals(tokenCookie1, tokenCookie2); + + // login to 2nd app and logout from it + driver.navigate().to("http://localhost:8081/customer-portal"); + Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal"); + assertLogged(); + + driver.navigate().to("http://localhost:8081/customer-portal/logout"); + driver.navigate().to("http://localhost:8081/customer-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + + // wait 2 secs until accessToken expires for customer-cookie-portal too. + Thread.sleep(2000); + + // assert not logged in customer-cookie-portal + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + + // Change timeout back + session = keycloakRule.startSession(); + realm = session.realms().getRealmByName("demo"); + realm.setAccessTokenLifespan(originalTokenTimeout); + session.getTransaction().commit(); + session.close(); + } + + @Test + public void testInvalidTokenCookie() throws Throwable { + // Login + String tokenCookie = loginToCustomerCookiePortal(); + String changedTokenCookie = tokenCookie.replace("a", "b"); + + // change cookie to invalid value + driver.manage().addCookie(new Cookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, changedTokenCookie, "/customer-cookie-portal")); + + // visit page and assert re-logged and cookie was refreshed + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal"); + String currentCookie = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue(); + Assert.assertNotEquals(currentCookie, tokenCookie); + Assert.assertNotEquals(currentCookie, changedTokenCookie); + + // logout + logoutFromCustomerCookiePortal(); + } + + // login to customer-cookie-portal and return the KEYCLOAK_ADAPTER_STATE cookie established on adapter + private String loginToCustomerCookiePortal() { + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + loginPage.login("bburke@redhat.com", "password"); + Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal"); + assertLogged(); + + // Assert no JSESSIONID cookie + Assert.assertNull(driver.manage().getCookieNamed("JSESSIONID")); + + return driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue(); + } + + private void logoutFromCustomerCookiePortal() { + driver.navigate().to("http://localhost:8081/customer-cookie-portal/logout"); + Assert.assertTrue(driver.getPageSource().contains("ok")); + Assert.assertNull(driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE)); + driver.navigate().to("http://localhost:8081/customer-cookie-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + } + + private void assertLogged() { + String pageSource = driver.getPageSource(); + Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen")); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java index eafe55b5d3..a32bc1711c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java @@ -29,8 +29,10 @@ public class CustomerServlet extends HttpServlet { if (req.getRequestURI().toString().endsWith("logout")) { resp.setStatus(200); pw.println("ok"); - pw.flush(); + + // Call logout before pw.flush req.logout(); + pw.flush(); return; } KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName()); diff --git a/testsuite/integration/src/test/resources/adapter-test/cust-app-cookie-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/cust-app-cookie-keycloak.json new file mode 100644 index 0000000000..92fe860ae7 --- /dev/null +++ b/testsuite/integration/src/test/resources/adapter-test/cust-app-cookie-keycloak.json @@ -0,0 +1,12 @@ +{ + "realm": "demo", + "resource": "customer-cookie-portal", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8081/auth", + "ssl-required" : "external", + "expose-token": true, + "token-store": "cookie", + "credentials": { + "secret": "password" + } +} \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json index fc6ebaedbd..9a5da2428a 100755 --- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json +++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json @@ -68,6 +68,15 @@ ], "secret": "password" }, + { + "name": "customer-cookie-portal", + "enabled": true, + "baseUrl": "http://localhost:8081/customer-cookie-portal", + "redirectUris": [ + "http://localhost:8081/customer-cookie-portal/*" + ], + "secret": "password" + }, { "name": "customer-portal-js", "enabled": true,