From f8effaee58c2b4a143b91cb4e6822cf91118a0df Mon Sep 17 00:00:00 2001 From: Michael Gerber Date: Wed, 14 Oct 2015 13:46:59 +0200 Subject: [PATCH 01/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 2c460c3adc798690b2a09ab2257e72892fcb9a6b Mon Sep 17 00:00:00 2001 From: mhajas Date: Thu, 15 Oct 2015 12:44:27 +0200 Subject: [PATCH 02/13] Test password policies using REST --- .../page/authentication/PasswordPolicy.java | 10 ++-- .../authentication/PasswordPolicyTest.java | 47 +++++++++++++++---- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java index 3df1e679d6..5eb8f1b10f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java @@ -34,6 +34,8 @@ public class PasswordPolicy extends Authentication { public void addPolicy(PasswordPolicy.Type policy, String value) { waitGuiForElement(addPolicySelectElement); addPolicySelect.selectByVisibleText(policy.getName()); + + //addPolicySelect.selectByValue(policy.getName()); setPolicyValue(policy, value); primaryButton.click(); } @@ -82,10 +84,10 @@ public class PasswordPolicy extends Authentication { public enum Type { - HASH_ITERATIONS("Hash Iterations"), LENGTH("Length"), DIGITS("Digits"), LOWER_CASE("Lower Case"), - UPPER_CASE("Upper Case"), SPECIAL_CHARS("Special Chars"), NOT_USERNAME("Not Username"), - REGEX_PATTERN("Regex Pattern"), PASSWORD_HISTORY("Password History"), - FORCE_EXPIRED_PASSWORD_CHANGE("Force Expired Password Change"); + HASH_ITERATIONS("HashIterations"), LENGTH("Length"), DIGITS("Digits"), LOWER_CASE("LowerCase"), + UPPER_CASE("UpperCase"), SPECIAL_CHARS("SpecialChars"), NOT_USERNAME("NotUsername"), + REGEX_PATTERN("RegexPattern"), PASSWORD_HISTORY("PasswordHistory"), + FORCE_EXPIRED_PASSWORD_CHANGE("ForceExpiredPasswordChange"); private String name; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java index bb578ff81a..aecb315d1e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java @@ -21,11 +21,13 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.console.AbstractConsoleTest; import org.keycloak.testsuite.console.page.authentication.PasswordPolicy; import org.keycloak.testsuite.console.page.users.UserCredentials; -import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy.Type.*; +import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy.Type.HASH_ITERATIONS; +import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy.Type.REGEX_PATTERN; /** * @author Petr Mensik @@ -43,11 +45,11 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Before public void beforePasswordPolicyTest() { testUserCredentialsPage.setId(testUser.getId()); - passwordPolicyPage.navigateTo(); } @Test public void testAddAndRemovePolicy() { + passwordPolicyPage.navigateTo(); passwordPolicyPage.addPolicy(HASH_ITERATIONS, 5); passwordPolicyPage.removePolicy(HASH_ITERATIONS); assertFlashMessageSuccess(); @@ -55,6 +57,7 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testInvalidPolicyValues() { + passwordPolicyPage.navigateTo(); passwordPolicyPage.addPolicy(HASH_ITERATIONS, "asd"); assertFlashMessageDanger(); passwordPolicyPage.removePolicy(HASH_ITERATIONS); @@ -65,7 +68,10 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testLengthPolicy() { - passwordPolicyPage.addPolicy(LENGTH, 8); + + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("length(8) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("1234567"); @@ -77,7 +83,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testDigitsPolicy() { - passwordPolicyPage.addPolicy(DIGITS, 2); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("digits(2) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("invalidPassword1"); @@ -89,7 +97,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testLowerCasePolicy() { - passwordPolicyPage.addPolicy(LOWER_CASE, 2); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("lowerCase(2) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("iNVALIDPASSWORD"); @@ -101,7 +111,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testUpperCasePolicy() { - passwordPolicyPage.addPolicy(UPPER_CASE, 2); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("upperCase(2) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("Invalidpassword"); @@ -113,7 +125,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testSpecialCharsPolicy() { - passwordPolicyPage.addPolicy(SPECIAL_CHARS, 2); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("specialChars(2) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("invalidPassword*"); @@ -125,7 +139,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testNotUsernamePolicy() { - passwordPolicyPage.addPolicy(NOT_USERNAME); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("notUsername(1) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword(testUser.getUsername()); @@ -137,7 +153,16 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testRegexPatternsPolicy() { - passwordPolicyPage.addPolicy(REGEX_PATTERN, "^[A-Z]+#[a-z]{8}$"); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("regexPattern(^[A-Z]+#[a-z]{8}$) and "); + System.out.println(realm.getPasswordPolicy()); + testRealmResource().update(realm); + + /* try { + Thread.sleep(45000000); + } catch (InterruptedException e) { + e.printStackTrace(); + }*/ testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("invalidPassword"); @@ -149,7 +174,9 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testPasswordHistoryPolicy() { - passwordPolicyPage.addPolicy(PASSWORD_HISTORY, 2); + RealmRepresentation realm = testRealmResource().toRepresentation(); + realm.setPasswordPolicy("passwordHistory(2) and "); + testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("firstPassword"); From b413bcb41dfa3d83c8dacc7d11e7702e2fee936f Mon Sep 17 00:00:00 2001 From: mhajas Date: Thu, 15 Oct 2015 14:57:58 +0200 Subject: [PATCH 03/13] Unignored test --- .../console/authentication/PasswordPolicyTest.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java index aecb315d1e..cbb3dd5a06 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java @@ -19,7 +19,6 @@ package org.keycloak.testsuite.console.authentication; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.console.AbstractConsoleTest; @@ -33,7 +32,7 @@ import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy. * @author Petr Mensik * @author mhajas */ -@Ignore // FIXME still unstable +//@Ignore // FIXME still unstable public class PasswordPolicyTest extends AbstractConsoleTest { @Page @@ -62,7 +61,7 @@ public class PasswordPolicyTest extends AbstractConsoleTest { assertFlashMessageDanger(); passwordPolicyPage.removePolicy(HASH_ITERATIONS); - passwordPolicyPage.addPolicy(REGEX_PATTERN, "^[A-Z]{8,5}"); + passwordPolicyPage.addPolicy(REGEX_PATTERN, "(["); assertFlashMessageDanger(); } @@ -158,12 +157,6 @@ public class PasswordPolicyTest extends AbstractConsoleTest { System.out.println(realm.getPasswordPolicy()); testRealmResource().update(realm); - /* try { - Thread.sleep(45000000); - } catch (InterruptedException e) { - e.printStackTrace(); - }*/ - testUserCredentialsPage.navigateTo(); testUserCredentialsPage.resetPassword("invalidPassword"); assertFlashMessageDanger(); From 802a39b1ce2f6d9620f2cfa1d179111896a2332f Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 14 Oct 2015 17:45:46 +0200 Subject: [PATCH 04/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 05/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 06/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 2910db559584a45f1bcfef70e86daa16aaeb525e Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 16 Oct 2015 11:23:54 +0200 Subject: [PATCH 07/13] KEYCLOAK-1973 Clear user from authentication context is password is not valid --- .../AuthenticationFlowContext.java | 5 ++ .../AuthenticationProcessor.java | 10 ++++ .../AbstractUsernameFormAuthenticator.java | 2 + .../keycloak/testsuite/forms/LoginTest.java | 51 +++++++++++++++++-- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 681e76c484..9f69e636e0 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -32,6 +32,11 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon */ void setUser(UserModel user); + /** + * Clear the user from the flow. + */ + void clearUser(); + void attachUserSession(UserSessionModel userSession); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 35b2ab94c8..2d7f0ebc9c 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -215,6 +215,9 @@ public class AuthenticationProcessor { getClientSession().setAuthenticatedUser(user); } + public void clearAuthenticatedUser() { + getClientSession().setAuthenticatedUser(null); + } public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext { AuthenticatorConfigModel authenticatorConfig; @@ -332,6 +335,8 @@ public class AuthenticationProcessor { } + + @Override public UserModel getUser() { return getClientSession().getAuthenticatedUser(); @@ -342,6 +347,11 @@ public class AuthenticationProcessor { setAutheticatedUser(user); } + @Override + public void clearUser() { + clearAuthenticatedUser(); + } + @Override public RealmModel getRealm() { return AuthenticationProcessor.this.getRealm(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java index 062d0ba0c8..294d220696 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java @@ -140,6 +140,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); Response challengeResponse = invalidCredentials(context); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); + context.clearUser(); return false; } credentials.add(UserCredentialModel.password(password)); @@ -149,6 +150,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); Response challengeResponse = invalidCredentials(context); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); + context.clearUser(); return false; } return true; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index bb6292267e..de7fff48b4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -66,19 +66,28 @@ public class LoginTest { @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + UserCredentialModel creds = new UserCredentialModel(); + creds.setType(CredentialRepresentation.PASSWORD); + creds.setValue("password"); + UserModel user = manager.getSession().users().addUser(appRealm, "login-test"); user.setEmail("login@test.com"); user.setEnabled(true); userId = user.getId(); - UserCredentialModel creds = new UserCredentialModel(); - creds.setType(CredentialRepresentation.PASSWORD); - creds.setValue("password"); - user.updateCredential(creds); + + UserModel user2 = manager.getSession().users().addUser(appRealm, "login-test2"); + user2.setEmail("login2@test.com"); + user2.setEnabled(true); + + user2Id = user2.getId(); + + user2.updateCredential(creds); } }); @@ -108,6 +117,8 @@ public class LoginTest { private static String userId; + private static String user2Id; + @Test public void testBrowserSecurityHeaders() { Client client = ClientBuilder.newClient(); @@ -122,6 +133,31 @@ public class LoginTest { response.close(); } + @Test + public void loginChangeUserAfterInvalidPassword() { + loginPage.open(); + loginPage.login("login-test2", "invalid"); + + loginPage.assertCurrent(); + + Assert.assertEquals("login-test2", loginPage.getUsername()); + Assert.assertEquals("", loginPage.getPassword()); + + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + events.expectLogin().user(user2Id).session((String) null).error("invalid_user_credentials") + .detail(Details.USERNAME, "login-test2") + .removeDetail(Details.CONSENT) + .assertEvent(); + + loginPage.login("login-test", "password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + } + @Test public void loginInvalidPassword() { loginPage.open(); @@ -247,6 +283,13 @@ public class LoginTest { .detail(Details.USERNAME, "invalid") .removeDetail(Details.CONSENT) .assertEvent(); + + loginPage.login("login-test", "password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); } @Test From 244848afc4ce82b0666fca8937044811a7cc88d4 Mon Sep 17 00:00:00 2001 From: mhajas Date: Fri, 16 Oct 2015 11:31:45 +0200 Subject: [PATCH 08/13] Fix events tests stability --- .../testsuite/console/events/AdminEventsTest.java | 15 +++++++++------ .../testsuite/console/events/LoginEventsTest.java | 15 ++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/AdminEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/AdminEventsTest.java index 5b22ca4f90..74418dd36c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/AdminEventsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/AdminEventsTest.java @@ -4,6 +4,7 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.console.AbstractConsoleTest; import org.keycloak.testsuite.console.clients.AbstractClientTest; @@ -13,11 +14,11 @@ import org.keycloak.testsuite.console.page.events.Config; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; -import static org.junit.Assert.assertEquals; - import javax.ws.rs.core.Response; import java.util.List; +import static org.junit.Assert.assertEquals; + /** * @author mhajas @@ -37,10 +38,12 @@ public class AdminEventsTest extends AbstractConsoleTest { @Before public void beforeAdminEventsTest() { - configPage.navigateTo(); - configPage.form().setSaveAdminEvents(true); - configPage.form().setIncludeRepresentation(true); - configPage.form().save(); + RealmRepresentation realm = testRealmResource().toRepresentation(); + + realm.setAdminEventsEnabled(true); + realm.setAdminEventsDetailsEnabled(true); + + testRealmResource().update(realm); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java index d8d112463b..bb0ac8d06c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java @@ -3,6 +3,7 @@ package org.keycloak.testsuite.console.events; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.admin.Users; import org.keycloak.testsuite.console.AbstractConsoleTest; import org.keycloak.testsuite.console.page.events.Config; @@ -10,6 +11,7 @@ import org.keycloak.testsuite.console.page.events.LoginEvents; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; +import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; @@ -26,13 +28,12 @@ public class LoginEventsTest extends AbstractConsoleTest { @Before public void beforeLoginEventsTest() { - configPage.navigateTo(); - configPage.form().setSaveEvents(true); - configPage.form().waitForClearEventsButtonPresent(); - configPage.form().addSaveType("LOGIN"); - configPage.form().addSaveType("LOGIN_ERROR"); - configPage.form().addSaveType("LOGOUT"); - configPage.form().save(); + RealmRepresentation realm = testRealmResource().toRepresentation(); + + realm.setEventsEnabled(true); + realm.setEnabledEventTypes(Arrays.asList("LOGIN", "LOGIN_ERROR", "LOGOUT")); + + testRealmResource().update(realm); } @Test From ce996f6a4123665e8aa25d8367ca8b321902195f Mon Sep 17 00:00:00 2001 From: mhajas Date: Fri, 16 Oct 2015 11:33:11 +0200 Subject: [PATCH 09/13] Fix PasswordPolicyTest stability --- .../testsuite/console/authentication/PasswordPolicyTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java index cbb3dd5a06..b48cf84b06 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java @@ -32,7 +32,6 @@ import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy. * @author Petr Mensik * @author mhajas */ -//@Ignore // FIXME still unstable public class PasswordPolicyTest extends AbstractConsoleTest { @Page @@ -67,7 +66,6 @@ public class PasswordPolicyTest extends AbstractConsoleTest { @Test public void testLengthPolicy() { - RealmRepresentation realm = testRealmResource().toRepresentation(); realm.setPasswordPolicy("length(8) and "); testRealmResource().update(realm); @@ -154,7 +152,6 @@ public class PasswordPolicyTest extends AbstractConsoleTest { public void testRegexPatternsPolicy() { RealmRepresentation realm = testRealmResource().toRepresentation(); realm.setPasswordPolicy("regexPattern(^[A-Z]+#[a-z]{8}$) and "); - System.out.println(realm.getPasswordPolicy()); testRealmResource().update(realm); testUserCredentialsPage.navigateTo(); From 39f985e04ac21b66a298db544ed8189f483f5f43 Mon Sep 17 00:00:00 2001 From: mhajas Date: Fri, 16 Oct 2015 12:09:35 +0200 Subject: [PATCH 10/13] Clean code --- .../testsuite/console/page/authentication/PasswordPolicy.java | 2 -- .../org/keycloak/testsuite/console/events/LoginEventsTest.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java index 5eb8f1b10f..818ff5d608 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java @@ -34,8 +34,6 @@ public class PasswordPolicy extends Authentication { public void addPolicy(PasswordPolicy.Type policy, String value) { waitGuiForElement(addPolicySelectElement); addPolicySelect.selectByVisibleText(policy.getName()); - - //addPolicySelect.selectByValue(policy.getName()); setPolicyValue(policy, value); primaryButton.click(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java index bb0ac8d06c..3b32274fb9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java @@ -23,9 +23,10 @@ import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD public class LoginEventsTest extends AbstractConsoleTest { @Page private LoginEvents loginEventsPage; + @Page private Config configPage; - + @Before public void beforeLoginEventsTest() { RealmRepresentation realm = testRealmResource().toRepresentation(); From eb9f69eee0f0d07fa27f08a2ec46d7f7b42856fb Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 16 Oct 2015 12:23:41 +0200 Subject: [PATCH 11/13] KEYCLOAK-1969 Admin console displays Clients flow page when execution or execution flow is created. --- .../theme/base/admin/resources/js/controllers/realm.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 023cdf1271..c8f27c3954 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 @@ -1657,12 +1657,12 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, pa $scope.save = function() { $scope.flow.provider = $scope.provider.id; CreateExecutionFlow.save({realm: realm.realm, alias: parentFlow.alias}, $scope.flow, function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); Notifications.success("Flow Created."); }) } $scope.cancel = function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); }; }); @@ -1671,7 +1671,7 @@ module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parent Notifications, $location) { $scope.realm = realm; $scope.parentFlow = parentFlow; - console.log('parentFlow.providerId: ' + parentFlow.providerId); + console.debug('parentFlow.providerId: ' + parentFlow.providerId); if (parentFlow.providerId == 'form-flow') { $scope.providers = formActionProviders; } else if (parentFlow.providerId == 'client-flow') { @@ -1690,12 +1690,12 @@ module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parent provider: $scope.provider.id } CreateExecution.save({realm: realm.realm, alias: parentFlow.alias}, execution, function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); Notifications.success("Execution Created."); }) } $scope.cancel = function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); }; }); From d2afb4892ad88b7749fb067bfca5181bd89e3c99 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 16 Oct 2015 12:51:11 +0200 Subject: [PATCH 12/13] KEYCLOAK-1975 Increase/decrease flow priority doesn't work --- .../AuthenticationManagementResource.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index 1d441ece5f..063e66c272 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -431,9 +431,16 @@ public class AuthenticationManagementResource { execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED); execution.setAuthenticatorFlow(true); execution.setAuthenticator(provider); + execution.setPriority(getNextPriority(parentFlow)); + realm.addAuthenticatorExecution(execution); } + private int getNextPriority(AuthenticationFlowModel parentFlow) { + List executions = getSortedExecutions(parentFlow); + return executions.isEmpty() ? 0 : executions.get(executions.size() - 1).getPriority() + 1; + } + /** * Add new authentication execution to a flow * @@ -453,12 +460,13 @@ public class AuthenticationManagementResource { } String provider = data.get("provider"); - AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); execution.setParentFlow(parentFlow.getId()); execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED); execution.setAuthenticatorFlow(false); execution.setAuthenticator(provider); + execution.setPriority(getNextPriority(parentFlow)); + realm.addAuthenticatorExecution(execution); } @@ -583,13 +591,7 @@ public class AuthenticationManagementResource { if (parentFlow.isBuiltIn()) { throw new BadRequestException("It is illegal to add execution to a built in flow"); } - int priority = 0; - List executions = getSortedExecutions(parentFlow); - for (AuthenticationExecutionModel execution : executions) { - priority = execution.getPriority(); - } - if (priority > 0) priority += 10; - model.setPriority(priority); + model.setPriority(getNextPriority(parentFlow)); model = realm.addAuthenticatorExecution(model); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } From 6386d95904cafd3f2277b170e3ecf6a32e512796 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 16 Oct 2015 14:03:30 +0200 Subject: [PATCH 13/13] Tweak authentication flow page a bit --- .../partials/authentication-flows.html | 34 +++++++++---------- .../keycloak/admin/resources/css/styles.css | 4 +++ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html index 09afda2d0d..e055978bd6 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html @@ -26,16 +26,20 @@ Auth Type Requirement - +   - {{execution.displayName|capitalize}} + + + + {{execution.displayName|capitalize}} + -