diff --git a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java index 6ad29c7ebd..a4f7f51bc4 100755 --- a/core/src/main/java/org/keycloak/adapters/AdapterConstants.java +++ b/core/src/main/java/org/keycloak/adapters/AdapterConstants.java @@ -18,4 +18,10 @@ public interface AdapterConstants { // org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor. We have this value in // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration. String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; + + // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains ID of HttpSession on adapter + public static final String HTTP_SESSION_ID = "http_session_id"; + + // Attribute passed in codeToToken request from adapter to Keycloak and saved in ClientSession. Contains hostname of adapter where HttpSession is served + public static final String HTTP_SESSION_HOST = "http_session_host"; } diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java index b89b10d6ff..a14b3f8340 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java +++ b/core/src/main/java/org/keycloak/representations/adapters/action/LogoutAction.java @@ -1,40 +1,25 @@ package org.keycloak.representations.adapters.action; +import java.util.List; + /** * @author Bill Burke * @version $Revision: 1 $ */ public class LogoutAction extends AdminAction { public static final String LOGOUT = "LOGOUT"; - protected String user; - private String session; + protected List adapterSessionIds; protected int notBefore; public LogoutAction() { } - public LogoutAction(String id, int expiration, String resource, String user, String session, int notBefore) { + public LogoutAction(String id, int expiration, String resource, List adapterSessionIds, int notBefore) { super(id, expiration, resource, LOGOUT); - this.user = user; - this.session = session; + this.adapterSessionIds = adapterSessionIds; this.notBefore = notBefore; } - public String getUser() { - return user; - } - - public void setUser(String user) { - this.user = user; - } - - public String getSession() { - return session; - } - - public void setSession(String session) { - this.session = session; - } public int getNotBefore() { return notBefore; @@ -44,6 +29,10 @@ public class LogoutAction extends AdminAction { this.notBefore = notBefore; } + public List getAdapterSessionIds() { + return adapterSessionIds; + } + @Override public boolean validate() { return LOGOUT.equals(action); diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java deleted file mode 100755 index f6644335c6..0000000000 --- a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStats.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.keycloak.representations.adapters.action; - -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SessionStats { - protected int activeSessions; - protected int activeUsers; - protected Map users; - - public int getActiveSessions() { - return activeSessions; - } - - public void setActiveSessions(int activeSessions) { - this.activeSessions = activeSessions; - } - - public int getActiveUsers() { - return activeUsers; - } - - public void setActiveUsers(int activeUsers) { - this.activeUsers = activeUsers; - } - - public Map getUsers() { - return users; - } - - public void setUsers(Map users) { - this.users = users; - } -} diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java deleted file mode 100755 index 1bcb1fafc4..0000000000 --- a/core/src/main/java/org/keycloak/representations/adapters/action/SessionStatsAction.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.keycloak.representations.adapters.action; - -/** - * Query session stats. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SessionStatsAction extends AdminAction { - - public static final String SESSION_STATS = "SESSION_STATS"; - - protected boolean listUsers; - - public SessionStatsAction() { - } - - public SessionStatsAction(String id, int expiration, String resource) { - super(id, expiration, resource, SESSION_STATS); - } - - public boolean isListUsers() { - return listUsers; - } - - public void setListUsers(boolean listUsers) { - this.listUsers = listUsers; - } - - @Override - public boolean validate() { - return SESSION_STATS.equals(action); - } - -} diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java deleted file mode 100755 index c7e5b63fc4..0000000000 --- a/core/src/main/java/org/keycloak/representations/adapters/action/UserStatsAction.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.keycloak.representations.adapters.action; - -/** - * Query session stats. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UserStatsAction extends AdminAction { - - public static final String USER_STATS = "USER_STATS"; - protected String user; - - public UserStatsAction() { - } - - public UserStatsAction(String id, int expiration, String resource, String user) { - super(id, expiration, resource, USER_STATS); - this.user = user; - } - - public String getUser() { - return user; - } - - @Override - public boolean validate() { - return USER_STATS.equals(action); - } - -} 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 fd54ce2b3e..7f62fd18bb 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 @@ -17,7 +17,7 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder; "connection-pool-size", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore", "client-keystore-password", "client-key-password", - "auth-server-url-for-backend-requests" + "auth-server-url-for-backend-requests", "always-refresh-token" }) public class AdapterConfig extends BaseAdapterConfig { @@ -39,6 +39,8 @@ public class AdapterConfig extends BaseAdapterConfig { protected int connectionPoolSize = 20; @JsonProperty("auth-server-url-for-backend-requests") protected String authServerUrlForBackendRequests; + @JsonProperty("always-refresh-token") + protected boolean alwaysRefreshToken = false; public boolean isAllowAnyHostname() { return allowAnyHostname; @@ -111,4 +113,12 @@ public class AdapterConfig extends BaseAdapterConfig { public void setAuthServerUrlForBackendRequests(String authServerUrlForBackendRequests) { this.authServerUrlForBackendRequests = authServerUrlForBackendRequests; } + + public boolean isAlwaysRefreshToken() { + return alwaysRefreshToken; + } + + public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { + this.alwaysRefreshToken = alwaysRefreshToken; + } } diff --git a/core/src/main/java/org/keycloak/util/HostUtils.java b/core/src/main/java/org/keycloak/util/HostUtils.java new file mode 100644 index 0000000000..fb1b29fce1 --- /dev/null +++ b/core/src/main/java/org/keycloak/util/HostUtils.java @@ -0,0 +1,36 @@ +package org.keycloak.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @author Marek Posolda + */ +public class HostUtils { + + public static String getHostName() { + String jbossHostName = System.getProperty("jboss.host.name"); + if (jbossHostName != null) { + return jbossHostName; + } else { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException uhe) { + throw new IllegalStateException(uhe); + } + } + } + + public static String getIpAddress() { + try { + String jbossHostName = System.getProperty("jboss.host.name"); + if (jbossHostName != null) { + return InetAddress.getByName(jbossHostName).getHostAddress(); + } else { + return java.net.InetAddress.getLocalHost().getHostAddress(); + } + } catch (UnknownHostException uhe) { + throw new IllegalStateException(uhe); + } + } +} diff --git a/core/src/main/java/org/keycloak/util/UriUtils.java b/core/src/main/java/org/keycloak/util/UriUtils.java index 60418ea225..873283f61e 100644 --- a/core/src/main/java/org/keycloak/util/UriUtils.java +++ b/core/src/main/java/org/keycloak/util/UriUtils.java @@ -1,8 +1,6 @@ package org.keycloak.util; -import java.net.InetAddress; import java.net.URI; -import java.net.UnknownHostException; /** * @author Stian Thorgersen @@ -18,12 +16,4 @@ public class UriUtils { return u.substring(0, u.indexOf('/', 8)); } - public static String getHostName() { - try { - return InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException uhe) { - throw new IllegalStateException(uhe); - } - } - } diff --git a/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java b/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java index 2a83775252..caa9ed6eea 100755 --- a/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java +++ b/examples/demo-template/admin-access-app/src/main/java/org/keycloak/example/AdminClient.java @@ -13,6 +13,7 @@ import org.keycloak.ServiceUrlConstants; import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.util.HostUtils; import org.keycloak.util.JsonSerialization; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.UriUtils; @@ -161,7 +162,7 @@ public class AdminClient { public static String getBaseUrl(HttpServletRequest request) { String useHostname = request.getServletContext().getInitParameter("useHostname"); if (useHostname != null && "true".equalsIgnoreCase(useHostname)) { - return "http://" + UriUtils.getHostName() + ":8080"; + return "http://" + HostUtils.getHostName() + ":8080"; } else { return UriUtils.getOrigin(request.getRequestURL().toString()); } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java index b42090f3e9..6d777478dd 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java @@ -131,7 +131,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { attributes.put("account", new AccountBean(user, profileFormData)); break; case TOTP: - attributes.put("totp", new TotpBean(user, baseUri)); + attributes.put("totp", new TotpBean(realm, user, baseUri)); break; case SOCIAL: attributes.put("social", new AccountSocialBean(session, realm, user, uriInfo.getBaseUri(), stateChecker)); diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java index 75c9fea725..33542e07c8 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java @@ -21,6 +21,7 @@ */ package org.keycloak.account.freemarker.model; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.Base32; @@ -35,17 +36,19 @@ import java.security.SecureRandom; */ public class TotpBean { - private String totpSecret; - private String totpSecretEncoded; - private boolean enabled; - private String contextUrl; + private final String totpSecret; + private final String totpSecretEncoded; + private final boolean enabled; + private final String contextUrl; + private final String realmName; - public TotpBean(UserModel user, URI baseUri) { + public TotpBean(RealmModel realm, UserModel user, URI baseUri) { + this.realmName = realm.getName(); this.enabled = user.isTotp(); this.contextUrl = baseUri.getPath(); - totpSecret = randomString(20); - totpSecretEncoded = Base32.encode(totpSecret.getBytes()); + this.totpSecret = randomString(20); + this.totpSecretEncoded = Base32.encode(totpSecret.getBytes()); } private static String randomString(int length) { @@ -86,7 +89,7 @@ public class TotpBean { } public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { - String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8"); + String contents = URLEncoder.encode("otpauth://totp/" + realmName + "?secret=" + totpSecretEncoded, "utf-8"); return contextUrl + "qrcode" + "?size=246x246&contents=" + contents; } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java index 497e75a7cf..7ac58644a9 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java @@ -35,19 +35,19 @@ import java.util.Random; */ public class TotpBean { - private String totpSecret; - private String totpSecretEncoded; - private boolean enabled; - private String contextUrl; - private String realmName; + private final String totpSecret; + private final String totpSecretEncoded; + private final boolean enabled; + private final String contextUrl; + private final String realmName; public TotpBean(RealmModel realm, UserModel user, URI baseUri) { this.realmName = realm.getName(); this.enabled = user.isTotp(); this.contextUrl = baseUri.getPath(); - - totpSecret = randomString(20); - totpSecretEncoded = Base32.encode(totpSecret.getBytes()); + + this.totpSecret = randomString(20); + this.totpSecretEncoded = Base32.encode(totpSecret.getBytes()); } private static String randomString(int length) { 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 f1e52923ad..69e61aaf20 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 @@ -326,6 +326,16 @@ public class AdapterDeploymentContext { public void setCorsAllowedHeaders(String corsAllowedHeaders) { delegate.setCorsAllowedHeaders(corsAllowedHeaders); } + + @Override + public boolean isAlwaysRefreshToken() { + return delegate.isAlwaysRefreshToken(); + } + + @Override + public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { + delegate.setAlwaysRefreshToken(alwaysRefreshToken); + } } protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) { 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 db284b19e0..c3761126f8 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 @@ -47,6 +47,7 @@ public class KeycloakDeployment { protected String corsAllowedHeaders; protected String corsAllowedMethods; protected boolean exposeToken; + protected boolean alwaysRefreshToken; protected volatile int notBefore; public KeycloakDeployment() { @@ -281,4 +282,11 @@ public class KeycloakDeployment { this.notBefore = notBefore; } + public boolean isAlwaysRefreshToken() { + return alwaysRefreshToken; + } + + public void setAlwaysRefreshToken(boolean alwaysRefreshToken) { + this.alwaysRefreshToken = alwaysRefreshToken; + } } 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 5aa996b428..6e5c29fd86 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 @@ -35,7 +35,7 @@ public class KeycloakDeploymentBuilder { String realmKeyPem = adapterConfig.getRealmKey(); if (realmKeyPem != null) { - PublicKey realmKey = null; + PublicKey realmKey; try { realmKey = PemUtils.decodePublicKey(realmKeyPem); } catch (Exception e) { @@ -60,9 +60,7 @@ public class KeycloakDeploymentBuilder { } deployment.setBearerOnly(adapterConfig.isBearerOnly()); - - if (adapterConfig.isBearerOnly()) { - } + deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); @@ -82,7 +80,7 @@ public class KeycloakDeploymentBuilder { public static KeycloakDeployment build(InputStream is) { ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); - AdapterConfig adapterConfig = null; + AdapterConfig adapterConfig; try { adapterConfig = mapper.readValue(is, AdapterConfig.class); } catch (IOException e) { 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 3932cb0369..c3c2e6e7ba 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 @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicLong; public abstract class OAuthRequestAuthenticator { private static final Logger log = Logger.getLogger(OAuthRequestAuthenticator.class); protected KeycloakDeployment deployment; + protected RequestAuthenticator reqAuthenticator; protected int sslRedirectPort; protected String tokenString; protected String idTokenString; @@ -31,7 +32,8 @@ public abstract class OAuthRequestAuthenticator { protected String refreshToken; protected String strippedOauthParametersRequestUri; - public OAuthRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) { + public OAuthRequestAuthenticator(RequestAuthenticator requestAuthenticator, HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) { + this.reqAuthenticator = requestAuthenticator; this.facade = facade; this.deployment = deployment; this.sslRedirectPort = sslRedirectPort; @@ -253,7 +255,8 @@ public abstract class OAuthRequestAuthenticator { AccessTokenResponse tokenResponse = null; strippedOauthParametersRequestUri = stripOauthParametersFromRedirect(); try { - tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri); + String httpSessionId = reqAuthenticator.getHttpSessionId(true); + tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId); } catch (ServerRequest.HttpFailure failure) { log.error("failed to turn code into token"); log.error("status from server: " + failure.getStatus()); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java index e2aff3f141..efc5463791 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java @@ -7,10 +7,6 @@ import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.representations.adapters.action.AdminAction; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; -import org.keycloak.representations.adapters.action.SessionStats; -import org.keycloak.representations.adapters.action.SessionStatsAction; -import org.keycloak.representations.adapters.action.UserStats; -import org.keycloak.representations.adapters.action.UserStatsAction; import org.keycloak.util.JsonSerialization; import org.keycloak.util.StreamUtil; @@ -60,14 +56,6 @@ public class PreAuthActionsHandler { if (!resolveDeployment()) return true; handlePushNotBefore(); return true; - } else if (requestUri.endsWith(AdapterConstants.K_GET_SESSION_STATS)) { - if (!resolveDeployment()) return true; - handleGetSessionStats(); - return true; - } else if (requestUri.endsWith(AdapterConstants.K_GET_USER_STATS)) { - if (!resolveDeployment()) return true; - handleGetUserStats(); - return true; } else if (requestUri.endsWith(AdapterConstants.K_VERSION)) { handleVersion(); return true; @@ -123,14 +111,10 @@ public class PreAuthActionsHandler { } LogoutAction action = JsonSerialization.readValue(token.getContent(), LogoutAction.class); if (!validateAction(action)) return; - String user = action.getUser(); - if (user != null) { - log.debug("logout of session for: " + user); - userSessionManagement.logoutUser(user); - } else if (action.getSession() != null) { - userSessionManagement.logoutKeycloakSession(action.getSession()); + if (action.getAdapterSessionIds() != null) { + userSessionManagement.logoutHttpSessions(action.getAdapterSessionIds()); } else { - log.debug("logout of all sessions"); + log.infof("logout of all sessions for application '%s'", action.getResource()); if (action.getNotBefore() > deployment.getNotBefore()) { deployment.setNotBefore(action.getNotBefore()); } @@ -208,50 +192,6 @@ public class PreAuthActionsHandler { return true; } - protected void handleGetSessionStats() { - if (log.isTraceEnabled()) { - log.trace("K_GET_SESSION_STATS sent"); - } - try { - JWSInput token = verifyAdminRequest(); - if (token == null) return; - SessionStatsAction action = JsonSerialization.readValue(token.getContent(), SessionStatsAction.class); - if (!validateAction(action)) return; - SessionStats stats = new SessionStats(); - stats.setActiveSessions(userSessionManagement.getActiveSessions()); - stats.setActiveUsers(userSessionManagement.getActiveUsers().size()); - if (action.isListUsers() && userSessionManagement.getActiveSessions() > 0) { - Map list = new HashMap(); - for (String user : userSessionManagement.getActiveUsers()) { - list.put(user, getUserStats(user)); - } - stats.setUsers(list); - } - facade.getResponse().setStatus(200); - facade.getResponse().setHeader("Content-Type", "application/json"); - JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), stats); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - protected void handleGetUserStats() { - if (log.isTraceEnabled()) { - log.trace("K_GET_USER_STATS sent"); - } - try { - JWSInput token = verifyAdminRequest(); - if (token == null) return; - UserStatsAction action = JsonSerialization.readValue(token.getContent(), UserStatsAction.class); - if (!validateAction(action)) return; - String user = action.getUser(); - UserStats stats = getUserStats(user); - facade.getResponse().setStatus(200); - facade.getResponse().setHeader("Content-Type", "application/json"); - JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), stats); - } catch (Exception e) { - throw new RuntimeException(e); - } - } protected void handleVersion() { try { facade.getResponse().setStatus(200); @@ -262,15 +202,4 @@ public class PreAuthActionsHandler { } } - protected UserStats getUserStats(String user) { - UserStats stats = new UserStats(); - Long loginTime = userSessionManagement.getUserLoginTime(user); - if (loginTime != null) { - stats.setLoggedIn(true); - stats.setWhenLoggedIn(loginTime); - } else { - stats.setLoggedIn(false); - } - return stats; - } } 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 7dfe62c722..3fc5b3d6e2 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 @@ -32,13 +32,13 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext @Override public AccessToken getToken() { - refreshExpiredToken(); + refreshExpiredToken(true); return super.getToken(); } @Override public String getTokenString() { - refreshExpiredToken(); + refreshExpiredToken(true); return super.getTokenString(); } @@ -62,12 +62,19 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext this.deployment = deployment; } - public void refreshExpiredToken() { - if (log.isTraceEnabled()) { - log.trace("checking whether to refresh."); + /** + * @param checkActive if true, then we won't send refresh request if current accessToken is still active. + * @return true if accessToken is active or was successfully refreshed + */ + public boolean refreshExpiredToken(boolean checkActive) { + if (checkActive) { + if (log.isTraceEnabled()) { + log.trace("checking whether to refresh."); + } + if (isActive()) return true; } - if (isActive()) return; - if (this.deployment == null || refreshToken == null) return; // Might be serialized in HttpSession? + + if (this.deployment == null || refreshToken == null) return false; // Might be serialized in HttpSession? if (log.isTraceEnabled()) { log.trace("Doing refresh"); @@ -77,10 +84,10 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext response = ServerRequest.invokeRefresh(deployment, refreshToken); } catch (IOException e) { log.error("Refresh token failure", e); - return; + return false; } catch (ServerRequest.HttpFailure httpFailure) { log.error("Refresh token failure status: " + httpFailure.getStatus() + " " + httpFailure.getError()); - return; + return false; } if (log.isTraceEnabled()) { log.trace("received refresh response"); @@ -100,7 +107,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext this.token = token; this.refreshToken = response.getRefreshToken(); this.tokenString = tokenString; - + return true; } 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 3cf2c91f55..0b3123816d 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 @@ -111,6 +111,7 @@ public abstract class RequestAuthenticator { 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); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java index 985495028d..b20697d354 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java @@ -11,6 +11,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.HostUtils; import org.keycloak.util.JsonSerialization; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.StreamUtil; @@ -84,21 +85,25 @@ public class ServerRequest { if (is != null) is.close(); } - public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri) throws HttpFailure, IOException { + public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId) throws HttpFailure, IOException { String codeUrl = deployment.getCodeUrl(); String client_id = deployment.getResourceName(); Map credentials = deployment.getResourceCredentials(); HttpClient client = deployment.getClient(); - return invokeAccessCodeToToken(client, deployment.isPublicClient(), code, codeUrl, redirectUri, client_id, credentials); + return invokeAccessCodeToToken(client, deployment.isPublicClient(), code, codeUrl, redirectUri, client_id, credentials, sessionId); } - public static AccessTokenResponse invokeAccessCodeToToken(HttpClient client, boolean publicClient, String code, String codeUrl, String redirectUri, String client_id, Map credentials) throws IOException, HttpFailure { + public static AccessTokenResponse invokeAccessCodeToToken(HttpClient client, boolean publicClient, String code, String codeUrl, String redirectUri, String client_id, Map credentials, String sessionId) throws IOException, HttpFailure { List formparams = new ArrayList(); redirectUri = stripOauthParametersFromRedirect(redirectUri); formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code")); formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri)); + if (sessionId != null) { + formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_ID, sessionId)); + formparams.add(new BasicNameValuePair(AdapterConstants.HTTP_SESSION_HOST, HostUtils.getIpAddress())); + } HttpResponse response = null; HttpPost post = new HttpPost(codeUrl); if (!publicClient) { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java index ca4653a20b..c89e3ec386 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/UserSessionManagement.java @@ -1,21 +1,14 @@ package org.keycloak.adapters; -import java.util.Set; +import java.util.List; /** * @author Bill Burke * @version $Revision: 1 $ */ public interface UserSessionManagement { - int getActiveSessions(); - - Long getUserLoginTime(String username); - - Set getActiveUsers(); void logoutAll(); - void logoutUser(String user); - - void logoutKeycloakSession(String id); + void logoutHttpSessions(List ids); } 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 aca4db7154..a1cb95c4eb 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 @@ -18,6 +18,8 @@ import java.security.Principal; import java.util.Collections; import java.util.Set; +import javax.servlet.http.HttpSession; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -40,7 +42,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { @Override protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(facade, deployment, sslRedirectPort) { + return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) { @Override protected void saveRequest() { try { @@ -64,15 +66,15 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { session.setNote(KeycloakSecurityContext.class.getName(), securityContext); String username = securityContext.getToken().getSubject(); log.debug("userSessionManage.login: " + username); - userSessionManagement.login(session, username, securityContext.getToken().getSessionState()); + userSessionManagement.login(session); } @Override protected void completeBearerAuthentication(KeycloakPrincipal principal) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); Set roles = getRolesFromToken(securityContext); - for (String role : roles) { - log.info("Bearer role: " + role); + if (log.isDebugEnabled()) { + log.debug("Completing bearer authentication. Bearer roles: " + roles); } Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles, securityContext); request.setUserPrincipal(generalPrincipal); @@ -123,4 +125,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } } } + + @Override + protected String getHttpSessionId(boolean create) { + HttpSession session = request.getSession(create); + return session != null ? session.getId() : null; + } } diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java index c1ac9803fc..ffb806e1ac 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagement.java @@ -1,17 +1,14 @@ package org.keycloak.adapters.as7; +import org.apache.catalina.Manager; import org.apache.catalina.Session; import org.apache.catalina.SessionEvent; import org.apache.catalina.SessionListener; import org.apache.catalina.realm.GenericPrincipal; import org.jboss.logging.Logger; -import org.keycloak.adapters.UserSessionManagement; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.io.IOException; +import java.util.List; /** * Manages relationship to users and sessions so that forced admin logout can be implemented @@ -19,112 +16,50 @@ import java.util.concurrent.ConcurrentHashMap; * @author Bill Burke * @version $Revision: 1 $ */ -public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { +public class CatalinaUserSessionManagement implements SessionListener { private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class); - protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); - protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); - public static class UserSessions { - protected String user; - protected long loggedIn = System.currentTimeMillis(); - protected Map keycloakSessionToHttpSession = new HashMap(); - protected Map httpSessionToKeycloakSession = new HashMap(); - protected Map sessions = new HashMap(); - public long getLoggedIn() { - return loggedIn; - } - } - - public synchronized int getActiveSessions() { - return keycloakSessionMap.size(); - } - - /** - * - * @param username - * @return null if user not logged in - */ - @Override - public synchronized Long getUserLoginTime(String username) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) return null; - return sessions.getLoggedIn(); - } - - @Override - public synchronized Set getActiveUsers() { - HashSet set = new HashSet(); - set.addAll(userSessionMap.keySet()); - return set; - } - - - public synchronized void login(Session session, String username, String keycloakSessionId) { - String sessionId = session.getId(); - - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) { - sessions = new UserSessions(); - sessions.user = username; - userSessionMap.put(username, sessions); - } - keycloakSessionMap.put(keycloakSessionId, sessions); - sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); - sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); - sessions.sessions.put(sessionId, session); + public void login(Session session) { session.addSessionListener(this); } - @Override - public void logoutAll() { - for (String user : userSessionMap.keySet()) logoutUser(user); + public void logoutAll(Manager sessionManager) { + Session[] allSessions = sessionManager.findSessions(); + for (Session session : allSessions) { + logoutSession(session); + } } - @Override - public void logoutUser(String user) { - log.debug("logoutUser: " + user); - UserSessions sessions = null; - sessions = userSessionMap.remove(user); - if (sessions == null) { - log.debug("no session for user: " + user); + public void logoutHttpSessions(Manager sessionManager, List sessionIds) { + log.debug("logoutHttpSessions: " + sessionIds); + + for (String sessionId : sessionIds) { + logoutSession(sessionManager, sessionId); + } + } + + protected void logoutSession(Manager manager, String httpSessionId) { + log.debug("logoutHttpSession: " + httpSessionId); + + Session session; + try { + session = manager.findSession(httpSessionId); + } catch (IOException ioe) { + log.warn("IO exception when looking for session " + httpSessionId, ioe); return; } - log.debug("found session for user"); - for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { - log.debug("invalidating session for user: " + user); - String sessionId = entry.getKey(); - String keycloakSessionId = entry.getValue(); - Session session = sessions.sessions.get(sessionId); - session.setPrincipal(null); - session.setAuthType(null); - session.getSession().invalidate(); - keycloakSessionMap.remove(keycloakSessionId); - } + + logoutSession(session); } - public synchronized void logoutKeycloakSession(String keycloakSessionId) { - log.debug("logoutKeycloakSession: " + keycloakSessionId); - UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); - if (sessions == null) { - log.debug("no session for keycloak session id: " + keycloakSessionId); - return; - } - String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - if (sessionId == null) { - log.debug("no session for keycloak session id: " + keycloakSessionId); - - } - sessions.httpSessionToKeycloakSession.remove(sessionId); - Session session = sessions.sessions.remove(sessionId); - session.setPrincipal(null); - session.setAuthType(null); - session.getSession().invalidate(); - if (sessions.keycloakSessionToHttpSession.size() == 0) { - userSessionMap.remove(sessions.user); + protected void logoutSession(Session session) { + try { + session.expire(); + } catch (Exception e) { + log.warnf("Session not present or already invalidated."); } } - public void sessionEvent(SessionEvent event) { // We only care about session destroyed events if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) @@ -133,28 +68,11 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi // Look up the single session id associated with this session (if any) Session session = event.getSession(); + log.debugf("Session %s destroyed", session.getId()); + GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); if (principal == null) return; session.setPrincipal(null); session.setAuthType(null); - - String username = principal.getUserPrincipal().getName(); - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - return; - } - String sessionid = session.getId(); - synchronized (this) { - String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid); - if (keycloakSessionId != null) { - userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - keycloakSessionMap.remove(keycloakSessionId); - } - userSessions.sessions.remove(sessionid); - if (userSessions.httpSessionToKeycloakSession.size() == 0) { - userSessionMap.remove(username); - } - - } } } diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagementWrapper.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagementWrapper.java new file mode 100644 index 0000000000..ff7ee07d5e --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaUserSessionManagementWrapper.java @@ -0,0 +1,30 @@ +package org.keycloak.adapters.as7; + +import java.util.List; + +import org.apache.catalina.Manager; +import org.keycloak.adapters.UserSessionManagement; + +/** + * @author Marek Posolda + */ +public class CatalinaUserSessionManagementWrapper implements UserSessionManagement { + + private final CatalinaUserSessionManagement delegate; + private final Manager sessionManager; + + public CatalinaUserSessionManagementWrapper(CatalinaUserSessionManagement delegate, Manager sessionManager) { + this.delegate = delegate; + this.sessionManager = sessionManager; + } + + @Override + public void logoutAll() { + delegate.logoutAll(sessionManager); + } + + @Override + public void logoutHttpSessions(List ids) { + delegate.logoutHttpSessions(sessionManager, ids); + } +} 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 eb3975630f..9bf2c42636 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 @@ -5,6 +5,7 @@ import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; import org.apache.catalina.Session; import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.connector.Request; @@ -127,7 +128,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif log.trace("invoke"); } CatalinaHttpFacade facade = new CatalinaHttpFacade(request, response); - PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade); + Manager sessionManager = request.getContext().getManager(); + CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(userSessionManagement, sessionManager); + PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, deploymentContext, facade); if (handler.handleRequest()) { return; } @@ -175,18 +178,22 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif if (session == null) return; // just in case session got serialized if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade)); - if (session.isActive()) return; + 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 - session.refreshExpiredToken(); - if (session.isActive()) return; + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) return; - request.getSessionInternal().removeNote(KeycloakSecurityContext.class.getName()); + // 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); - request.getSessionInternal().setPrincipal(null); - request.getSessionInternal().setAuthType(null); + catalinaSession.setPrincipal(null); + catalinaSession.setAuthType(null); + catalinaSession.expire(); } public void keycloakSaveRequest(Request request) throws IOException { diff --git a/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java index 32ebfdbf6d..d3ed5367cb 100644 --- a/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ b/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java @@ -227,7 +227,7 @@ public class KeycloakInstalled { } private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException { - AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri); + AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null); parseAccessToken(tokenResponse); } diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 0e414ecce5..2420c5308a 100755 --- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -41,7 +41,8 @@ public class ServletOAuthClient extends AbstractOAuthClient { } private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure { - return ServerRequest.invokeAccessCodeToToken(client, publicClient, code, getUrl(request, codeUrl, false), redirectUri, clientId, credentials); + // Don't send sessionId in oauth clients for now + return ServerRequest.invokeAccessCodeToToken(client, publicClient, code, getUrl(request, codeUrl, false), redirectUri, clientId, credentials, 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 6a05be844b..8516b1ad8b 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 @@ -16,8 +16,11 @@ 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; +import javax.servlet.http.HttpSession; + /** * @author Davide Ungari * @version $Revision: 1 $ @@ -40,7 +43,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { @Override protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(facade, deployment, sslRedirectPort) { + return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) { @Override protected void saveRequest() { try { @@ -63,16 +66,16 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { session.setAuthType("OAUTH"); session.setNote(KeycloakSecurityContext.class.getName(), securityContext); String username = securityContext.getToken().getSubject(); - log.finer("userSessionManage.login: " + username); - userSessionManagement.login(session, username, securityContext.getToken().getSessionState()); + log.finer("userSessionManagement.login: " + username); + userSessionManagement.login(session); } @Override protected void completeBearerAuthentication(KeycloakPrincipal principal) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); Set roles = getRolesFromToken(securityContext); - for (String role : roles) { - log.info("Bearer role: " + role); + if (log.isLoggable(Level.FINE)) { + log.fine("Completing bearer authentication. Bearer roles: " + roles); } Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles, securityContext); request.setUserPrincipal(generalPrincipal); @@ -123,4 +126,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } } } + + @Override + protected String getHttpSessionId(boolean create) { + HttpSession session = request.getSession(create); + return session != null ? session.getId() : null; + } } diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java index c21bbb8d09..66258ddf05 100755 --- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagement.java @@ -1,16 +1,13 @@ package org.keycloak.adapters.tomcat7; +import org.apache.catalina.Manager; import org.apache.catalina.Session; import org.apache.catalina.SessionEvent; import org.apache.catalina.SessionListener; import org.apache.catalina.realm.GenericPrincipal; -import org.keycloak.adapters.UserSessionManagement; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.io.IOException; +import java.util.List; import java.util.logging.Logger; /** @@ -19,105 +16,52 @@ import java.util.logging.Logger; * @author Davide Ungari * @version $Revision: 1 $ */ -public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement { +public class CatalinaUserSessionManagement implements SessionListener { + private static final Logger log = Logger.getLogger(""+CatalinaUserSessionManagement.class); - protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); - protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); - public static class UserSessions { - protected String user; - protected long loggedIn = System.currentTimeMillis(); - protected Map keycloakSessionToHttpSession = new HashMap(); - protected Map httpSessionToKeycloakSession = new HashMap(); - protected Map sessions = new HashMap(); - public long getLoggedIn() { - return loggedIn; - } - } - - public synchronized int getActiveSessions() { - return keycloakSessionMap.size(); - } - - /** - * - * @param username - * @return null if user not logged in - */ - @Override - public synchronized Long getUserLoginTime(String username) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) return null; - return sessions.getLoggedIn(); - } - - @Override - public synchronized Set getActiveUsers() { - HashSet set = new HashSet(); - set.addAll(userSessionMap.keySet()); - return set; - } - - - public synchronized void login(Session session, String username, String keycloakSessionId) { - String sessionId = session.getId(); - - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) { - sessions = new UserSessions(); - sessions.user = username; - userSessionMap.put(username, sessions); - } - keycloakSessionMap.put(keycloakSessionId, sessions); - sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); - sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); - sessions.sessions.put(sessionId, session); + public void login(Session session) { session.addSessionListener(this); } - @Override - public void logoutAll() { - for (String user : userSessionMap.keySet()) logoutUser(user); + public void logoutAll(Manager sessionManager) { + Session[] allSessions = sessionManager.findSessions(); + for (Session session : allSessions) { + logoutSession(session); + } } - @Override - public void logoutUser(String user) { - UserSessions sessions = null; - sessions = userSessionMap.remove(user); - if (sessions == null) { + public void logoutHttpSessions(Manager sessionManager, List sessionIds) { + log.fine("logoutHttpSessions: " + sessionIds); + + for (String sessionId : sessionIds) { + logoutSession(sessionManager, sessionId); + } + } + + protected void logoutSession(Manager manager, String httpSessionId) { + log.fine("logoutHttpSession: " + httpSessionId); + + Session session; + try { + session = manager.findSession(httpSessionId); + } catch (IOException ioe) { + log.warning("IO exception when looking for session " + httpSessionId); + ioe.printStackTrace(); return; } - for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { - String sessionId = entry.getKey(); - String keycloakSessionId = entry.getValue(); - Session session = sessions.sessions.get(sessionId); - session.setPrincipal(null); - session.setAuthType(null); - session.getSession().invalidate(); - keycloakSessionMap.remove(keycloakSessionId); - } + + logoutSession(session); } - public synchronized void logoutKeycloakSession(String keycloakSessionId) { - UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); - if (sessions == null) { - return; - } - String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - if (sessionId == null) { - - } - sessions.httpSessionToKeycloakSession.remove(sessionId); - Session session = sessions.sessions.remove(sessionId); - session.setPrincipal(null); - session.setAuthType(null); - session.getSession().invalidate(); - if (sessions.keycloakSessionToHttpSession.size() == 0) { - userSessionMap.remove(sessions.user); + protected void logoutSession(Session session) { + try { + session.expire(); + } catch (Exception e) { + log.warning("Session not present or already invalidated."); } } - public void sessionEvent(SessionEvent event) { // We only care about session destroyed events if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) @@ -126,28 +70,11 @@ public class CatalinaUserSessionManagement implements SessionListener, UserSessi // Look up the single session id associated with this session (if any) Session session = event.getSession(); + log.fine("Session " + session.getId() + " destroyed"); + GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); if (principal == null) return; session.setPrincipal(null); session.setAuthType(null); - - String username = principal.getUserPrincipal().getName(); - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - return; - } - String sessionid = session.getId(); - synchronized (this) { - String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionid); - if (keycloakSessionId != null) { - userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - keycloakSessionMap.remove(keycloakSessionId); - } - userSessions.sessions.remove(sessionid); - if (userSessions.httpSessionToKeycloakSession.size() == 0) { - userSessionMap.remove(username); - } - - } } } diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagementWrapper.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagementWrapper.java new file mode 100644 index 0000000000..b1e8828a40 --- /dev/null +++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaUserSessionManagementWrapper.java @@ -0,0 +1,30 @@ +package org.keycloak.adapters.tomcat7; + +import java.util.List; + +import org.apache.catalina.Manager; +import org.keycloak.adapters.UserSessionManagement; + +/** + * @author Marek Posolda + */ +public class CatalinaUserSessionManagementWrapper implements UserSessionManagement { + + private final CatalinaUserSessionManagement delegate; + private final Manager sessionManager; + + public CatalinaUserSessionManagementWrapper(CatalinaUserSessionManagement delegate, Manager sessionManager) { + this.delegate = delegate; + this.sessionManager = sessionManager; + } + + @Override + public void logoutAll() { + delegate.logoutAll(sessionManager); + } + + @Override + public void logoutHttpSessions(List ids) { + delegate.logoutHttpSessions(sessionManager, ids); + } +} 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 2fd8be480a..208882c447 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 @@ -5,6 +5,7 @@ import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; import org.apache.catalina.Session; import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.connector.Request; @@ -133,7 +134,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif public void invoke(Request request, Response response) throws IOException, ServletException { try { CatalinaHttpFacade facade = new CatalinaHttpFacade(request, response); - PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade); + Manager sessionManager = request.getContext().getManager(); + CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(userSessionManagement, sessionManager); + PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, deploymentContext, facade); if (handler.handleRequest()) { return; } @@ -177,18 +180,22 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif if (session == null) return; // just in case session got serialized if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade)); - if (session.isActive()) return; + 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 - session.refreshExpiredToken(); - if (session.isActive()) return; + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) return; - request.getSessionInternal().removeNote(KeycloakSecurityContext.class.getName()); + // 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); - request.getSessionInternal().setPrincipal(null); - request.getSessionInternal().setAuthType(null); + catalinaSession.setPrincipal(null); + catalinaSession.setAuthType(null); + catalinaSession.expire(); } public void keycloakSaveRequest(Request request) throws IOException { 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 58cbb023d5..472bc90410 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 @@ -92,14 +92,14 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA public boolean isActive() { // this object may have been serialized, so we need to reset realm config/metadata RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); - if (session.isActive()) { + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { log.debug("session is active"); return true; } - log.debug("session is not active try refresh"); - session.refreshExpiredToken(); - if (!session.isActive()) { + log.debug("session is not active or refresh is enforced. Try refresh"); + boolean success = session.refreshExpiredToken(false); + if (!success || !session.isActive()) { log.debug("session is not active return with failure"); return false; 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 c46f477d31..5416d3c29e 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,7 +18,9 @@ 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.HttpFacade; @@ -45,9 +47,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { @Override protected boolean isCached() { - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - HttpSession session = req.getSession(false); + HttpSession session = getSession(false); if (session == null) { log.debug("session was null, returning null"); return false; @@ -63,10 +63,12 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { 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; } - log.debug("Account was not active, returning null"); - session.setAttribute(KeycloakUndertowAccount.class.getName(), null); - return false; } @Override @@ -80,10 +82,9 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { @Override protected void login(KeycloakAccount account) { final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - HttpSession session = req.getSession(true); + HttpSession session = getSession(true); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session.getId(), account.getPrincipal().getName(), account.getKeycloakSecurityContext().getToken().getSessionState()); + userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager()); } @@ -91,4 +92,16 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator { protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal) { return new KeycloakUndertowAccount(principal); } + + @Override + protected String getHttpSessionId(boolean create) { + HttpSession session = getSession(create); + return session != null ? session.getId() : null; + } + + 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/SessionManagementBridge.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java index 0bdd434891..839d39e44a 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/SessionManagementBridge.java @@ -19,6 +19,7 @@ package org.keycloak.adapters.undertow; import io.undertow.server.session.SessionManager; import org.keycloak.adapters.UserSessionManagement; +import java.util.List; import java.util.Set; /** @@ -35,33 +36,13 @@ public class SessionManagementBridge implements UserSessionManagement { this.sessionManager = sessionManager; } - @Override - public int getActiveSessions() { - return userSessionManagement.getActiveSessions(); - } - - @Override - public Long getUserLoginTime(String username) { - return userSessionManagement.getUserLoginTime(username); - } - - @Override - public Set getActiveUsers() { - return userSessionManagement.getActiveUsers(); - } - @Override public void logoutAll() { userSessionManagement.logoutAll(sessionManager); } @Override - public void logoutUser(String user) { - userSessionManagement.logoutUser(sessionManager, user); - } - - @Override - public void logoutKeycloakSession(String id) { - userSessionManagement.logoutKeycloakSession(sessionManager, id); + public void logoutHttpSessions(List ids) { + userSessionManagement.logoutHttpSessions(sessionManager, ids); } } 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 79435aef2e..671f2e7c36 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 @@ -54,7 +54,7 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator @Override protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(facade, deployment, sslRedirectPort) { + return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) { @Override protected void saveRequest() { // todo @@ -73,9 +73,7 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator protected void login(KeycloakAccount account) { Session session = Sessions.getOrCreateSession(exchange); session.setAttribute(KeycloakUndertowAccount.class.getName(), account); - String username = account.getPrincipal().getName(); - String keycloakSessionId = account.getKeycloakSecurityContext().getToken().getSessionState(); - userSessionManagement.login(session.getSessionManager(), session.getId(), username, keycloakSessionId); + userSessionManagement.login(session.getSessionManager()); } @@ -90,24 +88,37 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator protected boolean isCached() { Session session = Sessions.getSession(exchange); if (session == null) { - log.info("session was null, returning null"); + log.debug("session was null, returning null"); return false; } KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName()); if (account == null) { - log.info("Account was not in session, returning null"); + log.debug("Account was not in session, returning null"); return false; } account.setDeployment(deployment); if (account.isActive()) { - log.info("Cached account found"); + 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) { + Session session = Sessions.getOrCreateSession(exchange); + return session.getId(); + } else { + Session session = Sessions.getSession(exchange); + return session != null ? session.getId() : null; } - log.info("Account was not active, returning false"); - session.removeAttribute(KeycloakUndertowAccount.class.getName()); - return false; } /** diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java index f39fe6a5bc..cc0a7c540a 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java @@ -27,6 +27,7 @@ import org.jboss.logging.Logger; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -40,106 +41,35 @@ import java.util.concurrent.ConcurrentHashMap; public class UndertowUserSessionManagement implements SessionListener { private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class); private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession"; - protected ConcurrentHashMap userSessionMap = new ConcurrentHashMap(); - protected ConcurrentHashMap keycloakSessionMap = new ConcurrentHashMap(); protected volatile boolean registered; - - public static class UserSessions { - protected String user; - protected long loggedIn = System.currentTimeMillis(); - protected Map keycloakSessionToHttpSession = new HashMap(); - protected Map httpSessionToKeycloakSession = new HashMap(); - public long getLoggedIn() { - return loggedIn; - } - } - - public synchronized int getActiveSessions() { - return keycloakSessionMap.size(); - } - - /** - * - * @param username - * @return null if user not logged in - */ - public synchronized Long getUserLoginTime(String username) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) return null; - return sessions.getLoggedIn(); - } - - public synchronized Set getActiveUsers() { - HashSet set = new HashSet(); - set.addAll(userSessionMap.keySet()); - return set; - } - - public synchronized void login(SessionManager manager, String sessionId, String username, String keycloakSessionId) { - UserSessions sessions = userSessionMap.get(username); - if (sessions == null) { - sessions = new UserSessions(); - sessions.user = username; - userSessionMap.put(username, sessions); - } - sessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); - sessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); - keycloakSessionMap.put(keycloakSessionId, sessions); + public void login(SessionManager manager) { if (!registered) { manager.registerSessionListener(this); registered = true; } } - public synchronized void logoutAll(SessionManager manager) { - for (String user : userSessionMap.keySet()) logoutUser(manager, user); + public void logoutAll(SessionManager manager) { + Set allSessions = manager.getAllSessions(); + for (String sessionId : allSessions) logoutSession(manager, sessionId); } - public synchronized void logoutUser(SessionManager manager, String user) { - log.debug("logoutUser: " + user); - UserSessions sessions = null; - sessions = userSessionMap.remove(user); - if (sessions == null) { - log.debug("no session for user: " + user); - return; - } - log.debug("found session for user"); - for (Map.Entry entry : sessions.httpSessionToKeycloakSession.entrySet()) { - log.debug("invalidating session for user: " + user); - String sessionId = entry.getKey(); - String keycloakSessionId = entry.getValue(); - Session session = getSessionById(manager, sessionId); - try { - session.invalidate(null); - } catch (Exception e) { - log.warn("Session already invalidated."); - } - keycloakSessionMap.remove(keycloakSessionId); + public void logoutHttpSessions(SessionManager manager, List sessionIds) { + log.debug("logoutHttpSessions: " + sessionIds); + + for (String sessionId : sessionIds) { + logoutSession(manager, sessionId); } } - public synchronized void logoutKeycloakSession(SessionManager manager, String keycloakSessionId) { - log.debug("logoutKeycloakSession: " + keycloakSessionId); - UserSessions sessions = keycloakSessionMap.remove(keycloakSessionId); - if (sessions == null) { - log.debug("no session for keycloak session id: " + keycloakSessionId); - return; - } - String sessionId = sessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - if (sessionId == null) { - log.debug("no session for keycloak session id: " + keycloakSessionId); - - } - sessions.httpSessionToKeycloakSession.remove(sessionId); - Session session = getSessionById(manager, sessionId); + protected void logoutSession(SessionManager manager, String httpSessionId) { + log.debug("logoutHttpSession: " + httpSessionId); + Session session = getSessionById(manager, httpSessionId); try { session.invalidate(null); } catch (Exception e) { - log.warn("Session already invalidated."); - } - if (sessions.keycloakSessionToHttpSession.size() == 0) { - userSessionMap.remove(sessions.user); + log.warnf("Session %s not present or already invalidated.", httpSessionId); } } @@ -188,23 +118,6 @@ public class UndertowUserSessionManagement implements SessionListener { // Look up the single session id associated with this session (if any) String username = getUsernameFromSession(session); log.debugf("Session destroyed for user: %s, sessionId: %s", username, session.getId()); - if (username == null) return; - String sessionId = session.getId(); - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - return; - } - synchronized (this) { - String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(sessionId); - if (keycloakSessionId != null) { - userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - keycloakSessionMap.remove(keycloakSessionId); - } - if (userSessions.httpSessionToKeycloakSession.size() == 0) { - userSessionMap.remove(username); - } - - } } protected String getUsernameFromSession(Session session) { @@ -217,23 +130,6 @@ public class UndertowUserSessionManagement implements SessionListener { @Override public void sessionIdChanged(Session session, String oldSessionId) { - String username = getUsernameFromSession(session); - if (username == null) return; - String sessionId = session.getId(); - - UserSessions userSessions = userSessionMap.get(username); - if (userSessions == null) { - return; - } - - synchronized (this) { - String keycloakSessionId = userSessions.httpSessionToKeycloakSession.remove(oldSessionId); - if (keycloakSessionId != null) { - userSessions.keycloakSessionToHttpSession.remove(keycloakSessionId); - userSessions.keycloakSessionToHttpSession.put(keycloakSessionId, sessionId); - userSessions.httpSessionToKeycloakSession.put(sessionId, keycloakSessionId); - } - } } @Override diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java index fe3bb2ded9..e2e9478337 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/MongoUserSessionProvider.java @@ -128,6 +128,7 @@ public class MongoUserSessionProvider implements UserSessionProvider { public List getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) { DBObject query = new QueryBuilder() .and("clientId").is(client.getId()) + .and("sessionId").notEquals(null) .get(); DBObject sort = new BasicDBObject("timestamp", 1).append("id", 1); @@ -142,7 +143,11 @@ public class MongoUserSessionProvider implements UserSessionProvider { @Override public int getActiveUserSessions(RealmModel realm, ClientModel client) { - return getUserSessions(realm, client).size(); + DBObject query = new QueryBuilder() + .and("clientId").is(client.getId()) + .and("sessionId").notEquals(null) + .get(); + return mongoStore.countEntities(MongoClientSessionEntity.class, query, invocationContext); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java index c67960c6ea..e520d8d004 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java @@ -142,7 +142,7 @@ public class OpenIDConnect implements LoginProtocol { ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); try { - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, null, userSession.getId(), executor, 0); + new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, app, clientSession, executor, 0); } finally { executor.getHttpClient().getConnectionManager().shutdown(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java index b88648e730..007925c028 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java @@ -14,6 +14,7 @@ import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -611,6 +612,17 @@ public class OpenIDConnectService { .build(); } + String httpSessionId = formData.getFirst(AdapterConstants.HTTP_SESSION_ID); + if (httpSessionId != null) { + String httpSessionHost = formData.getFirst(AdapterConstants.HTTP_SESSION_HOST); + logger.infof("Http Session '%s' saved in ClientSession for client '%s'. Host is '%s'", httpSessionId, client.getClientId(), httpSessionHost); + + event.detail(AdapterConstants.HTTP_SESSION_ID, httpSessionId); + clientSession.setNote(AdapterConstants.HTTP_SESSION_ID, httpSessionId); + event.detail(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); + clientSession.setNote(AdapterConstants.HTTP_SESSION_HOST, httpSessionHost); + } + AccessToken token = tokenManager.createClientAccessToken(accessCode.getRequestedRoles(), realm, client, user, userSession); try { diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 105751fa13..2a4576f99e 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -17,22 +17,23 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; -import org.keycloak.representations.adapters.action.SessionStats; -import org.keycloak.representations.adapters.action.SessionStatsAction; import org.keycloak.representations.adapters.action.UserStats; -import org.keycloak.representations.adapters.action.UserStatsAction; import org.keycloak.services.util.HttpClientBuilder; import org.keycloak.services.util.ResolveRelative; +import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.Time; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeMap; /** * @author Bill Burke @@ -40,17 +41,7 @@ import java.util.Map; */ public class ResourceAdminManager { protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); - - public SessionStats getSessionStats(URI requestUri, KeycloakSession session, RealmModel realm, ApplicationModel application, boolean users) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - return getSessionStats(requestUri, session, realm, application, users, executor); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } - - } + private static final String KC_SESSION_HOST = "${kc_session_host}"; public static ApacheHttpClient4Executor createExecutor() { HttpClient client = new HttpClientBuilder() @@ -59,49 +50,6 @@ public class ResourceAdminManager { return new ApacheHttpClient4Executor(client); } - public SessionStats getSessionStats(URI requestUri, KeycloakSession session, RealmModel realm, ApplicationModel application, boolean users, ApacheHttpClient4Executor client) { - String managementUrl = getManagementUrl(requestUri, application); - if (managementUrl != null) { - SessionStatsAction adminAction = new SessionStatsAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, application.getName()); - adminAction.setListUsers(users); - String token = new TokenManager().encodeToken(realm, adminAction); - logger.debugv("session stats for application: {0} url: {1}", application.getName(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_GET_SESSION_STATS).build().toString()); - ClientResponse response = null; - try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(SessionStats.class); - } catch (Exception e) { - throw new RuntimeException(e); - } - try { - if (response.getStatus() != 200) { - logger.warn("Failed to get stats: " + response.getStatus()); - return null; - } - SessionStats stats = response.getEntity(); - - // replace with username - if (users && stats.getUsers() != null) { - Map newUsers = new HashMap(); - for (Map.Entry entry : stats.getUsers().entrySet()) { - UserModel user = session.users().getUserById(entry.getKey(), realm); - if (user == null) continue; - newUsers.put(user.getUsername(), entry.getValue()); - - } - stats.setUsers(newUsers); - } - return stats; - } finally { - response.releaseConnection(); - } - } else { - logger.debug("no management url."); - return null; - } - - } - public static String getManagementUrl(URI requestUri, ApplicationModel application) { String mgmtUrl = application.getManagementUrl(); if (mgmtUrl == null || mgmtUrl.equals("")) { @@ -111,95 +59,56 @@ public class ResourceAdminManager { // this is to support relative admin urls when keycloak and applications are deployed on the same machine String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, mgmtUrl); - // this is for resolving URI like "http://${jboss.home.name}:8080/..." in order to send request to same machine and avoid LB in cluster env + // this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine and avoid request to LB in cluster environment return StringPropertyReplacer.replaceProperties(absoluteURI); } - public UserStats getUserStats(URI requestUri, RealmModel realm, ApplicationModel application, UserModel user) { + public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) { + List userSessions = keycloakSession.sessions().getUserSessions(realm, user); + logoutUserSessions(requestUri, realm, userSessions); + } + + protected void logoutUserSessions(URI requestUri, RealmModel realm, List userSessions) { ApacheHttpClient4Executor executor = createExecutor(); try { - return getUserStats(requestUri, realm, application, user, executor); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } - - } - - - public UserStats getUserStats(URI requestUri, RealmModel realm, ApplicationModel application, UserModel user, ApacheHttpClient4Executor client) { - String managementUrl = getManagementUrl(requestUri, application); - if (managementUrl != null) { - UserStatsAction adminAction = new UserStatsAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, application.getName(), user.getId()); - String token = new TokenManager().encodeToken(realm, adminAction); - logger.debugv("session stats for application: {0} url: {1}", application.getName(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_GET_USER_STATS).build().toString()); - ClientResponse response = null; - try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class); - } catch (Exception e) { - throw new RuntimeException(e); + // Map from "app" to clientSessions for this app + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + for (UserSessionModel userSession : userSessions) { + putClientSessions(clientSessions, userSession); } - try { - if (response.getStatus() != 200) { - logger.warn("Failed to get stats: " + response.getStatus()); - return null; - } - UserStats stats = response.getEntity(); - return stats; - } finally { - response.releaseConnection(); - } - } else { - logger.debug("no management url."); - return null; - } + logger.debugv("logging out {0} resources ", clientSessions.size()); + logger.infov("logging out resources: " + clientSessions); - } - - public void logoutUser(URI requestUri, RealmModel realm, String user, UserSessionModel session) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - List resources; - if (session != null) { - resources = new LinkedList(); - - for (ClientSessionModel clientSession : session.getClientSessions()) { - ClientModel client = clientSession.getClient(); - if (client instanceof ApplicationModel) { - resources.add((ApplicationModel) client); - } - } - } else { - resources = realm.getApplications(); - } - - logger.debugv("logging out {0} resources ", resources.size()); - for (ApplicationModel resource : resources) { - logoutApplication(requestUri, realm, resource, user, session != null ? session.getId() : null, executor, 0); + for (Map.Entry> entry : clientSessions.entrySet()) { + logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); } } finally { executor.getHttpClient().getConnectionManager().shutdown(); } } + private void putClientSessions(MultivaluedHashMap clientSessions, UserSessionModel userSession) { + for (ClientSessionModel clientSession : userSession.getClientSessions()) { + ClientModel client = clientSession.getClient(); + if (client instanceof ApplicationModel) { + clientSessions.add((ApplicationModel)client, clientSession); + } + } + } + public void logoutSession(URI requestUri, RealmModel realm, UserSessionModel session) { ApacheHttpClient4Executor executor = createExecutor(); try { - List resources = new LinkedList(); - for (ClientSessionModel clientSession : session.getClientSessions()) { - ClientModel client = clientSession.getClient(); - if (client instanceof ApplicationModel) { - resources.add((ApplicationModel) client); - } - } + // Map from "app" to clientSessions for this app + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + putClientSessions(clientSessions, session); - logger.debugv("logging out {0} resources ", resources.size()); - for (ApplicationModel resource : resources) { - logoutApplication(requestUri, realm, resource, null, session.getId(), executor, 0); + logger.debugv("logging out {0} resources ", clientSessions.size()); + for (Map.Entry> entry : clientSessions.entrySet()) { + logoutApplication(requestUri, realm, entry.getKey(), entry.getValue(), executor, 0); } } finally { executor.getHttpClient().getConnectionManager().shutdown(); @@ -214,46 +123,77 @@ public class ResourceAdminManager { List resources = realm.getApplications(); logger.debugv("logging out {0} resources ", resources.size()); for (ApplicationModel resource : resources) { - logoutApplication(requestUri, realm, resource, null, null, executor, realm.getNotBefore()); + logoutApplication(requestUri, realm, resource, (List)null, executor, realm.getNotBefore()); } } finally { executor.getHttpClient().getConnectionManager().shutdown(); } } - public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session) { + public void logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List userSessions) { ApacheHttpClient4Executor executor = createExecutor(); try { resource.setNotBefore(Time.currentTime()); - logoutApplication(requestUri, realm, resource, user, session, executor, resource.getNotBefore()); + + List ourAppClientSessions = null; + if (userSessions != null) { + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + for (UserSessionModel userSession : userSessions) { + putClientSessions(clientSessions, userSession); + } + ourAppClientSessions = clientSessions.get(resource); + } + + logoutApplication(requestUri, realm, resource, ourAppClientSessions, executor, resource.getNotBefore()); } finally { executor.getHttpClient().getConnectionManager().shutdown(); } } + public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client, int notBefore) { + return logoutApplication(requestUri, realm, resource, Arrays.asList(clientSession), client, notBefore); + } - public boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, String user, String session, ApacheHttpClient4Executor client, int notBefore) { + protected boolean logoutApplication(URI requestUri, RealmModel realm, ApplicationModel resource, List clientSessions, ApacheHttpClient4Executor client, int notBefore) { String managementUrl = getManagementUrl(requestUri, resource); if (managementUrl != null) { - LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, session, notBefore); - String token = new TokenManager().encodeToken(realm, adminAction); - logger.debugv("logout user: {0} resource: {1} url: {2}", user, resource.getName(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString()); - ClientResponse response; - try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class); - } catch (Exception e) { - logger.warn("Logout for application '" + resource.getName() + "' failed", e); - return false; + + // Key is host, value is list of http sessions for this host + MultivaluedHashMap adapterSessionIds = null; + if (clientSessions != null && clientSessions.size() > 0) { + adapterSessionIds = new MultivaluedHashMap(); + for (ClientSessionModel clientSession : clientSessions) { + String adapterSessionId = clientSession.getNote(AdapterConstants.HTTP_SESSION_ID); + if (adapterSessionId != null) { + String host = clientSession.getNote(AdapterConstants.HTTP_SESSION_HOST); + adapterSessionIds.add(host, adapterSessionId); + } + } } - try { - boolean success = response.getStatus() == 204; - logger.debug("logout success."); - return success; - } finally { - response.releaseConnection(); + + if (managementUrl.contains(KC_SESSION_HOST) && adapterSessionIds != null) { + boolean allPassed = true; + // Send logout separately to each host (needed for single-sign-out in cluster for non-distributable apps - KEYCLOAK-748) + for (Map.Entry> entry : adapterSessionIds.entrySet()) { + String host = entry.getKey(); + List sessionIds = entry.getValue(); + String currentHostMgmtUrl = managementUrl.replace(KC_SESSION_HOST, host); + allPassed = logoutApplicationOnHost(realm, resource, sessionIds, client, notBefore, currentHostMgmtUrl) && allPassed; + } + + return allPassed; + } else { + // Send single logout request + List allSessionIds = null; + if (adapterSessionIds != null) { + allSessionIds = new ArrayList(); + for (List currentIds : adapterSessionIds.values()) { + allSessionIds.addAll(currentIds); + } + } + return logoutApplicationOnHost(realm, resource, allSessionIds, client, notBefore, managementUrl); } } else { logger.debugv("Can't logout {0}: no management url", resource.getName()); @@ -261,6 +201,27 @@ public class ResourceAdminManager { } } + protected boolean logoutApplicationOnHost(RealmModel realm, ApplicationModel resource, List adapterSessionIds, ApacheHttpClient4Executor client, int notBefore, String managementUrl) { + LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), adapterSessionIds, notBefore); + String token = new TokenManager().encodeToken(realm, adminAction); + logger.infov("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getName(), managementUrl); + ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString()); + ClientResponse response; + try { + response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class); + } catch (Exception e) { + logger.warn("Logout for application '" + resource.getName() + "' failed", e); + return false; + } + try { + boolean success = response.getStatus() == 204; + logger.debug("logout success."); + return success; + } finally { + response.releaseConnection(); + } + } + public void pushRealmRevocationPolicy(URI requestUri, RealmModel realm) { ApacheHttpClient4Executor executor = createExecutor(); @@ -290,9 +251,9 @@ public class ResourceAdminManager { if (managementUrl != null) { PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore); String token = new TokenManager().encodeToken(realm, adminAction); - logger.debugv("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl); + logger.infov("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl); ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString()); - ClientResponse response = null; + ClientResponse response; try { response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); } catch (Exception e) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java index 237bf448b6..e8041614ef 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ApplicationResource.java @@ -13,8 +13,6 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; -import org.keycloak.representations.adapters.action.SessionStats; -import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; @@ -27,7 +25,6 @@ import org.keycloak.util.JsonSerialization; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; -import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -285,35 +282,6 @@ public class ApplicationResource { new ResourceAdminManager().pushApplicationRevocationPolicy(uriInfo.getRequestUri(), realm, application); } - /** - * If the application has an admin URL, query it directly for session stats. - * - * @param users whether to include users logged in. - * @return - */ - @Path("session-stats") - @GET - @NoCache - @Produces(MediaType.APPLICATION_JSON) - public SessionStats getSessionStats(@QueryParam("users") @DefaultValue("false") boolean users) { - logger.info("session-stats"); - auth.requireView(); - if (application.getManagementUrl() == null || application.getManagementUrl().trim().equals("")) { - logger.info("sending empty stats"); - SessionStats stats = new SessionStats(); - if (users) stats.setUsers(new HashMap()); - return stats; - } - SessionStats stats = new ResourceAdminManager().getSessionStats(uriInfo.getRequestUri(), session, realm, application, users); - if (stats == null) { - logger.info("app returned null stats"); - } else { - logger.info("activeUsers: " + stats.getActiveUsers()); - logger.info("activeSessions: " + stats.getActiveSessions()); - } - return stats; - } - /** * Number of user sessions associated with this application * @@ -363,7 +331,7 @@ public class ApplicationResource { @POST public void logoutAll() { auth.requireManage(); - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null, null); + new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, null); } /** @@ -378,7 +346,9 @@ public class ApplicationResource { if (user == null) { throw new NotFoundException("User not found"); } - new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, user.getId(), null); + + List userSessions = session.sessions().getUserSessions(realm, user); + new ResourceAdminManager().logoutApplication(uriInfo.getRequestUri(), realm, application, userSessions); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 5cd0ac591d..9c6047ac9f 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -19,7 +19,6 @@ import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.LDAPConnectionTestManager; @@ -323,28 +322,6 @@ public class RealmAdminResource { return data; } - /** - * Any application that has an admin URL will be asked directly how many sessions they have active and what users - * are involved with those sessions. - * - * @return - */ - @Path("session-stats") - @GET - @NoCache - @Produces(MediaType.APPLICATION_JSON) - public Map getSessionStats() { - logger.info("session-stats"); - auth.requireView(); - Map stats = new HashMap(); - for (ApplicationModel applicationModel : realm.getApplications()) { - if (applicationModel.getManagementUrl() == null) continue; - SessionStats appStats = new ResourceAdminManager().getSessionStats(uriInfo.getRequestUri(), this.session, realm, applicationModel, false); - stats.put(applicationModel.getName(), appStats); - } - return stats; - } - /** * View the events provider and how it is configured. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index eea2808d54..5eff07bf63 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -24,7 +24,6 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.OpenIDConnect; import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.MappingsRepresentation; @@ -217,36 +216,6 @@ public class UsersResource { return ModelToRepresentation.toRepresentation(user); } - /** - * For each application with an admin URL, query them for the set of users logged in. This not as reliable - * as getSessions(). - * - * @See getSessions - * - * @param username - * @return - */ - @Path("{username}/session-stats") - @GET - @NoCache - @Produces(MediaType.APPLICATION_JSON) - public Map getSessionStats(final @PathParam("username") String username) { - logger.info("session-stats"); - auth.requireView(); - UserModel user = session.users().getUserByUsername(username, realm); - if (user == null) { - throw new NotFoundException("User not found"); - } - Map stats = new HashMap(); - for (ApplicationModel applicationModel : realm.getApplications()) { - if (applicationModel.getManagementUrl() == null) continue; - UserStats appStats = new ResourceAdminManager().getUserStats(uriInfo.getRequestUri(), realm, applicationModel, user); - if (appStats == null) continue; - if (appStats.isLoggedIn()) stats.put(applicationModel.getName(), appStats); - } - return stats; - } - /** * List set of sessions associated with this user. * @@ -258,7 +227,6 @@ public class UsersResource { @NoCache @Produces(MediaType.APPLICATION_JSON) public List getSessions(final @PathParam("username") String username) { - logger.info("sessions"); auth.requireView(); UserModel user = session.users().getUserByUsername(username, realm); if (user == null) { @@ -345,8 +313,8 @@ public class UsersResource { if (user == null) { throw new NotFoundException("User not found"); } + new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user, session); session.sessions().removeUserSessions(realm, user); - new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), null); } /** diff --git a/testsuite/docker-cluster/shared-files/deploy-examples.sh b/testsuite/docker-cluster/shared-files/deploy-examples.sh index 10f9643798..f71f697a9a 100644 --- a/testsuite/docker-cluster/shared-files/deploy-examples.sh +++ b/testsuite/docker-cluster/shared-files/deploy-examples.sh @@ -23,15 +23,20 @@ done; # Configure admin-access.war sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml +# Enforce refreshing token for product-portal and customer-portal war +# sed -i -e 's/\"\/auth\",/&\n \"always-refresh-token\": true,/' customer-portal.war/WEB-INF/keycloak.json; +# sed -i -e 's/\"\/auth\",/&\n \"always-refresh-token\": true,/' product-portal.war/WEB-INF/keycloak.json; + # Configure other examples for I in *.war/WEB-INF/keycloak.json; do - sed -i -e 's/\"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I; + sed -i -e 's/\"auth-server-url\".*: \"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I; done; # Enable distributable for customer-portal sed -i -e 's/<\/module-name>/&\n /' customer-portal.war/WEB-INF/web.xml # Configure testrealm.json - Enable adminUrl to access adapters on local machine -sed -i -e 's/\"adminUrl\": \"/&http:\/\/\$\{jboss.host.name\}:8080/' /keycloak-docker-cluster/examples/testrealm.json +sed -i -e 's/\"adminUrl\": \"\/customer-portal/\"adminUrl\": \"http:\/\/\$\{jboss.host.name\}:8080\/customer-portal/' /keycloak-docker-cluster/examples/testrealm.json +sed -i -e 's/\"adminUrl\": \"\/product-portal/\"adminUrl\": \"http:\/\/\$\{kc_session_host\}:8080\/product-portal/' /keycloak-docker-cluster/examples/testrealm.json diff --git a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh index 55edc8267f..7d350aa762 100644 --- a/testsuite/docker-cluster/shared-files/keycloak-run-node.sh +++ b/testsuite/docker-cluster/shared-files/keycloak-run-node.sh @@ -24,6 +24,8 @@ function prepareHost # Enable Infinispan provider sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json + sed -i "s|keycloak.realm.cache.provider:mem|keycloak.realm.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json + sed -i "s|keycloak.user.cache.provider:mem|keycloak.user.cache.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json # Deploy and configure examples /keycloak-docker-cluster/shared-files/deploy-examples.sh 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 28bd4b61b1..5443a7e742 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 @@ -23,10 +23,8 @@ package org.keycloak.testsuite.adapter; import org.junit.Assert; import org.junit.ClassRule; -import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; -import org.junit.runners.MethodSorters; import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.Version; @@ -34,13 +32,13 @@ 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; import org.keycloak.protocol.oidc.OpenIDConnectService; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; -import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; @@ -48,6 +46,7 @@ import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testutils.KeycloakServer; @@ -67,6 +66,7 @@ import java.net.URI; import java.net.URL; import java.security.PublicKey; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; /** * Tests Undertow Adapter @@ -158,17 +158,16 @@ public class AdapterTest { Client client = ClientBuilder.newClient(); UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo"); - Map stats = adminTarget.path("session-stats").request() + Map stats = adminTarget.path("application-session-stats").request() .header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken) - .get(new GenericType>() { + .get(new GenericType>() { }); - - SessionStats custStats = stats.get("customer-portal"); - Assert.assertNotNull(custStats); - Assert.assertEquals(1, custStats.getActiveSessions()); - SessionStats prodStats = stats.get("product-portal"); - Assert.assertNotNull(prodStats); - Assert.assertEquals(1, prodStats.getActiveSessions()); + Integer custSessionsCount = stats.get("customer-portal"); + Assert.assertNotNull(custSessionsCount); + Assert.assertTrue(1 == custSessionsCount); + Integer prodStatsCount = stats.get("product-portal"); + Assert.assertNotNull(prodStatsCount); + Assert.assertTrue(1 == prodStatsCount); client.close(); @@ -299,7 +298,7 @@ public class AdapterTest { realm = session.realms().getRealmByName("demo"); // need to cleanup so other tests don't fail, so invalidate http sessions on remote clients. UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm); - new ResourceAdminManager().logoutUser(null, realm, user.getId(), null); + new ResourceAdminManager().logoutUser(null, realm, user, session); realm.setSsoSessionIdleTimeout(originalIdle); session.getTransaction().commit(); session.close(); @@ -426,6 +425,11 @@ public class AdapterTest { Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); } + /** + * KEYCLOAK-732 + * + * @throws Throwable + */ @Test public void testSingleSessionInvalidated() throws Throwable { AdapterTest browser1 = this; @@ -461,6 +465,57 @@ public class AdapterTest { } } + /** + * KEYCLOAK-741 + */ + @Test + public void testSessionInvalidatedAfterFailedRefresh() throws Throwable { + final AtomicInteger origTokenLifespan = new AtomicInteger(); + + // Delete adminUrl and set short accessTokenLifespan + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel demoRealm) { + ApplicationModel sessionPortal = demoRealm.getApplicationByName("session-portal"); + sessionPortal.setManagementUrl(null); + + origTokenLifespan.set(demoRealm.getAccessTokenLifespan()); + demoRealm.setAccessTokenLifespan(1); + } + }, "demo"); + + // Login + loginAndCheckSession(driver, loginPage); + + // Logout + String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth")) + .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/session-portal").build("demo").toString(); + driver.navigate().to(logoutUri); + + // Wait until accessToken is expired + Thread.sleep(2000); + + // Assert that http session was invalidated + driver.navigate().to("http://localhost:8081/session-portal"); + Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + loginPage.login("bburke@redhat.com", "password"); + Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/session-portal"); + String pageSource = driver.getPageSource(); + Assert.assertTrue(pageSource.contains("Counter=1")); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel demoRealm) { + ApplicationModel sessionPortal = demoRealm.getApplicationByName("session-portal"); + sessionPortal.setManagementUrl("http://localhost:8081/session-portal"); + + demoRealm.setAccessTokenLifespan(origTokenLifespan.get()); + } + + }, "demo"); + } + private static void loginAndCheckSession(WebDriver driver, LoginPage loginPage) { driver.navigate().to("http://localhost:8081/session-portal"); Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java index b0fd0951dc..7b616f332d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java @@ -83,6 +83,25 @@ public abstract class AbstractKeycloakRule extends ExternalResource { } } + public void update(KeycloakRule.KeycloakSetup configurer, String realmId) { + KeycloakSession session = server.getSessionFactory().create(); + session.getTransaction().begin(); + + try { + RealmManager manager = new RealmManager(session); + + RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm()); + RealmModel appRealm = manager.getRealm(realmId); + + configurer.session = session; + configurer.config(manager, adminstrationRealm, appRealm); + + session.getTransaction().commit(); + } finally { + session.close(); + } + } + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java index 481b1d82a3..74df6c814f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java @@ -81,22 +81,7 @@ public class KeycloakRule extends AbstractKeycloakRule { } public void update(KeycloakSetup configurer) { - KeycloakSession session = server.getSessionFactory().create(); - session.getTransaction().begin(); - - try { - RealmManager manager = new RealmManager(session); - - RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm()); - RealmModel appRealm = manager.getRealm("test"); - - configurer.session = session; - configurer.config(manager, adminstrationRealm, appRealm); - - session.getTransaction().commit(); - } finally { - session.close(); - } + update(configurer, "test"); } diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json index abc8e4086c..fc6ebaedbd 100755 --- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json +++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json @@ -1,4 +1,5 @@ { + "id": "demo", "realm": "demo", "enabled": true, "accessTokenLifespan": 3000,