From 5563118d79912f19263118dcc9e06c598044595a Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 14 Oct 2015 11:49:36 -0400 Subject: [PATCH 01/13] KEYCLOAK-1908 --- .../resources/theme/base/login/login-totp.ftl | 1 + .../AuthenticationFlowContext.java | 6 + .../AuthenticationProcessor.java | 5 + .../DefaultAuthenticationFlow.java | 108 +++++++++--------- .../keycloak/authentication/FlowStatus.java | 9 +- .../browser/OTPFormAuthenticator.java | 4 + .../testsuite/forms/LoginTotpTest.java | 10 ++ .../testsuite/pages/LoginTotpPage.java | 7 ++ 8 files changed, 98 insertions(+), 52 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl index c5d28a1691..e472fff1ff 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl @@ -25,6 +25,7 @@
+
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 9b02eb89d0..681e76c484 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -71,6 +71,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon */ void cancelLogin(); + /** + * Reset the current flow to the beginning and restarts it. + * + */ + void resetFlow(); + /** * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 409b0e7311..b6b3272ca6 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -454,6 +454,11 @@ public class AuthenticationProcessor { forceChallenge(response); } + @Override + public void resetFlow() { + this.status = FlowStatus.FLOW_RESET; + } + @Override public void fork() { this.status = FlowStatus.FORK; diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index cf825c0a07..d9ca0b094f 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -4,6 +4,9 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; +import org.omg.PortableInterceptor.SUCCESSFUL; + +import static org.keycloak.authentication.FlowStatus.*; import javax.ws.rs.core.Response; import java.util.Iterator; @@ -153,62 +156,65 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { public Response processResult(AuthenticationProcessor.Result result) { AuthenticationExecutionModel execution = result.getExecution(); FlowStatus status = result.getStatus(); - if (status == FlowStatus.SUCCESS) { - AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); - if (execution.isAlternative()) alternativeSuccessful = true; - return null; - } else if (status == FlowStatus.FAILED) { - AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); - processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); - if (result.getChallenge() != null) { - return sendChallenge(result, execution); - } - throw new AuthenticationFlowException(result.getError()); - } else if (status == FlowStatus.FORK) { - AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); - processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); - throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); - } else if (status == FlowStatus.FORCE_CHALLENGE) { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); - return sendChallenge(result, execution); - } else if (status == FlowStatus.CHALLENGE) { - AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); - if (execution.isRequired()) { + switch (status) { + case SUCCESS: + AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + if (execution.isAlternative()) alternativeSuccessful = true; + return null; + case FAILED: + AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); + processor.logFailure(); + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); + if (result.getChallenge() != null) { + return sendChallenge(result, execution); + } + throw new AuthenticationFlowException(result.getError()); + case FORK: + AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); + processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); + throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); + case FORCE_CHALLENGE: processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); - } - UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); - if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { + case CHALLENGE: + AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); + if (execution.isRequired()) { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + return sendChallenge(result, execution); + } + UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); + if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + return sendChallenge(result, execution); + } + if (execution.isAlternative()) { + alternativeChallenge = result.getChallenge(); + challengedAlternativeExecution = execution; + } else { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + } + return null; + case FAILURE_CHALLENGE: + AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); + processor.logFailure(); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); - } - if (execution.isAlternative()) { - alternativeChallenge = result.getChallenge(); - challengedAlternativeExecution = execution; - } else { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); - } - return null; - } else if (status == FlowStatus.FAILURE_CHALLENGE) { - AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); - processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); - return sendChallenge(result, execution); - } else if (status == FlowStatus.ATTEMPTED) { - AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); - if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { - throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); - } - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED); - return null; - } else { - AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); - AuthenticationProcessor.logger.error("Unknown result status"); - throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); + case ATTEMPTED: + AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); + if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { + throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); + } + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED); + return null; + case FLOW_RESET: + AuthenticationProcessor.resetFlow(processor.getClientSession()); + return processor.authenticate(); + default: + AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); + AuthenticationProcessor.logger.error("Unknown result status"); + throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); } - } public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) { diff --git a/services/src/main/java/org/keycloak/authentication/FlowStatus.java b/services/src/main/java/org/keycloak/authentication/FlowStatus.java index 6e6ecbd517..c8acbc7465 100755 --- a/services/src/main/java/org/keycloak/authentication/FlowStatus.java +++ b/services/src/main/java/org/keycloak/authentication/FlowStatus.java @@ -48,6 +48,13 @@ public enum FlowStatus { * This flow is being forked. The current client session is being cloned, reset, and redirected to browser login. * */ - FORK + FORK, + + /** + * This flow was reset to the beginning. An example is hitting cancel on the OTP page which will bring you back to the + * username password page. + * + */ + FLOW_RESET } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java index e3427e0a48..4a0c48dc48 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java @@ -37,6 +37,10 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl public void validateOTP(AuthenticationFlowContext context) { MultivaluedMap inputData = context.getHttpRequest().getDecodedFormParameters(); + if (inputData.containsKey("cancel")) { + context.resetFlow(); + return; + } List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.TOTP); if (password == null) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index b37401ca94..6eda3fa30d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -153,6 +153,16 @@ public class LoginTotpTest { events.expectLogin().assertEvent(); } + @Test + public void loginWithTotpCancel() throws Exception { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + loginTotpPage.assertCurrent(); + loginTotpPage.cancel(); + loginPage.assertCurrent(); + } + @Test public void loginWithTotpInvalidPassword() throws Exception { loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java index b725a169b9..232b102663 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java @@ -39,6 +39,9 @@ public class LoginTotpPage extends AbstractPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; + @FindBy(id = "kc-cancel") + private WebElement cancelButton; + @FindBy(className = "feedback-error") private WebElement loginErrorMessage; @@ -49,6 +52,10 @@ public class LoginTotpPage extends AbstractPage { submitButton.click(); } + public void cancel() { + cancelButton.click(); + } + public String getError() { return loginErrorMessage != null ? loginErrorMessage.getText() : null; } From f8effaee58c2b4a143b91cb4e6822cf91118a0df Mon Sep 17 00:00:00 2001 From: Michael Gerber Date: Wed, 14 Oct 2015 13:46:59 +0200 Subject: [PATCH 02/13] return null instead of an empty set. --- .../idm/RealmRepresentation.java | 10 ++++--- .../models/utils/ModelToRepresentation.java | 5 +++- .../keycloak/testsuite/admin/RealmTest.java | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index c1c53b32f0..a12f6cd24a 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -611,12 +611,16 @@ public class RealmRepresentation { } public Set getSupportedLocales() { - if(supportedLocales == null){ - supportedLocales = new HashSet(); - } return supportedLocales; } + public void addSupportedLocales(String locale) { + if(supportedLocales == null){ + supportedLocales = new HashSet<>(); + } + supportedLocales.add(locale); + } + public void setSupportedLocales(Set supportedLocales) { this.supportedLocales = supportedLocales; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 96b47cb1a4..01daddd736 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -210,7 +210,10 @@ public class ModelToRepresentation { } rep.setInternationalizationEnabled(realm.isInternationalizationEnabled()); - rep.getSupportedLocales().addAll(realm.getSupportedLocales()); + if(realm.getSupportedLocales() != null){ + rep.setSupportedLocales(new HashSet()); + rep.getSupportedLocales().addAll(realm.getSupportedLocales()); + } rep.setDefaultLocale(realm.getDefaultLocale()); if (internal) { exportAuthenticationFlows(realm, rep); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 8c41dfa2ef..bb3351542e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -15,6 +15,8 @@ import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.HashSet; +import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -102,6 +104,31 @@ public class RealmTest extends AbstractClientTest { assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed()); } + @Test + public void updateRealmWithNewRepresentation() { + // first change + RealmRepresentation rep = new RealmRepresentation(); + rep.setEditUsernameAllowed(true); + rep.setSupportedLocales(new HashSet<>(Arrays.asList("en", "de"))); + + realm.update(rep); + + rep = realm.toRepresentation(); + + assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed()); + assertEquals(2, rep.getSupportedLocales().size()); + + // second change + rep = new RealmRepresentation(); + rep.setEditUsernameAllowed(false); + + realm.update(rep); + + rep = realm.toRepresentation(); + assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed()); + assertEquals(2, rep.getSupportedLocales().size()); + } + @Test public void getRealmRepresentation() { RealmRepresentation rep = realm.toRepresentation(); From 11a8d3842e41524bf9c455585a80ffd85ed89132 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Thu, 15 Oct 2015 00:44:33 +0200 Subject: [PATCH 03/13] KEYCLOAK-1849 Overlay installation scripts not working --- .../configuration/keycloak-server.json | 2 +- .../WEB-INF/jboss-deployment-structure.xml | 1 + .../main/server-war/WEB-INF/web.xml | 6 +++++ .../WEB-INF/jboss-deployment-structure.xml | 1 + .../main/server-war/WEB-INF/web.xml | 6 +++++ .../eap6/eap6-server-overlay/assembly.xml | 4 +-- ...prepare-ha.cli => keycloak-install-ha.cli} | 5 +++- .../cli/keycloak-install.cli | 10 ++++++- .../cli/keycloak-prepare.cli | 7 ----- .../wf9-server-overlay/assembly.xml | 4 +-- ...prepare-ha.cli => keycloak-install-ha.cli} | 5 +++- .../cli/keycloak-install.cli | 6 +++++ .../cli/keycloak-prepare.cli | 6 ----- .../en/en-US/modules/server-installation.xml | 27 ++++++++++--------- 14 files changed, 56 insertions(+), 34 deletions(-) rename distribution/server-overlay/eap6/eap6-server-overlay/cli/{keycloak-prepare-ha.cli => keycloak-install-ha.cli} (79%) delete mode 100644 distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli rename distribution/server-overlay/wf9-server-overlay/cli/{keycloak-prepare-ha.cli => keycloak-install-ha.cli} (77%) delete mode 100644 distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json index 669cf41db5..3e4315c136 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json @@ -59,7 +59,7 @@ "connectionsInfinispan": { "default" : { - "cacheContainer" : "java:jboss/infinispan/Keycloak" + "cacheContainer" : "java:comp/env/infinispan/Keycloak" } } } \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 11f8141941..3540faaae6 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -54,6 +54,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml index 164f6beaff..f59d1d347d 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml @@ -39,4 +39,10 @@ Keycloak REST Interface /* + + + infinispan/Keycloak + org.infinispan.manager.EmbeddedCacheManager + java:jboss/infinispan/Keycloak + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 11f8141941..3540faaae6 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -54,6 +54,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/web.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/web.xml index 164f6beaff..f59d1d347d 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/web.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/web.xml @@ -39,4 +39,10 @@ Keycloak REST Interface /* + + + infinispan/Keycloak + org.infinispan.manager.EmbeddedCacheManager + java:jboss/infinispan/Keycloak + diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml b/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml index 580b1d7ef7..b3324df8b4 100755 --- a/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml +++ b/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml @@ -20,13 +20,13 @@ **/** - + diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install-ha.cli similarity index 79% rename from distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli rename to distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install-ha.cli index edcd1c6ab6..a95c20fa2c 100644 --- a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli +++ b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install-ha.cli @@ -5,4 +5,7 @@ /subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC") /subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC") /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1") -/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") \ No newline at end of file +/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") +/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) +/subsystem=keycloak-server:add(web-context=auth) +:shutdown(restart=true) \ No newline at end of file diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install.cli index cc594313f9..156197abfa 100644 --- a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install.cli +++ b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-install.cli @@ -1,2 +1,10 @@ +/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true,enabled=true) +/subsystem=logging/logger=org.jboss.resteasy.resteasy_jaxrs.i18n/:add(level=ERROR) +/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak",start="EAGER") +/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=users:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add() /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) -/subsystem=keycloak-server:add(web-context=auth) \ No newline at end of file +/subsystem=keycloak-server:add(web-context=auth) +:shutdown(restart=true) \ No newline at end of file diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli deleted file mode 100644 index 191cb284cf..0000000000 --- a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli +++ /dev/null @@ -1,7 +0,0 @@ -/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true,enabled=true) -/subsystem=logging/logger=org.jboss.resteasy.resteasy_jaxrs.i18n/:add(level=ERROR) -/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak",start="EAGER") -/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=users:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add() \ No newline at end of file diff --git a/distribution/server-overlay/wf9-server-overlay/assembly.xml b/distribution/server-overlay/wf9-server-overlay/assembly.xml index 0427f802ff..8f0daa28b5 100755 --- a/distribution/server-overlay/wf9-server-overlay/assembly.xml +++ b/distribution/server-overlay/wf9-server-overlay/assembly.xml @@ -47,13 +47,13 @@ - + diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install-ha.cli similarity index 77% rename from distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli rename to distribution/server-overlay/wf9-server-overlay/cli/keycloak-install-ha.cli index 5cfae3876a..f9a8d29ee1 100644 --- a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli +++ b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install-ha.cli @@ -4,4 +4,7 @@ /subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC") /subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC") /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1") -/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") \ No newline at end of file +/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") +/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) +/subsystem=keycloak-server:add(web-context=auth) +:shutdown(restart=true) \ No newline at end of file diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli index cc594313f9..d6c398886b 100644 --- a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli +++ b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli @@ -1,2 +1,8 @@ +/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) +/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak") +/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=users:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add() /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) /subsystem=keycloak-server:add(web-context=auth) \ No newline at end of file diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli deleted file mode 100644 index dcad93a2d9..0000000000 --- a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli +++ /dev/null @@ -1,6 +0,0 @@ -/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true) -/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak") -/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=users:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add() \ No newline at end of file diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index 8de75d7d0f..558f943f12 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -43,7 +43,7 @@
- Install on existing WildFly 9.0.0.Final + Install on existing WildFly 9.0.1.Final Keycloak can be installed into an existing WildFly 9.0.0.Final server. To do this download keycloak-overlay-&project.version;.zip or keycloak-overlay-&project.version;.tar.gz. @@ -59,22 +59,23 @@ (username: admin and password: admin). Keycloak will then prompt you to enter in a new password. - - - - - - - - - - - + + To add Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) start the server with + the desired server-config. If you are running the server in standalone mode run: + cd <WILDFLY_HOME>/bin + ./jboss-cli.sh -c --file=keycloak-install.cli + Or if you are running in clustering (HA) mode (by having used -c standalone-ha.xml) then run: + cd <WILDFLY_HOME>/bin + ./jboss-cli.sh -c --file=keycloak-install-ha.cli + You may see exceptions in the server log, but after restarting the server they should be gone. + You can restart the server with: + <WILDFLY_HOME>/bin/jboss-cli.sh -c :reload +
Install on existing JBoss EAP 6.4.0.GA - Same procedure as WildFly 9.0.0.Final, but download keycloak-overlay-eap6-&project.version;.zip or keycloak-overlay-eap6-&project.version;.tar.gz. + Same procedure as WildFly 9.0.1.Final, but download keycloak-overlay-eap6-&project.version;.zip or keycloak-overlay-eap6-&project.version;.tar.gz.
From 0c39e30241d0545c5c7a49d5254f35612799bc1f Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Thu, 15 Oct 2015 13:42:37 +0200 Subject: [PATCH 04/13] KEYCLOAK-1849 Overlay installation scripts not working - fix unsupported dependency 'org.infinispan:main' warning --- .../infinispan/main/module.xml | 34 +++++++++++++++++++ .../WEB-INF/jboss-deployment-structure.xml | 4 ++- .../eap6/eap6-server-modules/build.xml | 2 ++ .../WEB-INF/jboss-deployment-structure.xml | 4 ++- .../infinispan/main/module.xml | 34 +++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100755 distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml create mode 100755 distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml new file mode 100755 index 0000000000..65e3dc2d0c --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 3540faaae6..c339f44671 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -54,7 +54,9 @@ - + + + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/build.xml b/distribution/server-overlay/eap6/eap6-server-modules/build.xml index 22764719ac..60ccb65be0 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/build.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/build.xml @@ -290,6 +290,8 @@ + + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 3540faaae6..c339f44671 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -54,7 +54,9 @@ - + + + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml new file mode 100755 index 0000000000..65e3dc2d0c --- /dev/null +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/infinispan/main/module.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + From 802a39b1ce2f6d9620f2cfa1d179111896a2332f Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 14 Oct 2015 17:45:46 +0200 Subject: [PATCH 05/13] KEYCLOAK-904 Offline session idle timeout + admin console --- .../META-INF/jpa-changelog-1.6.0.xml | 4 + .../idm/RealmRepresentation.java | 9 ++ .../messages/admin-messages_en.properties | 3 + .../theme/base/admin/resources/js/app.js | 18 ++++ .../admin/resources/js/controllers/realm.js | 8 ++ .../admin/resources/js/controllers/users.js | 11 +++ .../theme/base/admin/resources/js/loaders.js | 10 +++ .../theme/base/admin/resources/js/services.js | 7 ++ .../partials/client-offline-sessions.html | 4 +- .../resources/partials/realm-tokens.html | 17 ++++ .../resources/partials/user-consents.html | 2 +- .../partials/user-offline-sessions.html | 35 ++++++++ .../migration/migrators/MigrateTo1_6_0.java | 2 + .../java/org/keycloak/models/Constants.java | 3 + .../java/org/keycloak/models/RealmModel.java | 4 +- .../keycloak/models/entities/RealmEntity.java | 9 ++ .../models/utils/ModelToRepresentation.java | 1 + .../models/utils/RepresentationToModel.java | 4 + .../models/file/adapter/RealmAdapter.java | 10 +++ .../models/cache/infinispan/RealmAdapter.java | 13 +++ .../models/cache/entities/CachedRealm.java | 6 ++ .../org/keycloak/models/jpa/RealmAdapter.java | 10 +++ .../models/jpa/entities/RealmEntity.java | 10 +++ .../mongo/keycloak/adapters/RealmAdapter.java | 11 +++ .../InfinispanUserSessionProvider.java | 35 +++++++- .../compat/MemUserSessionProvider.java | 23 ++++- .../initializer/InitializerState.java | 15 +++- .../initializer/OfflineUserSessionLoader.java | 2 +- .../ClientSessionsOfUserSessionMapper.java | 26 ++++-- .../mapreduce/UserSessionMapper.java | 10 ++- .../keycloak/protocol/oidc/TokenManager.java | 24 +++--- .../services/managers/ApplianceBootstrap.java | 1 + .../services/managers/UserSessionManager.java | 35 ++++++-- .../resources/admin/UsersResource.java | 38 ++++++++- .../keycloak/testsuite/model/ImportTest.java | 2 + .../model/UserSessionInitializerTest.java | 2 +- .../model/UserSessionProviderOfflineTest.java | 85 ++++++++++++++++++- .../testsuite/oauth/OfflineTokenTest.java | 31 +++++-- .../src/test/resources/model/testrealm.json | 1 + 39 files changed, 494 insertions(+), 47 deletions(-) create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml index 9e48a6aa08..07a187a519 100644 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml @@ -2,6 +2,10 @@ + + + + diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 1bec10b7cf..43bed913a6 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -14,6 +14,7 @@ public class RealmRepresentation { protected Integer accessTokenLifespan; protected Integer ssoSessionIdleTimeout; protected Integer ssoSessionMaxLifespan; + protected Integer offlineSessionIdleTimeout; protected Integer accessCodeLifespan; protected Integer accessCodeLifespanUserAction; protected Integer accessCodeLifespanLogin; @@ -199,6 +200,14 @@ public class RealmRepresentation { this.ssoSessionMaxLifespan = ssoSessionMaxLifespan; } + public Integer getOfflineSessionIdleTimeout() { + return offlineSessionIdleTimeout; + } + + public void setOfflineSessionIdleTimeout(Integer offlineSessionIdleTimeout) { + this.offlineSessionIdleTimeout = offlineSessionIdleTimeout; + } + public List getScopeMappings() { return scopeMappings; } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 802645e583..36da3e89e8 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -76,6 +76,8 @@ days=Days sso-session-max=SSO Session Max sso-session-idle.tooltip=Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired. sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired. +offline-session-idle=Offline Session Idle +offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire. access-token-lifespan=Access Token Lifespan access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout. client-login-timeout=Client login timeout @@ -336,6 +338,7 @@ offline-tokens.tooltip=Total number of offline tokens for this client. show-offline-tokens=Show Offline Tokens show-offline-tokens.tooltip=Warning, this is a potentially expensive operation depending on number of offline tokens. token-issued=Token Issued +last-access=Last Access key-export=Key Export key-import=Key Import export-saml-key=Export SAML Key diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 4a1d5a3885..f1d922b31a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -498,6 +498,24 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'UserConsentsCtrl' }) + .when('/realms/:realm/users/:user/offline-sessions/:client', { + templateUrl : resourceUrl + '/partials/user-offline-sessions.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + user : function(UserLoader) { + return UserLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + offlineSessions : function(UserOfflineSessionsLoader) { + return UserOfflineSessionsLoader(); + } + }, + controller : 'UserOfflineSessionsCtrl' + }) .when('/realms/:realm/users', { templateUrl : resourceUrl + '/partials/user-list.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index af93ac8da1..023cdf1271 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -912,6 +912,12 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm.ssoSessionMaxLifespan = TimeUnit.convert($scope.realm.ssoSessionMaxLifespan, from, to); }); + $scope.realm.offlineSessionIdleTimeoutUnit = TimeUnit.autoUnit(realm.offlineSessionIdleTimeout); + $scope.realm.offlineSessionIdleTimeout = TimeUnit.toUnit(realm.offlineSessionIdleTimeout, $scope.realm.offlineSessionIdleTimeoutUnit); + $scope.$watch('realm.offlineSessionIdleTimeoutUnit', function(to, from) { + $scope.realm.offlineSessionIdleTimeout = TimeUnit.convert($scope.realm.offlineSessionIdleTimeout, from, to); + }); + $scope.realm.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan); $scope.realm.accessCodeLifespan = TimeUnit.toUnit(realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit); $scope.$watch('realm.accessCodeLifespanUnit', function(to, from) { @@ -943,6 +949,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, var realmCopy = angular.copy($scope.realm); delete realmCopy["accessTokenLifespanUnit"]; delete realmCopy["ssoSessionMaxLifespanUnit"]; + delete realmCopy["offlineSessionIdleTimeoutUnit"]; delete realmCopy["accessCodeLifespanUnit"]; delete realmCopy["ssoSessionIdleTimeoutUnit"]; delete realmCopy["accessCodeLifespanUserActionUnit"]; @@ -951,6 +958,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit) realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit) realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit) + realmCopy.offlineSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.offlineSessionIdleTimeout, $scope.realm.offlineSessionIdleTimeoutUnit) realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit) realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit) realmCopy.accessCodeLifespanLogin = TimeUnit.toSeconds($scope.realm.accessCodeLifespanLogin, $scope.realm.accessCodeLifespanLoginUnit) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 3746740749..4f45d63bb3 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -216,6 +216,17 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents } }); +module.controller('UserOfflineSessionsCtrl', function($scope, $location, realm, user, client, offlineSessions) { + $scope.realm = realm; + $scope.user = user; + $scope.client = client; + $scope.offlineSessions = offlineSessions; + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + '/users/' + user.id + '/consents'); + }; +}); + module.controller('UserListCtrl', function($scope, realm, User, UserImpersonation, BruteForce, Notifications, $route, Dialog) { $scope.realm = realm; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 9c05940392..7706f0fda8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -181,6 +181,16 @@ module.factory('UserSessionsLoader', function(Loader, UserSessions, $route, $q) }); }); +module.factory('UserOfflineSessionsLoader', function(Loader, UserOfflineSessions, $route, $q) { + return Loader.query(UserOfflineSessions, function() { + return { + realm : $route.current.params.realm, + user : $route.current.params.user, + client : $route.current.params.client + } + }); +}); + module.factory('UserFederatedIdentityLoader', function(Loader, UserFederatedIdentities, $route, $q) { return Loader.query(UserFederatedIdentities, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index b92fd83648..ec0d475bf7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -369,6 +369,13 @@ module.factory('UserSessions', function($resource) { user : '@user' }); }); +module.factory('UserOfflineSessions', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/users/:user/offline-sessions/:client', { + realm : '@realm', + user : '@user', + client : '@client' + }); +}); module.factory('UserSessionLogout', function($resource) { return $resource(authUrl + '/admin/realms/:realm/sessions/:session', { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html index 86f574b6eb..82f5562e06 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html @@ -21,7 +21,7 @@ - + @@ -49,6 +50,7 @@ +
+ @@ -31,6 +31,7 @@ {{:: 'user' | translate}} {{:: 'from-ip' | translate}} {{:: 'token-issued' | translate}}{{:: 'last-access' | translate}}
{{session.username}} {{session.ipAddress}} {{session.start | date:'medium'}}{{session.lastAccess | date:'medium'}}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html index bb5502223d..dee13f666b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html @@ -48,6 +48,23 @@ {{:: 'sso-session-max.tooltip' | translate}} +
+ + +
+ + +
+ {{:: 'offline-session-idle.tooltip' | translate}} +
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html index cf6db992f1..0d0a92ceb5 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html @@ -38,7 +38,7 @@ - , {{additionalGrant}} + , {{additionalGrant.key}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html new file mode 100644 index 0000000000..b06f32625b --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html @@ -0,0 +1,35 @@ +
+ + + + + + + + + + + + + + + + + + + +
IP AddressStartedLast Access
{{session.ipAddress}}{{session.start | date:'medium'}}{{session.lastAccess | date:'medium'}}
+ +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java index ca47f3e906..730810b37b 100644 --- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java @@ -47,6 +47,8 @@ public class MigrateTo1_6_0 { List realms = session.realms().getRealms(); for (RealmModel realm : realms) { + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); + if (realm.getRole(Constants.OFFLINE_ACCESS_ROLE) == null) { for (RoleModel realmRole : realm.getRoles()) { realmRole.setScopeParamRequired(false); diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java index 5fe3189e3f..43bdc7dc68 100755 --- a/model/api/src/main/java/org/keycloak/models/Constants.java +++ b/model/api/src/main/java/org/keycloak/models/Constants.java @@ -19,4 +19,7 @@ public interface Constants { String READ_TOKEN_ROLE = "read-token"; String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE}; String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS; + + // 30 days + int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000; } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 7471a4c2b1..8eb13ee4a8 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -100,8 +100,8 @@ public interface RealmModel extends RoleContainerModel { int getSsoSessionMaxLifespan(); void setSsoSessionMaxLifespan(int seconds); -// int getOfflineSessionIdleTimeout(); -// void setOfflineSessionIdleTimeout(int seconds); + int getOfflineSessionIdleTimeout(); + void setOfflineSessionIdleTimeout(int seconds); int getAccessTokenLifespan(); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index e5fe6d272f..389ec0aabc 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -42,6 +42,7 @@ public class RealmEntity extends AbstractIdentifiableEntity { private boolean revokeRefreshToken; private int ssoSessionIdleTimeout; private int ssoSessionMaxLifespan; + private int offlineSessionIdleTimeout; private int accessTokenLifespan; private int accessCodeLifespan; private int accessCodeLifespanUserAction; @@ -254,6 +255,14 @@ public class RealmEntity extends AbstractIdentifiableEntity { this.ssoSessionMaxLifespan = ssoSessionMaxLifespan; } + public int getOfflineSessionIdleTimeout() { + return offlineSessionIdleTimeout; + } + + public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) { + this.offlineSessionIdleTimeout = offlineSessionIdleTimeout; + } + public int getAccessTokenLifespan() { return accessTokenLifespan; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 6b4a6be2cc..c2a8a178be 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -148,6 +148,7 @@ public class ModelToRepresentation { rep.setAccessTokenLifespan(realm.getAccessTokenLifespan()); rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout()); rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan()); + rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 6c61e663f3..0e1e40eb04 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1,5 +1,6 @@ package org.keycloak.models.utils; +import org.keycloak.models.Constants; import org.keycloak.util.Base64; import org.jboss.logging.Logger; import org.keycloak.enums.SslRequired; @@ -106,6 +107,8 @@ public class RepresentationToModel { else newRealm.setSsoSessionIdleTimeout(1800); if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan()); else newRealm.setSsoSessionMaxLifespan(36000); + if (rep.getOfflineSessionIdleTimeout() != null) newRealm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); + else newRealm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); else newRealm.setAccessCodeLifespan(60); @@ -535,6 +538,7 @@ public class RepresentationToModel { if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout()); if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan()); + if (rep.getOfflineSessionIdleTimeout() != null) realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout()); if (rep.getRequiredCredentials() != null) { realm.updateRequiredCredentials(rep.getRequiredCredentials()); } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 26227b1ee4..381c172b47 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -355,6 +355,16 @@ public class RealmAdapter implements RealmModel { realm.setSsoSessionMaxLifespan(seconds); } + @Override + public int getOfflineSessionIdleTimeout() { + return realm.getOfflineSessionIdleTimeout(); + } + + @Override + public void setOfflineSessionIdleTimeout(int seconds) { + realm.setOfflineSessionIdleTimeout(seconds); + } + @Override public int getAccessTokenLifespan() { return realm.getAccessTokenLifespan(); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 51d445c890..e9b92a63eb 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -275,6 +275,19 @@ public class RealmAdapter implements RealmModel { updated.setSsoSessionMaxLifespan(seconds); } + @Override + public int getOfflineSessionIdleTimeout() { + if (updated != null) return updated.getOfflineSessionIdleTimeout(); + return cached.getOfflineSessionIdleTimeout(); + } + + + @Override + public void setOfflineSessionIdleTimeout(int seconds) { + getDelegateForUpdate(); + updated.setOfflineSessionIdleTimeout(seconds); + } + @Override public int getAccessTokenLifespan() { if (updated != null) return updated.getAccessTokenLifespan(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index 5193588d44..3aa7d383f6 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -58,6 +58,7 @@ public class CachedRealm implements Serializable { private boolean revokeRefreshToken; private int ssoSessionIdleTimeout; private int ssoSessionMaxLifespan; + private int offlineSessionIdleTimeout; private int accessTokenLifespan; private int accessCodeLifespan; private int accessCodeLifespanUserAction; @@ -140,6 +141,7 @@ public class CachedRealm implements Serializable { revokeRefreshToken = model.isRevokeRefreshToken(); ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout(); ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan(); + offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout(); accessTokenLifespan = model.getAccessTokenLifespan(); accessCodeLifespan = model.getAccessCodeLifespan(); accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction(); @@ -327,6 +329,10 @@ public class CachedRealm implements Serializable { return ssoSessionMaxLifespan; } + public int getOfflineSessionIdleTimeout() { + return offlineSessionIdleTimeout; + } + public int getAccessTokenLifespan() { return accessTokenLifespan; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 2c8e2ad9fc..9290013ddc 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -377,6 +377,16 @@ public class RealmAdapter implements RealmModel { realm.setSsoSessionMaxLifespan(seconds); } + @Override + public int getOfflineSessionIdleTimeout() { + return realm.getOfflineSessionIdleTimeout(); + } + + @Override + public void setOfflineSessionIdleTimeout(int seconds) { + realm.setOfflineSessionIdleTimeout(seconds); + } + @Override public int getAccessCodeLifespan() { return realm.getAccessCodeLifespan(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 27c4824f94..bf5b339577 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -82,6 +82,8 @@ public class RealmEntity { private int ssoSessionIdleTimeout; @Column(name="SSO_MAX_LIFESPAN") private int ssoSessionMaxLifespan; + @Column(name="OFFLINE_SESSION_IDLE_TIMEOUT") + private int offlineSessionIdleTimeout; @Column(name="ACCESS_TOKEN_LIFESPAN") protected int accessTokenLifespan; @Column(name="ACCESS_CODE_LIFESPAN") @@ -314,6 +316,14 @@ public class RealmEntity { this.ssoSessionMaxLifespan = ssoSessionMaxLifespan; } + public int getOfflineSessionIdleTimeout() { + return offlineSessionIdleTimeout; + } + + public void setOfflineSessionIdleTimeout(int offlineSessionIdleTimeout) { + this.offlineSessionIdleTimeout = offlineSessionIdleTimeout; + } + public int getAccessTokenLifespan() { return accessTokenLifespan; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 05ae8bcd4c..b03c463792 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -344,6 +344,17 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + @Override + public int getOfflineSessionIdleTimeout() { + return realm.getOfflineSessionIdleTimeout(); + } + + @Override + public void setOfflineSessionIdleTimeout(int seconds) { + realm.setOfflineSessionIdleTimeout(seconds); + updateRealm(); + } + @Override public int getAccessTokenLifespan() { return realm.getAccessTokenLifespan(); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index dbfecb4357..34cc4bc3c9 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -13,6 +13,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; @@ -302,8 +303,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void removeExpiredUserSessions(RealmModel realm) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan(); int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); + int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); Map map = new MapReduceTask(sessionCache) @@ -323,6 +327,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { for (String id : map.keySet()) { tx.remove(sessionCache, id); } + + // Remove expired offline user sessions + map = new MapReduceTask(offlineSessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(offlineSessionCache, id); + // propagate to persister + persister.removeUserSession(id, true); + } + + // Remove offline client sessions of expired offline user sessions + map = new MapReduceTask(offlineSessionCache) + .mappedWith(new ClientSessionsOfUserSessionMapper(realm.getId(), new HashSet<>(map.keySet())).emitKey()) + .reducedWith(new FirstResultReducer()) + .execute(); + + for (String id : map.keySet()) { + tx.remove(offlineSessionCache, id); + } + } @Override @@ -477,6 +504,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { tx.remove(cache, userSessionId); + // TODO: We can retrieve it from userSessionEntity directly Map map = new MapReduceTask(cache) .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) .reducedWith(new FirstResultReducer()) @@ -534,14 +562,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setBrokerSessionId(userSession.getBrokerSessionId()); entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); - entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); entity.setLoginUsername(userSession.getLoginUsername()); entity.setNotes(userSession.getNotes()); entity.setRememberMe(userSession.isRememberMe()); - entity.setStarted(userSession.getStarted()); entity.setState(userSession.getState()); entity.setUser(userSession.getUser().getId()); + // started and lastSessionRefresh set to current time + int currentTime = Time.currentTime(); + entity.setStarted(currentTime); + entity.setLastSessionRefresh(currentTime); + tx.put(offlineSessionCache, userSession.getId(), entity); return wrap(userSession.getRealm(), entity, true); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java index 0e5f2b9784..6cbb1eb82c 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java @@ -10,6 +10,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UsernameLoginFailureModel; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity; @@ -297,6 +298,8 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public void removeExpiredUserSessions(RealmModel realm) { + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + Iterator itr = userSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); @@ -314,6 +317,19 @@ public class MemUserSessionProvider implements UserSessionProvider { citr.remove(); } } + + // Remove expired offline sessions + itr = offlineUserSessions.values().iterator(); + while (itr.hasNext()) { + UserSessionEntity s = itr.next(); + if (s.getRealm().equals(realm.getId()) && (s.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout())) { + itr.remove(); + remove(s, true); + + // propagate to persister + persister.removeUserSession(s.getId(), true); + } + } } @Override @@ -415,16 +431,19 @@ public class MemUserSessionProvider implements UserSessionProvider { entity.setBrokerSessionId(userSession.getBrokerSessionId()); entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); - entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); entity.setLoginUsername(userSession.getLoginUsername()); if (userSession.getNotes() != null) { entity.getNotes().putAll(userSession.getNotes()); } entity.setRememberMe(userSession.isRememberMe()); - entity.setStarted(userSession.getStarted()); entity.setState(userSession.getState()); entity.setUser(userSession.getUser().getId()); + // started and lastSessionRefresh set to current time + int currentTime = Time.currentTime(); + entity.setStarted(currentTime); + entity.setLastSessionRefresh(currentTime); + offlineUserSessions.put(userSession.getId(), entity); return new UserSessionAdapter(session, this, userSession.getRealm(), entity); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java index eda7370b42..ccc6fd6a69 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java @@ -15,6 +15,7 @@ public class InitializerState extends SessionEntity { private int sessionsCount; private List segments = new ArrayList<>(); + private int lowestUnfinishedSegment = 0; public void init(int sessionsCount, int sessionsPerSegment) { @@ -31,18 +32,21 @@ public class InitializerState extends SessionEntity { for (int i=0 ; i getUnfinishedSegments(int segmentCount) { List result = new ArrayList<>(); - boolean remaining = true; - int next=0; + int next = lowestUnfinishedSegment; + boolean remaining = lowestUnfinishedSegment != -1; + while (remaining && result.size() < segmentCount) { next = getNextUnfinishedSegmentFromIndex(next); if (next == -1) { @@ -58,6 +62,11 @@ public class InitializerState extends SessionEntity { public void markSegmentFinished(int index) { segments.set(index, true); + updateLowestUnfinishedSegment(); + } + + private void updateLowestUnfinishedSegment() { + this.lowestUnfinishedSegment = getNextUnfinishedSegmentFromIndex(lowestUnfinishedSegment); } private int getNextUnfinishedSegmentFromIndex(int index) { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java index e53236997a..20ec696c07 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java @@ -29,7 +29,7 @@ public class OfflineUserSessionLoader implements SessionLoader { for (UserSessionModel persistentSession : sessions) { - // Update and persist lastSessionRefresh time + // Update and persist lastSessionRefresh time TODO: Do bulk DB update instead? persistentSession.setLastSessionRefresh(currentTime); persister.updateUserSession(persistentSession, true); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java index 8300944b37..2f538aef76 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java @@ -13,18 +13,29 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity; * * @author Marek Posolda */ -public class ClientSessionsOfUserSessionMapper implements Mapper, Serializable { +public class ClientSessionsOfUserSessionMapper implements Mapper, Serializable { private String realm; private Collection userSessions; + private EmitValue emit = EmitValue.ENTITY; + + private enum EmitValue { + KEY, ENTITY + } + public ClientSessionsOfUserSessionMapper(String realm, Collection userSessions) { this.realm = realm; this.userSessions = userSessions; } + public ClientSessionsOfUserSessionMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + @Override - public void map(String key, SessionEntity e, Collector collector) { + public void map(String key, SessionEntity e, Collector collector) { if (!realm.equals(e.getRealm())) { return; } @@ -35,9 +46,14 @@ public class ClientSessionsOfUserSessionMapper implements Mapper expiredRefresh) { + return; + } + switch (emit) { case KEY: collector.emit(key, key); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 0888dab878..8882da4f8b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -98,9 +98,17 @@ public class TokenManager { ClientSessionModel clientSession = null; if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { - clientSession = new UserSessionManager(session).findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState()); + UserSessionManager sessionManager = new UserSessionManager(session); + clientSession = sessionManager.findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState()); if (clientSession != null) { userSession = clientSession.getUserSession(); + + // Revoke timeouted offline userSession + if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) { + sessionManager.revokeOfflineUserSession(userSession); + userSession = null; + clientSession = null; + } } } else { // Find userSession regularly for online tokens @@ -172,16 +180,12 @@ public class TokenManager { validation.userSession.setLastSessionRefresh(currentTime); - AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession) + AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession) .accessToken(validation.newToken) - .generateIDToken(); + .generateIDToken() + .generateRefreshToken() + .build(); - // Don't generate refresh token again if refresh was triggered with offline token - if (!refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) { - responseBuilder.generateRefreshToken(); - } - - AccessTokenResponse res = responseBuilder.build(); return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType())); } @@ -507,7 +511,7 @@ public class TokenManager { refreshToken = new RefreshToken(accessToken); refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE); - sessionManager.persistOfflineSession(clientSession, userSession); + sessionManager.createOrUpdateOfflineSession(clientSession, userSession); } else { refreshToken = new RefreshToken(accessToken); refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout()); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 95b4243667..d3569650a2 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -52,6 +52,7 @@ public class ApplianceBootstrap { realm.setSsoSessionIdleTimeout(1800); realm.setAccessTokenLifespan(60); realm.setSsoSessionMaxLifespan(36000); + realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); realm.setAccessCodeLifespan(60); realm.setAccessCodeLifespanUserAction(300); realm.setAccessCodeLifespanLogin(1800); diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java index 5759d2031c..2f18d24e95 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java @@ -1,6 +1,7 @@ package org.keycloak.services.managers; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -15,6 +16,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.util.Time; /** * @@ -32,17 +34,23 @@ public class UserSessionManager { this.persister = session.getProvider(UserSessionPersisterProvider.class); } - public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { + public void createOrUpdateOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { UserModel user = userSession.getUser(); - // Verify if we already have UserSession with this ID. If yes, don't create another one + // Create and persist offline userSession if we don't have one UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId()); if (offlineUserSession == null) { offlineUserSession = createOfflineUserSession(user, userSession); + } else { + // update lastSessionRefresh but don't need to persist + offlineUserSession.setLastSessionRefresh(Time.currentTime()); } - // Create clientSession and save to DB. - createOfflineClientSession(user, clientSession, offlineUserSession); + // Create and persist clientSession + ClientSessionModel offlineClientSession = kcSession.sessions().getOfflineClientSession(clientSession.getRealm(), clientSession.getId()); + if (offlineClientSession == null) { + createOfflineClientSession(user, clientSession, offlineUserSession); + } } // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation @@ -69,6 +77,15 @@ public class UserSessionManager { return clients; } + public List findOfflineSessions(RealmModel realm, ClientModel client, UserModel user) { + List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); + List userSessions = new LinkedList<>(); + for (ClientSessionModel clientSession : clientSessions) { + userSessions.add(clientSession.getUserSession()); + } + return userSessions; + } + public boolean revokeOfflineToken(UserModel user, ClientModel client) { RealmModel realm = client.getRealm(); @@ -91,6 +108,14 @@ public class UserSessionManager { return anyRemoved; } + public void revokeOfflineUserSession(UserSessionModel userSession) { + if (logger.isTraceEnabled()) { + logger.tracef("Removing offline user session '%s' for user '%s' ", userSession.getId(), userSession.getLoginUsername()); + } + kcSession.sessions().removeOfflineUserSession(userSession.getRealm(), userSession.getId()); + persister.removeUserSession(userSession.getId(), true); + } + public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) { RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE); if (offlineAccessRole == null) { @@ -107,7 +132,7 @@ public class UserSessionManager { } UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession); - persister.createUserSession(userSession, true); + persister.createUserSession(offlineUserSession, true); return offlineUserSession; } 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 22ebbb5c11..4c8796d431 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 @@ -349,6 +349,35 @@ public class UsersResource { return reps; } + /** + * Get offline sessions associated with the user and client + * + * @param id User id + * @return + */ + @Path("{id}/offline-sessions/{clientId}") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List getSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) { + auth.requireView(); + UserModel user = session.users().getUserById(id, realm); + if (user == null) { + throw new NotFoundException("User not found"); + } + ClientModel client = realm.getClientById(clientId); + if (client == null) { + throw new NotFoundException("Client not found"); + } + List sessions = new UserSessionManager(session).findOfflineSessions(realm, client, user); + List reps = new ArrayList(); + for (UserSessionModel session : sessions) { + UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); + reps.add(rep); + } + return reps; + } + /** * Get social logins associated with the user * @@ -469,7 +498,14 @@ public class UsersResource { currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles())); currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles())); - List additionalGrants = hasOfflineToken ? Arrays.asList("Offline Token") : Collections.emptyList(); + List> additionalGrants = new LinkedList<>(); + if (hasOfflineToken) { + Map offlineTokens = new HashMap<>(); + offlineTokens.put("client", client.getId()); + // TODO: translate + offlineTokens.put("key", "Offline Token"); + additionalGrants.add(offlineTokens); + } currentRep.put("additionalGrants", additionalGrants); result.add(currentRep); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index b13bcdc5cd..5d7eae7ef7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -76,6 +76,7 @@ public class ImportTest extends AbstractModelTest { // Moved to static method, so it's possible to test this from other places too (for example export-import tests) public static void assertDataImportedInRealm(KeycloakSession session, RealmModel realm) { Assert.assertTrue(realm.isVerifyEmail()); + Assert.assertEquals(3600000, realm.getOfflineSessionIdleTimeout()); List creds = realm.getRequiredCredentials(); Assert.assertEquals(1, creds.size()); @@ -361,6 +362,7 @@ public class ImportTest extends AbstractModelTest { RealmModel realm =manager.importRealm(rep); Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction()); + Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout()); verifyRequiredCredentials(realm.getRequiredCredentials(), "password"); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java index cf7cc8ab42..9e0358ff09 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -68,7 +68,7 @@ public class UserSessionInitializerTest { for (UserSessionModel origSession : origSessions) { UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId()); for (ClientSessionModel clientSession : userSession.getClientSessions()) { - sessionManager.persistOfflineSession(clientSession, userSession); + sessionManager.createOrUpdateOfflineSession(clientSession, userSession); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index c843928429..57c99f8ad5 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.model; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -17,6 +18,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -36,6 +38,7 @@ public class UserSessionProviderOfflineTest { private KeycloakSession session; private RealmModel realm; private UserSessionManager sessionManager; + private UserSessionPersisterProvider persister; @Before public void before() { @@ -44,6 +47,7 @@ public class UserSessionProviderOfflineTest { session.users().addUser(realm, "user1").setEmail("user1@localhost"); session.users().addUser(realm, "user2").setEmail("user2@localhost"); sessionManager = new UserSessionManager(session); + persister = session.getProvider(UserSessionPersisterProvider.class); } @After @@ -157,7 +161,7 @@ public class UserSessionProviderOfflineTest { fooRealm = session.realms().getRealm("foo"); userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId()); - sessionManager.persistOfflineSession(userSession.getClientSessions().get(0), userSession); + sessionManager.createOrUpdateOfflineSession(userSession.getClientSessions().get(0), userSession); resetSession(); @@ -291,13 +295,85 @@ public class UserSessionProviderOfflineTest { } + @Test + public void testExpired() { + // Create some online sessions in infinispan + int started = Time.currentTime(); + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + Map offlineSessions = new HashMap<>(); + + // Persist 3 created userSessions and clientSessions as offline + ClientModel testApp = realm.getClientByClientId("test-app"); + List userSessions = session.sessions().getUserSessions(realm, testApp); + for (UserSessionModel userSession : userSessions) { + offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession)); + } + + resetSession(); + + // Assert all previously saved offline sessions found + for (Map.Entry entry : offlineSessions.entrySet()) { + Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null); + } + + UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId()); + Assert.assertNotNull(session0); + List clientSessions = new LinkedList<>(); + for (ClientSessionModel clientSession : session0.getClientSessions()) { + clientSessions.add(clientSession.getId()); + Assert.assertNotNull(session.sessions().getOfflineClientSession(realm, clientSession.getId())); + } + + // sessions are in persister too + Assert.assertEquals(3, persister.getUserSessionsCount(true)); + + // Set lastSessionRefresh to session[0] to 0 + session0.setLastSessionRefresh(0); + + resetSession(); + + session.sessions().removeExpiredUserSessions(realm); + + resetSession(); + + // assert sessions not found now + Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId())); + for (String clientSession : clientSessions) { + Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId())); + offlineSessions.remove(clientSession); + } + + // Assert other offline sessions still found + for (Map.Entry entry : offlineSessions.entrySet()) { + Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null); + } + Assert.assertEquals(2, persister.getUserSessionsCount(true)); + + // Expire everything and assert nothing found + Time.setOffset(3000000); + try { + session.sessions().removeExpiredUserSessions(realm); + + resetSession(); + + for (Map.Entry entry : offlineSessions.entrySet()) { + Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) == null); + } + Assert.assertEquals(0, persister.getUserSessionsCount(true)); + + } finally { + Time.setOffset(0); + } + } + private Map createOfflineSessionIncludeClientSessions(UserSessionModel userSession) { Map offlineSessions = new HashMap<>(); - UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(userSession); for (ClientSessionModel clientSession : userSession.getClientSessions()) { - ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession); - offlineClientSession.setUserSession(offlineUserSession); + sessionManager.createOrUpdateOfflineSession(clientSession, userSession); offlineSessions.put(clientSession.getId(), userSession.getId()); } return offlineSessions; @@ -310,6 +386,7 @@ public class UserSessionProviderOfflineTest { session = kc.startSession(); realm = session.realms().getRealm("test"); sessionManager = new UserSessionManager(session); + persister = session.getProvider(UserSessionPersisterProvider.class); } private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index 9f95f2617d..370e7fdf58 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -227,10 +227,27 @@ public class OfflineTokenTest { Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); Assert.assertEquals(0, offlineToken.getExpiration()); - testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId); + String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId); + + // Change offset to very big value to ensure offline session expires + Time.setOffset(3000000); + + OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(newRefreshTokenString, "secret1"); + Assert.assertEquals(400, response.getStatusCode()); + assertEquals("invalid_grant", response.getError()); + + events.expectRefresh(offlineToken.getId(), sessionId) + .client("offline-client") + .error(Errors.INVALID_TOKEN) + .user(userId) + .clearDetails() + .assertEvent(); + + + Time.setOffset(0); } - private void testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString, + private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString, final String sessionId, String userId) { // Change offset to big value to ensure userSession expired Time.setOffset(99999); @@ -261,8 +278,9 @@ public class OfflineTokenTest { Assert.assertEquals(200, response.getStatusCode()); Assert.assertEquals(sessionId, refreshedToken.getSessionState()); - // Assert no refreshToken in the response - Assert.assertNull(response.getRefreshToken()); + // Assert new refreshToken in the response + String newRefreshToken = response.getRefreshToken(); + Assert.assertNotNull(newRefreshToken); Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId()); Assert.assertEquals(userId, refreshedToken.getSubject()); @@ -283,6 +301,7 @@ public class OfflineTokenTest { Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID)); Time.setOffset(0); + return newRefreshToken; } @Test @@ -382,11 +401,11 @@ public class OfflineTokenTest { String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId(); String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.getId(); - // Assert access token will be refreshed, but offline token will be still the same + // Assert access token and offline token are refreshed Time.setOffset(9999); driver.navigate().to(offlineClientAppUri); Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri)); - Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId); + Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId); Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId); // Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB) diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index 521ef83600..53e7f7ee25 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -4,6 +4,7 @@ "accessTokenLifespan": 6000, "accessCodeLifespan": 30, "accessCodeLifespanUserAction": 600, + "offlineSessionIdleTimeout": 3600000, "requiredCredentials": [ "password" ], "defaultRoles": [ "foo", "bar" ], "verifyEmail" : "true", From b4520baee589abcf310273afc986855d19a1b363 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 15 Oct 2015 11:18:02 +0200 Subject: [PATCH 06/13] KEYCLOAK-1959 Role offline_access was effective only when explicitly added to user --- .../keycloak/protocol/oidc/TokenManager.java | 28 +++++++++++ .../testsuite/oauth/OfflineTokenTest.java | 50 ++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 8882da4f8b..f4de6656cb 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -44,6 +44,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; @@ -325,6 +327,21 @@ public class TokenManager { } } } + + // Add all roles specified in scope parameter directly into requestedRoles, even if they are available just through composite role + List scopeRoles = new LinkedList<>(); + for (String scopeParamPart : scopeParamRoles) { + RoleModel scopeParamRole = getRoleFromScopeParam(client.getRealm(), scopeParamPart); + if (scopeParamRole != null) { + for (RoleModel role : roles) { + if (role.hasRole(scopeParamRole)) { + scopeRoles.add(scopeParamRole); + } + } + } + } + + roles.addAll(scopeRoles); requestedRoles = roles; } @@ -341,6 +358,17 @@ public class TokenManager { } } + // For now, just use "roleName" for realm roles and "clientId/roleName" for client roles + private static RoleModel getRoleFromScopeParam(RealmModel realm, String scopeParamRole) { + String[] parts = scopeParamRole.split("/"); + if (parts.length == 1) { + return realm.getRole(parts[0]); + } else { + ClientModel roleClient = realm.getClientByClientId(parts[0]); + return roleClient!=null ? roleClient.getRole(parts[1]) : null; + } + } + public void verifyAccess(AccessToken token, AccessToken newToken) throws OAuthErrorException { if (token.getRealmAccess() != null) { if (newToken.getRealmAccess() == null) throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "User no long has permission for realm roles"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index 370e7fdf58..b7596a073d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -30,6 +30,7 @@ import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; import org.keycloak.services.managers.ClientManager; @@ -285,7 +286,6 @@ public class OfflineTokenTest { Assert.assertEquals(userId, refreshedToken.getSubject()); - Assert.assertEquals(2, refreshedToken.getRealmAccess().getRoles().size()); Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user")); Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE)); @@ -384,6 +384,54 @@ public class OfflineTokenTest { testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId); } + @Test + public void offlineTokenAllowedWithCompositeRole() throws Exception { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + ClientModel offlineClient = appRealm.getClientByClientId("offline-client"); + UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm); + RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE); + + // Test access + Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess)); + Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess)); + + // Grant offline_access role indirectly through composite role + RoleModel composite = appRealm.addRole("composite"); + composite.addCompositeRole(offlineAccess); + + testUser.deleteRoleMapping(offlineAccess); + testUser.grantRole(composite); + + // Test access + Assert.assertFalse(TokenManager.getAccess(null, true, offlineClient, testUser).contains(offlineAccess)); + Assert.assertTrue(TokenManager.getAccess(OAuth2Constants.OFFLINE_ACCESS, true, offlineClient, testUser).contains(offlineAccess)); + } + + }); + + // Integration test + offlineTokenDirectGrantFlow(); + + // Revert changes + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + RoleModel composite = appRealm.getRole("composite"); + RoleModel offlineAccess = appRealm.getRole(Constants.OFFLINE_ACCESS_ROLE); + UserModel testUser = session.users().getUserByUsername("test-user@localhost", appRealm); + + testUser.deleteRoleMapping(composite); + appRealm.removeRole(composite); + testUser.grantRole(offlineAccess); + } + + }); + } + @Test public void testServlet() { OfflineTokenServlet.tokenInfo = null; From 67435791ed379be1916fb1b186d244bcbb752f6d Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 15 Oct 2015 20:46:08 +0200 Subject: [PATCH 07/13] KEYCLOAK-1961 revokeRefreshToken support for offline tokens and other fixes --- .../META-INF/jpa-changelog-1.6.0.xml | 10 +- .../messages/admin-messages_en.properties | 1 + .../partials/client-offline-sessions.html | 2 +- .../partials/user-offline-sessions.html | 2 +- .../keycloak/models/UserSessionProvider.java | 4 + .../PersistentClientSessionEntity.java | 9 ++ .../DisabledUserSessionPersisterProvider.java | 5 + .../PersistentClientSessionAdapter.java | 17 +-- .../session/PersistentClientSessionModel.java | 9 ++ .../session/UserSessionPersisterProvider.java | 3 + .../JpaUserSessionPersisterProvider.java | 24 ++-- .../PersistentClientSessionEntity.java | 16 ++- .../session/PersistentUserSessionEntity.java | 3 +- .../MongoUserSessionPersisterProvider.java | 48 +++++-- .../InfinispanUserSessionProvider.java | 121 +++++++++++------- .../InfinispanUserSessionProviderFactory.java | 10 +- .../compat/MemUserSessionProvider.java | 49 ++++++- .../compat/SimpleUserSessionInitializer.java | 12 ++ .../InfinispanUserSessionInitializer.java | 20 ++- .../initializer/InitializerState.java | 19 +-- .../initializer/OfflineUserSessionLoader.java | 29 +++-- .../infinispan/initializer/SessionLoader.java | 2 + .../keycloak/protocol/oidc/TokenManager.java | 8 +- .../DefaultKeycloakSessionFactory.java | 1 + .../resources/admin/ClientResource.java | 10 ++ .../resources/admin/UsersResource.java | 10 ++ .../model/UserSessionInitializerTest.java | 38 +++--- .../UserSessionPersisterProviderTest.java | 51 +++++++- .../model/UserSessionProviderOfflineTest.java | 20 ++- .../testsuite/oauth/OfflineTokenTest.java | 65 ++++++++++ 30 files changed, 451 insertions(+), 167 deletions(-) diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml index 07a187a519..3d5b99f1ab 100644 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml @@ -4,6 +4,9 @@ + + + @@ -47,16 +50,11 @@ + - - - - - - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 36da3e89e8..f13d6e8e65 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -339,6 +339,7 @@ show-offline-tokens=Show Offline Tokens show-offline-tokens.tooltip=Warning, this is a potentially expensive operation depending on number of offline tokens. token-issued=Token Issued last-access=Last Access +last-refresh=Last Refresh key-export=Key Export key-import=Key Import export-saml-key=Export SAML Key diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html index 82f5562e06..3d2aaf7a1b 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html @@ -31,7 +31,7 @@ {{:: 'user' | translate}} {{:: 'from-ip' | translate}} {{:: 'token-issued' | translate}} - {{:: 'last-access' | translate}} + {{:: 'last-refresh' | translate}} diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html index b06f32625b..bc7ad501bd 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-offline-sessions.html @@ -13,7 +13,7 @@ IP Address Started - Last Access + Last Refresh diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index 836cc75769..1a59f4ffef 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -59,6 +59,10 @@ public interface UserSessionProvider extends Provider { int getOfflineSessionsCount(RealmModel realm, ClientModel client); List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max); + // Triggered by persister during pre-load + UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline); + ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline); + void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java index a03edd31d5..1c1802ef6e 100644 --- a/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/PersistentClientSessionEntity.java @@ -7,6 +7,7 @@ public class PersistentClientSessionEntity { private String clientSessionId; private String clientId; + private int timestamp; private String data; public String getClientSessionId() { @@ -25,6 +26,14 @@ public class PersistentClientSessionEntity { this.clientId = clientId; } + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + public String getData() { return data; } diff --git a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java index 6daf7c749c..809fbd29e0 100644 --- a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java +++ b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java @@ -92,6 +92,11 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste } + @Override + public void updateAllTimestamps(int time) { + + } + @Override public List loadUserSessions(int firstResult, int maxResults, boolean offline) { return Collections.emptyList(); diff --git a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java index 1fced88bbe..8465269e85 100644 --- a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java @@ -37,7 +37,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { data.setProtocolMappers(clientSession.getProtocolMappers()); data.setRedirectUri(clientSession.getRedirectUri()); data.setRoles(clientSession.getRoles()); - data.setTimestamp(clientSession.getTimestamp()); data.setUserSessionNotes(clientSession.getUserSessionNotes()); model = new PersistentClientSessionModel(); @@ -47,6 +46,7 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { model.setUserId(clientSession.getAuthenticatedUser().getId()); } model.setUserSessionId(clientSession.getUserSession().getId()); + model.setTimestamp(clientSession.getTimestamp()); realm = clientSession.getRealm(); client = clientSession.getClient(); @@ -122,12 +122,12 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { @Override public int getTimestamp() { - return getData().getTimestamp(); + return model.getTimestamp(); } @Override public void setTimestamp(int timestamp) { - getData().setTimestamp(timestamp); + model.setTimestamp(timestamp); } @Override @@ -309,9 +309,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { @JsonProperty("executionStatus") private Map executionStatus = new HashMap<>(); - @JsonProperty("timestamp") - private int timestamp; - @JsonProperty("action") private String action; @@ -374,14 +371,6 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { this.executionStatus = executionStatus; } - public int getTimestamp() { - return timestamp; - } - - public void setTimestamp(int timestamp) { - this.timestamp = timestamp; - } - public String getAction() { return action; } diff --git a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java index 96e900fb7b..b1a388b82e 100644 --- a/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java @@ -9,6 +9,7 @@ public class PersistentClientSessionModel { private String userSessionId; private String clientId; private String userId; + private int timestamp; private String data; public String getClientSessionId() { @@ -43,6 +44,14 @@ public class PersistentClientSessionModel { this.userId = userId; } + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + public String getData() { return data; } diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java index 4b3355e9d3..5863fdb4a3 100644 --- a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java +++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java @@ -35,6 +35,9 @@ public interface UserSessionPersisterProvider extends Provider { // Called at startup to remove userSessions without any clientSession void clearDetachedUserSessions(); + // Update "lastSessionRefresh" of all userSessions and "timestamp" of all clientSessions to specified time + void updateAllTimestamps(int time); + // Called during startup. For each userSession, it loads also clientSessions List loadUserSessions(int firstResult, int maxResults, boolean offline); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index ffc04557b1..bf19d96980 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -58,6 +58,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv PersistentClientSessionEntity entity = new PersistentClientSessionEntity(); entity.setClientSessionId(clientSession.getId()); entity.setClientId(clientSession.getClient().getId()); + entity.setTimestamp(clientSession.getTimestamp()); entity.setOffline(offline); entity.setUserSessionId(clientSession.getUserSession().getId()); entity.setData(model.getData()); @@ -128,26 +129,32 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv @Override public void onRealmRemoved(RealmModel realm) { - em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); - em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + int num = em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); + num = em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate(); } @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate(); - em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); + int num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate(); + num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); } @Override public void onUserRemoved(RealmModel realm, UserModel user) { - em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); - em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); + int num = em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); + num = em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate(); } @Override public void clearDetachedUserSessions() { - em.createNamedQuery("deleteDetachedClientSessions").executeUpdate(); - em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); + int num = em.createNamedQuery("deleteDetachedClientSessions").executeUpdate(); + num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate(); + } + + @Override + public void updateAllTimestamps(int time) { + int num = em.createNamedQuery("updateClientSessionsTimestamps").setParameter("timestamp", time).executeUpdate(); + num = em.createNamedQuery("updateUserSessionsTimestamps").setParameter("lastSessionRefresh", time).executeUpdate(); } @Override @@ -220,6 +227,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv model.setClientId(entity.getClientId()); model.setUserSessionId(userSession.getId()); model.setUserId(userSession.getUser().getId()); + model.setTimestamp(entity.getTimestamp()); model.setData(entity.getData()); return new PersistentClientSessionAdapter(model, realm, client, userSession); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index a11b87516a..faf3f80556 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -17,13 +17,14 @@ import javax.persistence.Table; * @author Marek Posolda */ @NamedQueries({ - @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.realmId=:realmId)"), + @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId=:clientId"), - @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId in (select u from PersistentUserSessionEntity u where u.userId=:userId)"), + @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId=:userId)"), @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId NOT IN (select u.userSessionId from PersistentUserSessionEntity u)"), @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"), + @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp=:timestamp"), }) @Table(name="OFFLINE_CLIENT_SESSION") @Entity @@ -40,6 +41,9 @@ public class PersistentClientSessionEntity { @Column(name="CLIENT_ID", length = 36) protected String clientId; + @Column(name="TIMESTAMP") + protected int timestamp; + @Id @Column(name = "OFFLINE") protected boolean offline; @@ -71,6 +75,14 @@ public class PersistentClientSessionEntity { this.clientId = clientId; } + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + public boolean isOffline() { return offline; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java index f739091fe9..95745a823a 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java @@ -26,7 +26,8 @@ import org.keycloak.models.jpa.entities.UserEntity; @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"), @NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"), @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where offline=:offline"), - @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId") + @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId"), + @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh=:lastSessionRefresh"), }) @Table(name="OFFLINE_USER_SESSION") diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java index 53917c2b11..f23e7fbe99 100644 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java @@ -45,10 +45,6 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr return invocationContext.getMongoStore(); } - private Class getClazz(boolean offline) { - return offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; - } - private MongoUserSessionEntity loadUserSession(String userSessionId, boolean offline) { Class clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class; return getMongoStore().loadEntity(clazz, userSessionId, invocationContext); @@ -220,6 +216,41 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr return getMongoStore().countEntities(clazz, query, invocationContext); } + @Override + public void updateAllTimestamps(int time) { + // 1) Update timestamp of clientSessions + + DBObject timestampSubquery = new QueryBuilder() + .and("timestamp").notEquals(time).get(); + + DBObject query = new QueryBuilder() + .and("clientSessions").elemMatch(timestampSubquery).get(); + + + DBObject update = new QueryBuilder() + .and("$set").is(new BasicDBObject("clientSessions.$.timestamp", time)).get(); + + // Not sure how to do in single query :/ + int countModified = 1; + while (countModified > 0) { + countModified = getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext); + } + + countModified = 1; + while (countModified > 0) { + countModified = getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext); + } + + // 2) update lastSessionRefresh of userSessions + query = new QueryBuilder().get(); + + update = new QueryBuilder() + .and("$set").is(new BasicDBObject("lastSessionRefresh", time)).get(); + + getMongoStore().updateEntities(MongoOfflineUserSessionEntity.class, query, update, invocationContext); + getMongoStore().updateEntities(MongoOnlineUserSessionEntity.class, query, update, invocationContext); + } + @Override public List loadUserSessions(int firstResult, int maxResults, boolean offline) { DBObject query = new QueryBuilder() @@ -232,13 +263,13 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr List results = new LinkedList<>(); for (MongoUserSessionEntity entity : entities) { - PersistentUserSessionAdapter userSession = toAdapter(entity, offline); + PersistentUserSessionAdapter userSession = toAdapter(entity); results.add(userSession); } return results; } - private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity, boolean offline) { + private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) { RealmModel realm = session.realms().getRealm(entity.getRealmId()); UserModel user = session.users().getUserById(entity.getUserId(), realm); @@ -250,14 +281,14 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr List clientSessions = new LinkedList<>(); PersistentUserSessionAdapter userSessionAdapter = new PersistentUserSessionAdapter(model, realm, user, clientSessions); for (PersistentClientSessionEntity clientSessEntity : entity.getClientSessions()) { - PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, offline, clientSessEntity); + PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, clientSessEntity); clientSessions.add(clientSessAdapter); } return userSessionAdapter; } - private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, boolean offline, PersistentClientSessionEntity entity) { + private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { ClientModel client = realm.getClientById(entity.getClientId()); PersistentClientSessionModel model = new PersistentClientSessionModel(); @@ -265,6 +296,7 @@ public class MongoUserSessionPersisterProvider implements UserSessionPersisterPr model.setClientId(entity.getClientId()); model.setUserSessionId(userSession.getId()); model.setUserId(userSession.getUser().getId()); + model.setTimestamp(entity.getTimestamp()); model.setData(entity.getData()); return new PersistentClientSessionAdapter(model, realm, client, userSession); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 34cc4bc3c9..627edcfe96 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -329,25 +329,32 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } // Remove expired offline user sessions - map = new MapReduceTask(offlineSessionCache) - .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline).emitKey()) + Map map2 = new MapReduceTask(offlineSessionCache) + .mappedWith(UserSessionMapper.create(realm.getId()).expired(null, expiredOffline)) .reducedWith(new FirstResultReducer()) .execute(); - for (String id : map.keySet()) { - tx.remove(offlineSessionCache, id); - // propagate to persister - persister.removeUserSession(id, true); + for (Map.Entry entry : map2.entrySet()) { + String userSessionId = entry.getKey(); + tx.remove(offlineSessionCache, userSessionId); + // Propagate to persister + persister.removeUserSession(userSessionId, true); + + UserSessionEntity entity = (UserSessionEntity) entry.getValue(); + for (String clientSessionId : entity.getClientSessions()) { + tx.remove(offlineSessionCache, clientSessionId); + } } - // Remove offline client sessions of expired offline user sessions + // Remove expired offline client sessions map = new MapReduceTask(offlineSessionCache) - .mappedWith(new ClientSessionsOfUserSessionMapper(realm.getId(), new HashSet<>(map.keySet())).emitKey()) + .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredOffline).emitKey()) .reducedWith(new FirstResultReducer()) .execute(); - for (String id : map.keySet()) { - tx.remove(offlineSessionCache, id); + for (String clientSessionId : map.keySet()) { + tx.remove(offlineSessionCache, clientSessionId); + persister.removeClientSession(clientSessionId, true); } } @@ -504,7 +511,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { tx.remove(cache, userSessionId); - // TODO: We can retrieve it from userSessionEntity directly + // TODO: Isn't more effective to retrieve from userSessionEntity directly? Map map = new MapReduceTask(cache) .mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey()) .reducedWith(new FirstResultReducer()) @@ -554,27 +561,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { - UserSessionEntity entity = new UserSessionEntity(); - entity.setId(userSession.getId()); - entity.setRealm(userSession.getRealm().getId()); - - entity.setAuthMethod(userSession.getAuthMethod()); - entity.setBrokerSessionId(userSession.getBrokerSessionId()); - entity.setBrokerUserId(userSession.getBrokerUserId()); - entity.setIpAddress(userSession.getIpAddress()); - entity.setLoginUsername(userSession.getLoginUsername()); - entity.setNotes(userSession.getNotes()); - entity.setRememberMe(userSession.isRememberMe()); - entity.setState(userSession.getState()); - entity.setUser(userSession.getUser().getId()); + UserSessionAdapter offlineUserSession = importUserSession(userSession, true); // started and lastSessionRefresh set to current time int currentTime = Time.currentTime(); - entity.setStarted(currentTime); - entity.setLastSessionRefresh(currentTime); + offlineUserSession.getEntity().setStarted(currentTime); + offlineUserSession.setLastSessionRefresh(currentTime); - tx.put(offlineSessionCache, userSession.getId(), entity); - return wrap(userSession.getRealm(), entity, true); + return offlineUserSession; } @Override @@ -589,26 +583,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) { - ClientSessionEntity entity = new ClientSessionEntity(); - entity.setId(clientSession.getId()); - entity.setRealm(clientSession.getRealm().getId()); + ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true); - entity.setAction(clientSession.getAction()); - entity.setAuthenticatorStatus(clientSession.getExecutionStatus()); - entity.setAuthMethod(clientSession.getAuthMethod()); - if (clientSession.getAuthenticatedUser() != null) { - entity.setAuthUserId(clientSession.getAuthenticatedUser().getId()); - } - entity.setClient(clientSession.getClient().getId()); - entity.setNotes(clientSession.getNotes()); - entity.setProtocolMappers(clientSession.getProtocolMappers()); - entity.setRedirectUri(clientSession.getRedirectUri()); - entity.setRoles(clientSession.getRoles()); - entity.setTimestamp(clientSession.getTimestamp()); - entity.setUserSessionNotes(clientSession.getUserSessionNotes()); + // update timestamp to current time + offlineClientSession.setTimestamp(Time.currentTime()); - tx.put(offlineSessionCache, clientSession.getId(), entity); - return wrap(clientSession.getRealm(), entity, true); + return offlineClientSession; } @Override @@ -653,6 +633,55 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return getUserSessions(realm, client, first, max, true); } + @Override + public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) { + UserSessionEntity entity = new UserSessionEntity(); + entity.setId(userSession.getId()); + entity.setRealm(userSession.getRealm().getId()); + + entity.setAuthMethod(userSession.getAuthMethod()); + entity.setBrokerSessionId(userSession.getBrokerSessionId()); + entity.setBrokerUserId(userSession.getBrokerUserId()); + entity.setIpAddress(userSession.getIpAddress()); + entity.setLoginUsername(userSession.getLoginUsername()); + entity.setNotes(userSession.getNotes()); + entity.setRememberMe(userSession.isRememberMe()); + entity.setState(userSession.getState()); + entity.setUser(userSession.getUser().getId()); + + entity.setStarted(userSession.getStarted()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + + Cache cache = getCache(offline); + tx.put(cache, userSession.getId(), entity); + return wrap(userSession.getRealm(), entity, offline); + } + + @Override + public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) { + ClientSessionEntity entity = new ClientSessionEntity(); + entity.setId(clientSession.getId()); + entity.setRealm(clientSession.getRealm().getId()); + + entity.setAction(clientSession.getAction()); + entity.setAuthenticatorStatus(clientSession.getExecutionStatus()); + entity.setAuthMethod(clientSession.getAuthMethod()); + if (clientSession.getAuthenticatedUser() != null) { + entity.setAuthUserId(clientSession.getAuthenticatedUser().getId()); + } + entity.setClient(clientSession.getClient().getId()); + entity.setNotes(clientSession.getNotes()); + entity.setProtocolMappers(clientSession.getProtocolMappers()); + entity.setRedirectUri(clientSession.getRedirectUri()); + entity.setRoles(clientSession.getRoles()); + entity.setTimestamp(clientSession.getTimestamp()); + entity.setUserSessionNotes(clientSession.getUserSessionNotes()); + + Cache cache = getCache(offline); + tx.put(cache, clientSession.getId(), entity); + return wrap(clientSession.getRealm(), entity, offline); + } + class InfinispanKeycloakTransaction implements KeycloakTransaction { private boolean active; diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index c88e4901f8..1d7c279542 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -63,20 +63,12 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider if (compatMode) { compatProviderFactory = new MemUserSessionProviderFactory(); } - - log.debug("Clearing detached sessions from persistent storage"); - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - if (persister == null) { - throw new RuntimeException("userSessionPersister not configured. Please see the migration docs and upgrade your configuration"); - } else { - persister.clearDetachedUserSessions(); - } } }); // Max count of worker errors. Initialization will end with exception when this number is reached - int maxErrors = config.getInt("maxErrors", 50); + int maxErrors = config.getInt("maxErrors", 20); // Count of sessions to be computed in each segment int sessionsPerSegment = config.getInt("sessionsPerSegment", 100); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java index 6cbb1eb82c..23c1286c4d 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java @@ -318,7 +318,7 @@ public class MemUserSessionProvider implements UserSessionProvider { } } - // Remove expired offline sessions + // Remove expired offline user sessions itr = offlineUserSessions.values().iterator(); while (itr.hasNext()) { UserSessionEntity s = itr.next(); @@ -330,6 +330,18 @@ public class MemUserSessionProvider implements UserSessionProvider { persister.removeUserSession(s.getId(), true); } } + + // Remove expired offline client sessions + citr = offlineClientSessions.values().iterator(); + while (citr.hasNext()) { + ClientSessionEntity s = citr.next(); + if (s.getRealmId().equals(realm.getId()) && (s.getTimestamp() < Time.currentTime() - realm.getOfflineSessionIdleTimeout())) { + citr.remove(); + + // propagate to persister + persister.removeClientSession(s.getId(), true); + } + } } @Override @@ -423,6 +435,18 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { + UserSessionAdapter importedUserSession = importUserSession(userSession, true); + + // started and lastSessionRefresh set to current time + int currentTime = Time.currentTime(); + importedUserSession.getEntity().setStarted(currentTime); + importedUserSession.setLastSessionRefresh(currentTime); + + return importedUserSession; + } + + @Override + public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) { UserSessionEntity entity = new UserSessionEntity(); entity.setId(userSession.getId()); entity.setRealm(userSession.getRealm().getId()); @@ -439,12 +463,11 @@ public class MemUserSessionProvider implements UserSessionProvider { entity.setState(userSession.getState()); entity.setUser(userSession.getUser().getId()); - // started and lastSessionRefresh set to current time - int currentTime = Time.currentTime(); - entity.setStarted(currentTime); - entity.setLastSessionRefresh(currentTime); + entity.setStarted(userSession.getStarted()); + entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); - offlineUserSessions.put(userSession.getId(), entity); + ConcurrentHashMap sessionsMap = offline ? offlineUserSessions : userSessions; + sessionsMap.put(userSession.getId(), entity); return new UserSessionAdapter(session, this, userSession.getRealm(), entity); } @@ -469,6 +492,17 @@ public class MemUserSessionProvider implements UserSessionProvider { @Override public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) { + ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true); + + // update timestamp to current time + offlineClientSession.setTimestamp(Time.currentTime()); + + return offlineClientSession; + } + + @Override + public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) { + ClientSessionEntity entity = new ClientSessionEntity(); entity.setId(clientSession.getId()); entity.setRealmId(clientSession.getRealm().getId()); @@ -492,7 +526,8 @@ public class MemUserSessionProvider implements UserSessionProvider { entity.getUserSessionNotes().putAll(clientSession.getUserSessionNotes()); } - offlineClientSessions.put(clientSession.getId(), entity); + ConcurrentHashMap clientSessionsMap = offline ? offlineClientSessions : clientSessions; + clientSessionsMap.put(clientSession.getId(), entity); return new ClientSessionAdapter(session, this, clientSession.getRealm(), entity); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java index 450bbe14b2..cb5a7e74f7 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java @@ -22,11 +22,23 @@ public class SimpleUserSessionInitializer { } public void loadPersistentSessions() { + // Rather use separate transactions for update and loading + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + sessionLoader.init(session); + } + + }); + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override public void run(KeycloakSession session) { int count = sessionLoader.getSessionsCount(session); + for (int i=0 ; i<=count ; i+=sessionsPerSegment) { sessionLoader.loadSessions(session, i, sessionsPerSegment); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java index 0d038bd6a2..89f2d4f56a 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java @@ -82,8 +82,8 @@ public class InfinispanUserSessionInitializer { private boolean isFinished() { - InitializerState stateEntity = (InitializerState) cache.get(stateKey); - return stateEntity != null && stateEntity.isFinished(); + InitializerState state = (InitializerState) cache.get(stateKey); + return state != null && state.isFinished(); } @@ -92,6 +92,16 @@ public class InfinispanUserSessionInitializer { if (state == null) { final int[] count = new int[1]; + // Rather use separate transactions for update and counting + + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + sessionLoader.init(session); + } + + }); + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override public void run(KeycloakSession session) { @@ -133,7 +143,7 @@ public class InfinispanUserSessionInitializer { } - // Just coordinator is supposed to run this + // Just coordinator will run this private void startLoading() { InitializerState state = getOrCreateInitializerState(); @@ -196,7 +206,7 @@ public class InfinispanUserSessionInitializer { saveStateToCache(state); // TODO - log.info("New initializer state pushed. The state is: " + state.printState(false)); + log.info("New initializer state pushed. The state is: " + state.printState()); } } finally { distributedExecutorService.shutdown(); @@ -225,7 +235,7 @@ public class InfinispanUserSessionInitializer { @ViewChanged public void viewChanged(ViewChangedEvent event) { boolean isCoordinator = isCoordinator(); - // TODO: + // TODO: debug log.info("View Changed: is coordinator: " + isCoordinator); if (isCoordinator) { diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java index ccc6fd6a69..6066077779 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java @@ -26,8 +26,8 @@ public class InitializerState extends SessionEntity { segmentsCount = segmentsCount + 1; } - // TODO: trace - log.info(String.format("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount)); + // TODO: debug + log.infof("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount); for (int i=0 ; i finishedList = new ArrayList<>(); - List nonFinishedList = new ArrayList<>(); int size = segments.size(); for (int i=0 ; i sessions = persister.loadUserSessions(first, max, true); - // TODO: Each worker may have different time. Improve if needed... - int currentTime = Time.currentTime(); - for (UserSessionModel persistentSession : sessions) { - // Update and persist lastSessionRefresh time TODO: Do bulk DB update instead? - persistentSession.setLastSessionRefresh(currentTime); - persister.updateUserSession(persistentSession, true); - // Save to memory/infinispan - UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(persistentSession); + UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true); for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) { - ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(persistentClientSession); + ClientSessionModel offlineClientSession = session.sessions().importClientSession(persistentClientSession, true); offlineClientSession.setUserSession(offlineUserSession); } } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java index 5014147284..1fe977aead 100644 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java @@ -9,6 +9,8 @@ import org.keycloak.models.KeycloakSession; */ public interface SessionLoader extends Serializable { + void init(KeycloakSession session); + int getSessionsCount(KeycloakSession session); boolean loadSessions(KeycloakSession session, int first, int max); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index f4de6656cb..a2acde95c6 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -172,14 +172,16 @@ public class TokenManager { int currentTime = Time.currentTime(); - if (realm.isRevokeRefreshToken() && !refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) { - if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp()) { + if (realm.isRevokeRefreshToken()) { + int serverStartupTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000); + + if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (serverStartupTime != validation.clientSession.getTimestamp())) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token"); } - validation.clientSession.setTimestamp(currentTime); } + validation.clientSession.setTimestamp(currentTime); validation.userSession.setLastSessionRefresh(currentTime); AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession) diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index c5be21be10..08699e07c8 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -28,6 +28,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { private Map, Map> factoriesMap = new HashMap, Map>(); protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); + // TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps protected long serverStartupTimestamp; @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 46e2a096a3..0c90af9371 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -7,6 +7,7 @@ import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; @@ -433,6 +434,15 @@ public class ClientResource { List userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults); for (UserSessionModel userSession : userSessions) { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); + + // Update lastSessionRefresh with the timestamp from clientSession + for (ClientSessionModel clientSession : userSession.getClientSessions()) { + if (client.getId().equals(clientSession.getClient().getId())) { + rep.setLastAccess(Time.toMillis(clientSession.getTimestamp())); + break; + } + } + sessions.add(rep); } return sessions; 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 4c8796d431..bd7924b955 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 @@ -79,6 +79,7 @@ import org.keycloak.models.UsernameLoginFailureModel; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.AccountService; +import org.keycloak.util.Time; /** * Base resource for managing users @@ -373,6 +374,15 @@ public class UsersResource { List reps = new ArrayList(); for (UserSessionModel session : sessions) { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); + + // Update lastSessionRefresh with the timestamp from clientSession + for (ClientSessionModel clientSession : session.getClientSessions()) { + if (clientId.equals(clientSession.getClient().getId())) { + rep.setLastAccess(Time.toMillis(clientSession.getTimestamp())); + break; + } + } + reps.add(rep); } return reps; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java index 9e0358ff09..6508cfb88d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -65,6 +65,9 @@ public class UserSessionInitializerTest { resetSession(); // Create and persist offline sessions + int started = Time.currentTime(); + int serverStartTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000); + for (UserSessionModel origSession : origSessions) { UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId()); for (ClientSessionModel clientSession : userSession.getClientSessions()) { @@ -88,32 +91,23 @@ public class UserSessionInitializerTest { Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, testApp)); Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, thirdparty)); - int started = Time.currentTime(); + // Load sessions from persister into infinispan/memory + UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class); + userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 1, 2); - try { - // Set some offset to ensure lastSessionRefresh will be updated - Time.setOffset(10); + resetSession(); - // Load sessions from persister into infinispan/memory - UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class); - userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 10, 2); + // Assert sessions are in + testApp = realm.getClientByClientId("test-app"); + Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp)); + Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty)); - resetSession(); + List loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10); + UserSessionProviderTest.assertSessions(loadedSessions, origSessions); - // Assert sessions are in - testApp = realm.getClientByClientId("test-app"); - Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp)); - Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty)); - - List loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10); - UserSessionProviderTest.assertSessions(loadedSessions, origSessions); - - UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started+10, "test-app", "third-party"); - UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app"); - UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started+10, "test-app"); - } finally { - Time.setOffset(0); - } + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, serverStartTime, "test-app", "third-party"); + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, serverStartTime, "test-app"); + UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, serverStartTime, "test-app"); } private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java index 53480aa05f..4edf9516b5 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -93,6 +93,52 @@ public class UserSessionPersisterProviderTest { assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app"); } + @Test + public void testUpdateTimestamps() { + // Create some sessions in infinispan + int started = Time.currentTime(); + UserSessionModel[] origSessions = createSessions(); + + resetSession(); + + // Persist 3 created userSessions and clientSessions as offline + ClientModel testApp = realm.getClientByClientId("test-app"); + List userSessions = session.sessions().getUserSessions(realm, testApp); + for (UserSessionModel userSession : userSessions) { + persistUserSession(userSession, true); + } + + // Persist 1 online session + UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId()); + persistUserSession(userSession, false); + + resetSession(); + + // update timestamps + int newTime = started + 50; + persister.updateAllTimestamps(newTime); + + // Assert online session + List loadedSessions = loadPersistedSessionsPaginated(false, 1, 1, 1); + Assert.assertEquals(2, assertTimestampsUpdated(loadedSessions, newTime)); + + // Assert offline sessions + loadedSessions = loadPersistedSessionsPaginated(true, 2, 2, 3); + Assert.assertEquals(4, assertTimestampsUpdated(loadedSessions, newTime)); + } + + private int assertTimestampsUpdated(List loadedSessions, int expectedTime) { + int clientSessionsCount = 0; + for (UserSessionModel loadedSession : loadedSessions) { + Assert.assertEquals(expectedTime, loadedSession.getLastSessionRefresh()); + for (ClientSessionModel clientSession : loadedSession.getClientSessions()) { + Assert.assertEquals(expectedTime, clientSession.getTimestamp()); + clientSessionsCount++; + } + } + return clientSessionsCount; + } + @Test public void testUpdateAndRemove() { // Create some sessions in infinispan @@ -245,11 +291,6 @@ public class UserSessionPersisterProviderTest { realmMgr.removeRealm(realmMgr.getRealm("foo")); } -// @Test -// public void testExpiredUserSessions() { -// -// } - private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index 57c99f8ad5..dc15b44574 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -327,30 +327,42 @@ public class UserSessionProviderOfflineTest { Assert.assertNotNull(session.sessions().getOfflineClientSession(realm, clientSession.getId())); } + UserSessionModel session1 = session.sessions().getOfflineUserSession(realm, origSessions[1].getId()); + Assert.assertEquals(1, session1.getClientSessions().size()); + ClientSessionModel cls1 = session1.getClientSessions().get(0); + // sessions are in persister too Assert.assertEquals(3, persister.getUserSessionsCount(true)); // Set lastSessionRefresh to session[0] to 0 session0.setLastSessionRefresh(0); + // Set timestamp to cls1 to 0 + cls1.setTimestamp(0); + resetSession(); session.sessions().removeExpiredUserSessions(realm); resetSession(); - // assert sessions not found now + // assert session0 not found now Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId())); for (String clientSession : clientSessions) { Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId())); offlineSessions.remove(clientSession); } - // Assert other offline sessions still found + // Assert cls1 not found too for (Map.Entry entry : offlineSessions.entrySet()) { - Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null); + String userSessionId = entry.getValue(); + if (userSessionId.equals(session1.getId())) { + Assert.assertFalse(sessionManager.findOfflineClientSession(realm, entry.getKey(), userSessionId) != null); + } else { + Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), userSessionId) != null); + } } - Assert.assertEquals(2, persister.getUserSessionsCount(true)); + Assert.assertEquals(1, persister.getUserSessionsCount(true)); // Expire everything and assert nothing found Time.setOffset(3000000); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index b7596a073d..4a86fec596 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -332,6 +332,71 @@ public class OfflineTokenTest { Assert.assertEquals(0, offlineToken.getExpiration()); testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); + + // Assert same token can be refreshed again + testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); + } + + @Test + public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() throws Exception { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setRevokeRefreshToken(true); + } + + }); + + oauth.scope(OAuth2Constants.OFFLINE_ACCESS); + oauth.clientId("offline-client"); + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password"); + + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String offlineTokenString = tokenResponse.getRefreshToken(); + RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString); + + events.expectLogin() + .client("offline-client") + .user(userId) + .session(token.getSessionState()) + .detail(Details.RESPONSE_TYPE, "token") + .detail(Details.TOKEN_ID, token.getId()) + .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId()) + .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE) + .detail(Details.USERNAME, "test-user@localhost") + .removeDetail(Details.CODE_ID) + .removeDetail(Details.REDIRECT_URI) + .removeDetail(Details.CONSENT) + .assertEvent(); + + Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType()); + Assert.assertEquals(0, offlineToken.getExpiration()); + + String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId); + RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2); + + // Assert second refresh with same refresh token will fail + OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1"); + Assert.assertEquals(400, response.getStatusCode()); + events.expectRefresh(offlineToken.getId(), token.getSessionState()) + .client("offline-client") + .error(Errors.INVALID_TOKEN) + .user(userId) + .clearDetails() + .assertEvent(); + + // Refresh with new refreshToken is successful now + testRefreshWithOfflineToken(token, offlineToken2, offlineTokenString2, token.getSessionState(), userId); + + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setRevokeRefreshToken(false); + } + + }); } @Test From 20f18eec1594b0701050dca7ae31c7ffb90dc86f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 16:30:21 -0400 Subject: [PATCH 08/13] support browser refresh --- .../keycloak/models/ClientSessionModel.java | 3 +- .../AuthenticationProcessor.java | 38 +++++- .../DefaultAuthenticationFlow.java | 14 +- .../FormAuthenticationFlow.java | 4 +- .../authentication/RequiredActionContext.java | 2 +- .../RequiredActionContextResult.java | 20 +-- .../resetcred/ResetCredentialEmail.java | 2 + .../requiredactions/VerifyEmail.java | 6 +- .../oidc/endpoints/AuthorizationEndpoint.java | 66 ++++++---- .../managers/AuthenticationManager.java | 2 + .../services/managers/ClientSessionCode.java | 9 +- .../resources/IdentityBrokerService.java | 3 +- .../resources/LoginActionsService.java | 124 ++++++++---------- .../federation/AbstractKerberosTest.java | 8 ++ .../testsuite/perf/AccessTokenPerfTest.java | 8 ++ 15 files changed, 194 insertions(+), 115 deletions(-) diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java index b4f638ddff..293f38b97b 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -98,7 +98,8 @@ public interface ClientSessionModel { SOCIAL_CALLBACK, LOGGED_OUT, RESET_CREDENTIALS, - EXECUTE_ACTIONS + EXECUTE_ACTIONS, + REQUIRED_ACTIONS } public enum ExecutionStatus { diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index b6b3272ca6..35b2ab94c8 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -55,6 +55,7 @@ public class AuthenticationProcessor { protected HttpRequest request; protected String flowId; protected String flowPath; + protected boolean browserFlow; /** * This could be an error message forwarded from another authenticator */ @@ -73,6 +74,15 @@ public class AuthenticationProcessor { public AuthenticationProcessor() { } + public boolean isBrowserFlow() { + return browserFlow; + } + + public AuthenticationProcessor setBrowserFlow(boolean browserFlow) { + this.browserFlow = browserFlow; + return this; + } + public RealmModel getRealm() { return realm; } @@ -645,6 +655,31 @@ public class AuthenticationProcessor { } } + public Response createSuccessRedirect() { + // redirect to non-action url so browser refresh button works without reposting past data + String code = generateCode(); + + URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo()) + .path(flowPath) + .queryParam(OAuth2Constants.CODE, code).build(getRealm().getName()); + return Response.status(302).location(redirect).build(); + + } + + public static Response createRequiredActionRedirect(RealmModel realm, ClientSessionModel clientSession, UriInfo uriInfo) { + + // redirect to non-action url so browser refresh button works without reposting past data + ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); + accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name()); + clientSession.setTimestamp(Time.currentTime()); + + URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo) + .path(LoginActionsService.REQUIRED_ACTION) + .queryParam(OAuth2Constants.CODE, accessCode.getCode()).build(realm.getName()); + return Response.status(302).location(redirect).build(); + + } + public static void resetFlow(ClientSessionModel clientSession) { clientSession.setTimestamp(Time.currentTime()); clientSession.setAuthenticatedUser(null); @@ -778,7 +813,8 @@ public class AuthenticationProcessor { protected Response authenticationComplete() { attachSession(); - return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event); + return createRequiredActionRedirect(realm, clientSession, uriInfo); + //return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event); } diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index d9ca0b094f..e4b6dc2e96 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -1,14 +1,19 @@ package org.keycloak.authentication; +import org.keycloak.OAuth2Constants; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.util.Time; import org.omg.PortableInterceptor.SUCCESSFUL; import static org.keycloak.authentication.FlowStatus.*; import javax.ws.rs.core.Response; +import java.net.URI; import java.util.Iterator; import java.util.List; @@ -64,7 +69,14 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); authenticator.action(result); Response response = processResult(result); - if (response == null) return processFlow(); + if (response == null) { + if (result.status == SUCCESS && processor.isBrowserFlow()) { + // redirect to a non-action URL so browser refresh works without reposting. + return processor.createSuccessRedirect(); + } else { + return processFlow(); + } + } else return response; } } diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java index c94e8cbc08..9fb9b1ba2a 100755 --- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java @@ -219,8 +219,10 @@ public class FormAuthenticationFlow implements AuthenticationFlow { action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser()); } - return null; + processor.getClientSession().setExecutionStatus(actionExecution, ClientSessionModel.ExecutionStatus.SUCCESS); + // redirect to no execution so browser refresh button works without reposting past data + return processor.createSuccessRedirect(); } public URI getActionUrl(String executionId, String code) { diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java index 9b650c870a..cc37f89e91 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java @@ -86,7 +86,7 @@ public interface RequiredActionContext { * * @return */ - String generateAccessCode(String action); + String generateCode(); Status getStatus(); diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index b9e06f8da4..f2ada5d11c 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -12,6 +12,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.util.Time; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -92,13 +93,6 @@ public class RequiredActionContextResult implements RequiredActionContext { return httpRequest; } - @Override - public String generateAccessCode(String action) { - ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession()); - code.setAction(action); - return code.getCode(); - } - @Override public Status getStatus() { return status; @@ -135,16 +129,24 @@ public class RequiredActionContextResult implements RequiredActionContext { .build(getRealm().getName()); } + @Override + public String generateCode() { + ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession()); + clientSession.setTimestamp(Time.currentTime()); + return accessCode.getCode(); + } + + @Override public URI getActionUrl() { - String accessCode = generateAccessCode(factory.getId()); + String accessCode = generateCode(); return getActionUrl(accessCode); } @Override public LoginFormsProvider form() { - String accessCode = generateAccessCode(factory.getId()); + String accessCode = generateCode(); URI action = getActionUrl(accessCode); LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class) .setUser(getUser()) diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java index 24ff0851d1..87493609cc 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java @@ -105,6 +105,8 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge); return; } + // We now know email is valid, so set it to valid. + context.getUser().setEmailVerified(true); context.success(); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java index 11755b26f9..1a593cb867 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java @@ -36,9 +36,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor } @Override public void requiredActionChallenge(RequiredActionContext context) { - // if this is EXECUTE_ACTIONS we know that the email sent is valid so we can verify it automatically - if (context.getClientSession().getNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name()) != null) { - context.getUser().setEmailVerified(true); + if (context.getUser().isEmailVerified()) { context.success(); return; } @@ -52,7 +50,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId()); LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) - .setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.VERIFY_EMAIL.name())) + .setClientSessionCode(context.generateCode()) .setUser(context.getUser()); Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); context.challenge(challenge); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 7c51b74df0..da13ae6334 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -22,10 +22,10 @@ import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.services.ErrorPageException; +import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; -import org.keycloak.services.Urls; import org.keycloak.services.resources.LoginActionsService; import javax.ws.rs.GET; @@ -174,7 +174,7 @@ public class AuthorizationEndpoint { private void checkClient() { if (clientId == null) { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM ); + throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM); } event.client(clientId); @@ -182,12 +182,12 @@ public class AuthorizationEndpoint { client = realm.getClientByClientId(clientId); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); - throw new ErrorPageException(session, Messages.CLIENT_NOT_FOUND ); + throw new ErrorPageException(session, Messages.CLIENT_NOT_FOUND); } if (client.isBearerOnly()) { event.error(Errors.NOT_ALLOWED); - throw new ErrorPageException(session, Messages.BEARER_ONLY ); + throw new ErrorPageException(session, Messages.BEARER_ONLY); } if (client.isDirectGrantsOnly()) { @@ -204,7 +204,7 @@ public class AuthorizationEndpoint { responseType = legacyResponseType; } else { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM ); + throw new ErrorPageException(session, Messages.MISSING_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM); } } @@ -216,7 +216,7 @@ public class AuthorizationEndpoint { } } else { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM ); + throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM); } } @@ -280,29 +280,43 @@ public class AuthorizationEndpoint { String flowId = flow.getId(); AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.AUTHENTICATE_PATH); - Response challenge = null; - try { - challenge = processor.authenticateOnly(); + if (prompt != null && prompt.equals("none")) { + // OIDC prompt == NONE + // This means that client is just checking if the user is already completely logged in. + // + // here we cancel login if any authentication action or required action is required + Response challenge = null; + try { + challenge = processor.authenticateOnly(); + if (challenge == null) { + challenge = processor.attachSessionExecutionRequiredActions(); + } + } catch (Exception e) { + return processor.handleBrowserException(e); + } + + if (challenge != null) { + if (processor.isUserSessionCreated()) { + session.sessions().removeUserSession(realm, processor.getUserSession()); + } + OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event); + return oauth.cancelLogin(clientSession); + } + if (challenge == null) { - challenge = processor.attachSessionExecutionRequiredActions(); + return processor.finishAuthentication(); + } else { + RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); + return challenge; } - } catch (Exception e) { - return processor.handleBrowserException(e); - } - - if (challenge != null && prompt != null && prompt.equals("none")) { - if (processor.isUserSessionCreated()) { - session.sessions().removeUserSession(realm, processor.getUserSession()); - } - OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event); - return oauth.cancelLogin(clientSession); - } - - if (challenge == null) { - return processor.finishAuthentication(); } else { - RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); - return challenge; + try { + RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession); + return processor.authenticate(); + } catch (Exception e) { + return processor.handleBrowserException(e); + } + } } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 332d71d136..f31d1ff8c8 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -68,6 +68,7 @@ public class AuthenticationManager { public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION"; public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME"; public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL"; + public static final String CURRENT_REQUIRED_ACTION = "CURRENT_REQUIRED_ACTION"; protected BruteForceProtector protector; @@ -525,6 +526,7 @@ public class AuthenticationManager { return protocol.consentDenied(context.getClientSession()); } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { + clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId()); return context.getChallenge(); } else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) { diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 6ebe4f32e2..672bb2ad71 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -73,8 +73,12 @@ public class ClientSessionCode { } public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm) { + ParseResult result = new ParseResult(); + if (code == null) { + result.illegalHash = true; + return result; + } try { - ParseResult result = new ParseResult(); String[] parts = code.split("\\."); String id = parts[1]; @@ -93,7 +97,8 @@ public class ClientSessionCode { result.code = new ClientSessionCode(realm, clientSession); return result; } catch (RuntimeException e) { - return null; + result.illegalHash = true; + return result; } } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 9bbfcf5208..e6be392f3a 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -322,8 +322,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal LOGGER.debugf("Performing local authentication for user [%s].", federatedUser); } - return AuthenticationManager.nextActionAfterAuthentication(this.session, userSession, clientSession, this.clientConnection, this.request, - this.uriInfo, event); + return AuthenticationProcessor.createRequiredActionRedirect(realmModel, clientSession, uriInfo); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index b6e4ce300e..6e525341ce 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -85,6 +85,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; +import java.net.URI; import java.util.List; import java.util.concurrent.TimeUnit; @@ -99,6 +100,7 @@ public class LoginActionsService { public static final String AUTHENTICATE_PATH = "authenticate"; public static final String REGISTRATION_PATH = "registration"; public static final String RESET_CREDENTIALS_PATH = "reset-credentials"; + public static final String REQUIRED_ACTION = "required-action"; private RealmModel realm; @@ -167,12 +169,22 @@ public class LoginActionsService { boolean verifyCode(String code, String requiredAction) { if (!verifyCode(code)) { return false; - } else if (!clientCode.isValidAction(requiredAction)) { + } + if (!verifyAction(requiredAction)) { + return false; + } else { + return true; + } + } + + public boolean verifyAction(String requiredAction) { + if (!clientCode.isValidAction(requiredAction)) { event.client(clientCode.getClientSession().getClient()); event.error(Errors.INVALID_CODE); response = ErrorPage.error(session, Messages.INVALID_CODE); return false; - } else if (!clientCode.isActionActive(requiredAction)) { + } + if (!clientCode.isActionActive(requiredAction)) { event.client(clientCode.getClientSession().getClient()); event.clone().error(Errors.EXPIRED_CODE); if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) { @@ -182,35 +194,8 @@ public class LoginActionsService { } response = ErrorPage.error(session, Messages.EXPIRED_CODE); return false; - } else { - return true; - } - } - - boolean verifyCode(String code, String requiredAction, String alternativeRequiredAction) { - if (!verifyCode(code)) { - return false; - } else if (!(clientCode.isValidAction(requiredAction) || clientCode.isValidAction(alternativeRequiredAction))) { - event.client(clientCode.getClientSession().getClient()); - event.error(Errors.INVALID_CODE); - response = ErrorPage.error(session, Messages.INVALID_CODE); - return false; - } else if (!(clientCode.isActionActive(requiredAction) || clientCode.isActionActive(alternativeRequiredAction))) { - event.client(clientCode.getClientSession().getClient()); - event.clone().error(Errors.EXPIRED_CODE); - if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) { - AuthenticationProcessor.resetFlow(clientCode.getClientSession()); - response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT); - } else { - if (clientCode.getClientSession().getUserSession() == null) { - session.sessions().removeClientSession(realm, clientCode.getClientSession()); - } - response = ErrorPage.error(session, Messages.EXPIRED_CODE); - } - return false; - } else { - return true; } + return true; } public boolean verifyCode(String code) { @@ -298,6 +283,7 @@ public class LoginActionsService { AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setClientSession(clientSession) .setFlowPath(flowPath) + .setBrowserFlow(true) .setFlowId(flow.getId()) .setConnection(clientConnection) .setEventBuilder(event) @@ -559,11 +545,17 @@ public class LoginActionsService { event.event(EventType.VERIFY_EMAIL); if (key != null) { Checks checks = new Checks(); - if (!checks.verifyCode(key, ClientSessionModel.Action.VERIFY_EMAIL.name())) { + if (!checks.verifyCode(key, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; ClientSessionModel clientSession = accessCode.getClientSession(); + if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) { + logger.error("required action doesn't match current required action"); + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + UserSessionModel userSession = clientSession.getUserSession(); UserModel user = userSession.getUser(); initEvent(clientSession); @@ -583,10 +575,10 @@ public class LoginActionsService { event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN); - return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); + return AuthenticationProcessor.createRequiredActionRedirect(realm, clientSession, uriInfo); } else { Checks checks = new Checks(); - if (!checks.verifyCode(code, ClientSessionModel.Action.VERIFY_EMAIL.name())) { + if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -619,9 +611,11 @@ public class LoginActionsService { return checks.response; } ClientSessionModel clientSession = checks.clientCode.getClientSession(); + // verify user email as we know it is valid as this entry point would never have gotten here. + clientSession.getUserSession().getUser().setEmailVerified(true); clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true"); - return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event); + return AuthenticationProcessor.createRequiredActionRedirect(realm, clientSession, uriInfo); } else { event.error(Errors.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_CODE); @@ -658,7 +652,7 @@ public class LoginActionsService { } } - @Path("required-action") + @Path(REQUIRED_ACTION) @POST public Response requiredActionPOST(@QueryParam("code") final String code, @QueryParam("action") String action) { @@ -668,7 +662,7 @@ public class LoginActionsService { } - @Path("required-action") + @Path(REQUIRED_ACTION) @GET public Response requiredActionGET(@QueryParam("code") final String code, @QueryParam("action") String action) { @@ -678,22 +672,8 @@ public class LoginActionsService { public Response processRequireAction(final String code, String action) { event.event(EventType.CUSTOM_REQUIRED_ACTION); event.detail(Details.CUSTOM_REQUIRED_ACTION, action); - if (action == null) { - logger.error("required action query param was null"); - event.error(Errors.INVALID_CODE); - throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); - - } - - RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action); - if (factory == null) { - logger.error("required action provider was null"); - event.error(Errors.INVALID_CODE); - throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); - } - RequiredActionProvider provider = factory.create(session); Checks checks = new Checks(); - if (!checks.verifyCode(code, action)) { + if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { return checks.response; } final ClientSessionCode clientCode = checks.clientCode; @@ -704,24 +684,31 @@ public class LoginActionsService { event.error(Errors.USER_SESSION_NOT_FOUND); throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE)); } + if (action == null && clientSession.getUserSession() != null) { // do next required action only if user is already authenticated + initEvent(clientSession); + event.event(EventType.LOGIN); + return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event); + } + + if (!action.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) { + logger.error("required action doesn't match current required action"); + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + + RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action); + if (factory == null) { + logger.error("required action provider was null"); + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + RequiredActionProvider provider = factory.create(session); initEvent(clientSession); event.event(EventType.CUSTOM_REQUIRED_ACTION); RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), factory) { - @Override - public String generateAccessCode(String action) { - String clientSessionAction = clientSession.getAction(); - if (action.equals(clientSessionAction)) { - clientSession.setTimestamp(Time.currentTime()); - return code; - } - ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession()); - code.setAction(action); - return code.getCode(); - } - @Override public void ignore() { throw new RuntimeException("Cannot call ignore within processAction()"); @@ -729,13 +716,16 @@ public class LoginActionsService { }; provider.processAction(context); if (context.getStatus() == RequiredActionContext.Status.SUCCESS) { - event.clone().success(); + event.success(); // do both clientSession.removeRequiredAction(factory.getId()); clientSession.getUserSession().getUser().removeRequiredAction(factory.getId()); - event.event(EventType.LOGIN); - return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event); - } + // redirect to a generic code URI so that browser refresh will work + URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo) + .path(LoginActionsService.REQUIRED_ACTION) + .queryParam(OAuth2Constants.CODE, code).build(realm.getName()); + return Response.status(302).location(redirect).build(); + } if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { return context.getChallenge(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index 8eb05b64e1..294fdb5060 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -175,6 +175,7 @@ public abstract class AbstractKerberosTest { events.clear(); Response spnegoResponse = spnegoLogin("jduke", "theduke"); Assert.assertEquals(302, spnegoResponse.getStatus()); + String redirect = spnegoResponse.getLocation().toString(); events.expectLogin() .client("kerberos-app") .user(keycloakRule.getUser("test", "jduke").getId()) @@ -244,6 +245,13 @@ public abstract class AbstractKerberosTest { spnegoSchemeFactory.setCredentials(username, password); Response response = client.target(kcLoginPageLocation).request().get(); SpnegoAuthenticator.bypassChallengeJavascript = false; + if (response.getStatus() == 302) { + if (response.getLocation() == null) return response; + String uri = response.getLocation().toString(); + if (uri.contains("login-actions/required-action")) { + response = client.target(uri).request().get(); + } + } return response; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java index d5d7296049..9cabdb4189 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java @@ -172,6 +172,14 @@ public class AccessTokenPerfTest { URI uri = null; Assert.assertEquals(302, response.getStatus()); uri = response.getLocation(); + if (response.getStatus() == 302) { + while (uri.toString().contains("login-actions/")) { + response = client.target(uri).request().get(); + Assert.assertEquals(302, response.getStatus()); + uri = response.getLocation(); + } + } + for (String header : response.getHeaders().keySet()) { for (Object value : response.getHeaders().get(header)) { System.out.println(header + ": " + value); From 9cf4186eb29f187e413c4a31d8b1e0991eefc4ef Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 17:56:51 -0400 Subject: [PATCH 09/13] fix arquillian test --- .../keycloak/testsuite/account/ResetCredentialsTest.java | 2 +- .../src/test/java/org/keycloak/testsuite/Jetty8Test.java | 8 +++++--- .../test/java/org/keycloak/testsuite/JettySamlTest.java | 8 +++++--- .../src/test/java/org/keycloak/testsuite/Jetty9Test.java | 8 +++++--- .../test/java/org/keycloak/testsuite/JettySamlTest.java | 8 +++++--- .../src/test/java/org/keycloak/testsuite/Jetty9Test.java | 7 +++++-- .../test/java/org/keycloak/testsuite/JettySamlTest.java | 7 +++++-- 7 files changed, 31 insertions(+), 17 deletions(-) mode change 100644 => 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java old mode 100644 new mode 100755 index 95bff4121d..68edaa1710 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResetCredentialsTest.java @@ -91,7 +91,7 @@ public class ResetCredentialsTest extends AbstractAccountManagementTest { log.info("navigating to " + url); driver.navigate().to(url); - assertCurrentUrlStartsWith(testRealmResetCredentialsPage); + //assertCurrentUrlStartsWith(testRealmResetCredentialsPage); testRealmResetCredentialsPage.updatePassword("newPassword"); assertCurrentUrlStartsWith(testRealmAccountManagementPage); testRealmAccountManagementPage.signOut(); diff --git a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/Jetty8Test.java b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/Jetty8Test.java index a2e4b42b6a..d158555756 100755 --- a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/Jetty8Test.java +++ b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/Jetty8Test.java @@ -85,9 +85,11 @@ public class Jetty8Test { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); - Thread.sleep(1000); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Rule diff --git a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java index 916d855e43..1506b0ce6a 100755 --- a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java +++ b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java @@ -97,9 +97,11 @@ public class JettySamlTest { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); - Thread.sleep(1000); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Test diff --git a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/Jetty9Test.java b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/Jetty9Test.java index fcf75ca465..402779b977 100755 --- a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/Jetty9Test.java +++ b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/Jetty9Test.java @@ -85,9 +85,11 @@ public class Jetty9Test { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); - Thread.sleep(1000); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Rule diff --git a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java index c6092b4f8c..e71887e203 100755 --- a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java +++ b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java @@ -96,9 +96,11 @@ public class JettySamlTest { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); - Thread.sleep(1000); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Test diff --git a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/Jetty9Test.java b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/Jetty9Test.java index 037769f9d0..15ee763eed 100755 --- a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/Jetty9Test.java +++ b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/Jetty9Test.java @@ -85,8 +85,11 @@ public class Jetty9Test { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Rule diff --git a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java index 96709ad20d..e71887e203 100755 --- a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java +++ b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java @@ -96,8 +96,11 @@ public class JettySamlTest { @AfterClass public static void shutdownJetty() throws Exception { - server.stop(); - server.destroy(); + try { + server.stop(); + server.destroy(); + Thread.sleep(100); + } catch (Exception e) {} } @Test From 181fdeb0d04a907e0b3a790202e541e65d48d831 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 18:54:57 -0400 Subject: [PATCH 10/13] KEYCLOAK-1960 --- .../admin/client/resource/UserResource.java | 8 ++++++ .../resources/admin/UsersResource.java | 27 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index e21ee7999c..d9b90d98c4 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -46,6 +46,14 @@ public interface UserResource { @Path("reset-password") public void resetPassword(CredentialRepresentation credentialRepresentation); + @PUT + @Path("reset-password-email") + public void resetPasswordEmail(); + + @PUT + @Path("reset-password-email") + public void resetPasswordEmail(@QueryParam("client_id") String clientId); + @PUT @Path("execute-actions-email") public void executeActionsEmail(List actions); 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 22ebbb5c11..9939d6e8d5 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 @@ -882,15 +882,38 @@ public class UsersResource { } /** - * Send a password-reset email to the user + * Send an email to the user with a link they can click to reset their password. + * The redirectUri and clientId parameters are optional. The default for the + * redirect is the account client. * - * An email contains a link the user can click to reset their password. + * @param id + * @param redirectUri redirect uri + * @param clientId client id + * @return + */ + @Path("{id}/reset-password-email") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public Response resetPasswordEmail(@PathParam("id") String id, + @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, + @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + return executeActionsEmail(id, redirectUri, clientId, actions); + } + + + /** + * Send a update account email to the user + * + * An email contains a link the user can click to perform a set of required actions. * The redirectUri and clientId parameters are optional. The default for the * redirect is the account client. * * @param id User is * @param redirectUri Redirect uri * @param clientId Client id + * @param actions required actions the user needs to complete * @return */ @Path("{id}/execute-actions-email") From 235ffb2ff6371ccd864d7588e61ef4f81645b57e Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 18:56:56 -0400 Subject: [PATCH 11/13] KEYCLOAK-1960 --- .../keycloak/admin/client/resource/UserResource.java | 10 ++++++++++ .../services/resources/admin/UsersResource.java | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index d9b90d98c4..a2490b6120 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -46,12 +46,22 @@ public interface UserResource { @Path("reset-password") public void resetPassword(CredentialRepresentation credentialRepresentation); + /** + * Use executeActionsEmail and pass in the UPDATE_PASSWORD required action + * + */ @PUT @Path("reset-password-email") + @Deprecated public void resetPasswordEmail(); + /** + * Use executeActionsEmail and pass in the UPDATE_PASSWORD required action + * + */ @PUT @Path("reset-password-email") + @Deprecated public void resetPasswordEmail(@QueryParam("client_id") String clientId); @PUT 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 9939d6e8d5..9a47885ad8 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 @@ -886,11 +886,15 @@ public class UsersResource { * The redirectUri and clientId parameters are optional. The default for the * redirect is the account client. * + * This endpoint has been deprecated. Please use the execute-actions-email passing a list with + * UPDATE_PASSWORD within it. + * * @param id * @param redirectUri redirect uri * @param clientId client id * @return */ + @Deprecated @Path("{id}/reset-password-email") @PUT @Consumes(MediaType.APPLICATION_JSON) From 14426f26f2086c2db8d48699207504d17a507146 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 20:05:44 -0400 Subject: [PATCH 12/13] KEYCLOAK-1954 --- .../java/org/keycloak/broker/saml/SAMLIdentityProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index e28aa9713f..248c4de35b 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -228,7 +228,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider\n" + + "\n" + " \n" + " " + getConfig().getNameIDPolicyFormat() + "\n" + From 1e4b3fc3b6234d48be5a096390864ae86e438d30 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 Oct 2015 20:26:20 -0400 Subject: [PATCH 13/13] KEYCLOAK-1941 --- .../theme/base/admin/resources/js/controllers/clients.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 27a97941ea..e35e1c62d2 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -779,7 +779,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se } else if ($scope.client.attributes['saml.signature.algorithm'] == 'DSA_SHA1') { $scope.signatureAlgorithm = $scope.signatureAlgorithms[3]; } - if ($scope.client.attributes['saml_name_id_format'] == 'unspecified') { + if ($scope.client.attributes['saml_name_id_format'] == 'username') { $scope.nameIdFormat = $scope.nameIdFormats[0]; } else if ($scope.client.attributes['saml_name_id_format'] == 'email') { $scope.nameIdFormat = $scope.nameIdFormats[1];