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 b50a4f465f..5b0a0da893 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 @@ -31,7 +31,7 @@ - + @@ -47,14 +47,14 @@ - + - - + + \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml new file mode 100644 index 0000000000..4e0129eee7 --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.1.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index 6cd96c626c..3010118039 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -9,5 +9,5 @@ - + diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java index 60b063513e..401cf74b2d 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java @@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider { public String FIRST_VERSION = "1.0.0.Final"; - public String LAST_VERSION = "1.6.0"; + public String LAST_VERSION = "1.6.1"; public String getCurrentVersionSql(String defaultSchema); diff --git a/distribution/saml-adapters/jetty91-adapter-zip/pom.xml b/distribution/saml-adapters/jetty91-adapter-zip/pom.xml index 2c15be444a..733aabf157 100755 --- a/distribution/saml-adapters/jetty91-adapter-zip/pom.xml +++ b/distribution/saml-adapters/jetty91-adapter-zip/pom.xml @@ -4,7 +4,7 @@ keycloak-parent org.keycloak - 1.6.0.Final-SNAPSHOT + ../../../pom.xml diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml b/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml index 33d62dca2d..2036afbd8a 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/javascript-adapter.xml @@ -210,6 +210,7 @@ new Keycloak({ url: 'http://localhost/auth', realm: 'myrealm', clientId: 'myApp' prompt - can be set to 'none' to check if the user is logged in already (if not logged in, a login form is not displayed) loginHint - used to pre-fill the username/email field on the login form action - if value is 'register' then user is redirected to registration page, otherwise to login page + locale - specifies the desired locale for the UI diff --git a/examples/pom.xml b/examples/pom.xml index ec25be9438..6423a7c018 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -14,14 +14,18 @@ pom + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + - - org.apache.maven.plugins - maven-deploy-plugin - - true - - org.apache.maven.plugins maven-war-plugin diff --git a/examples/saml/post-with-encryption/pom.xml b/examples/saml/post-with-encryption/pom.xml index 0c95d95563..090f6afc0e 100755 --- a/examples/saml/post-with-encryption/pom.xml +++ b/examples/saml/post-with-encryption/pom.xml @@ -2,9 +2,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - org.keycloak.examples + + keycloak-examples-saml-parent + org.keycloak + 1.7.0.Final-SNAPSHOT + + saml-post-encryption - 1.6.0.Final-SNAPSHOT war diff --git a/examples/saml/post-with-signature/pom.xml b/examples/saml/post-with-signature/pom.xml index 8841ac054c..14db058a9b 100755 --- a/examples/saml/post-with-signature/pom.xml +++ b/examples/saml/post-with-signature/pom.xml @@ -2,9 +2,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - org.keycloak.examples + + keycloak-examples-saml-parent + org.keycloak + 1.7.0.Final-SNAPSHOT + + saml-post-signatures - 1.6.0.Final-SNAPSHOT war diff --git a/examples/saml/redirect-with-signature/pom.xml b/examples/saml/redirect-with-signature/pom.xml index 55f426b7df..06a4e55d5e 100755 --- a/examples/saml/redirect-with-signature/pom.xml +++ b/examples/saml/redirect-with-signature/pom.xml @@ -2,9 +2,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - org.keycloak.examples + + keycloak-examples-saml-parent + org.keycloak + 1.7.0.Final-SNAPSHOT + + saml-redirect-signatures - 1.6.0.Final-SNAPSHOT war diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-reset-password.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-reset-password.ftl index e19a266b9d..404de17ea2 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-reset-password.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-reset-password.ftl @@ -11,7 +11,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl index e472fff1ff..3f46b76ba1 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl @@ -12,7 +12,7 @@
- +
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index cb5f2be8cd..fccfce160c 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setClientSessionCode(String accessCode); + public LoginFormsProvider setClientSession(ClientSessionModel clientSession); + public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested, List protocolMappers); public LoginFormsProvider setAccessRequest(String message); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 7fc1bcddcb..6125d0b83b 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -47,6 +47,7 @@ import org.keycloak.login.freemarker.model.TotpBean; import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -138,7 +139,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { case VERIFY_EMAIL: try { UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode); + builder.queryParam(OAuth2Constants.CODE, accessCode); + builder.queryParam("key", clientSession.getNote(Constants.VERIFY_EMAIL_KEY)); String link = builder.build(realm.getName()).toString(); long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); @@ -531,6 +533,12 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setClientSession(ClientSessionModel clientSession) { + this.clientSession = clientSession; + return this; + } + @Override public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested, List protocolMappersRequested) { this.realmRolesRequested = realmRolesRequested; diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java index f10d031d07..a29271895b 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java @@ -4,6 +4,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.services.util.ResolveRelative; import java.net.URI; +import java.util.Map; /** * @author Bill Burke @@ -32,4 +33,11 @@ public class ClientBean { return ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), client.getBaseUrl()); } + public Map getAttributes(){ + return client.getAttributes(); + } + + public String getAttribute(String key){ + return client.getAttribute(key); + } } diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js index d189a361f8..7b378de347 100755 --- a/integration/js/src/main/resources/keycloak.js +++ b/integration/js/src/main/resources/keycloak.js @@ -168,6 +168,10 @@ url += '&scope=' + options.scope; } + if (options && options.locale) { + url += '&ui_locales=' + options.locale; + } + return url; } 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 88817acb27..45c73a31cf 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 @@ -63,10 +63,8 @@ public class MigrateTo1_6_0 { KeycloakModelUtils.setupOfflineTokens(realm); RoleModel role = realm.getRole(Constants.OFFLINE_ACCESS_ROLE); - // Check if possible to avoid iterating over users - for (UserModel user : session.userStorage().getUsers(realm, true)) { - user.grantRole(role); - } + // Bulk grant of offline_access role to all users + session.users().grantToAllUsers(realm, role); } ClientModel adminConsoleClient = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); 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 43bdc7dc68..8977def5c3 100755 --- a/model/api/src/main/java/org/keycloak/models/Constants.java +++ b/model/api/src/main/java/org/keycloak/models/Constants.java @@ -22,4 +22,6 @@ public interface Constants { // 30 days int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000; + + public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY"; } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index bea51e0473..cde5eb9411 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -333,6 +333,12 @@ public class UserFederationManager implements UserProvider { return session.userStorage().getFederatedIdentity(user, socialProvider, realm); } + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + // not federation-aware for now + session.userStorage().grantToAllUsers(realm, role); + } + @Override public void preRemove(RealmModel realm) { for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 7d7064d76e..82be2fe160 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -43,6 +43,8 @@ public interface UserProvider extends Provider { Set getFederatedIdentities(UserModel user, RealmModel realm); FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm); + void grantToAllUsers(RealmModel realm, RoleModel role); + void preRemove(RealmModel realm); void preRemove(RealmModel realm, UserFederationProviderModel link); diff --git a/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java b/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java new file mode 100644 index 0000000000..513836f3ba --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/PostMigrationEvent.java @@ -0,0 +1,11 @@ +package org.keycloak.models.utils; + +import org.keycloak.provider.ProviderEvent; + +/** + * Executed at startup after model migration is finished + * + * @author Marek Posolda + */ +public class PostMigrationEvent implements ProviderEvent { +} diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index 8edfe3ec2e..8c24262a12 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -422,6 +422,13 @@ public class FileUserProvider implements UserProvider { return this.addUser(realm, KeycloakModelUtils.generateId(), username.toLowerCase(), true, true); } + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + for (UserModel user : inMemoryModel.getUsers(realm.getId())) { + user.grantRole(role); + } + } + @Override public void preRemove(RealmModel realm) { // Nothing to do here? Federation links are attached to users, which are removed by InMemoryModel diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java index 69fc5cf577..9045f911f0 100644 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java @@ -303,6 +303,12 @@ public class DefaultCacheUserProvider implements CacheUserProvider { return getDelegate().validCredentials(realm, input); } + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm + getDelegate().grantToAllUsers(realm, role); + } + @Override public void preRemove(RealmModel realm) { realmInvalidations.add(realm.getId()); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index d4d533a9dc..c9031476cf 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -147,7 +147,13 @@ public class JpaUserProvider implements UserProvider { } } - + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + int num = em.createNamedQuery("grantRoleToAllUsers") + .setParameter("realmId", realm.getId()) + .setParameter("roleId", role.getId()) + .executeUpdate(); + } @Override public void preRemove(RealmModel realm) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java index 1ce81e8297..45dfc3de95 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java @@ -23,7 +23,8 @@ import java.io.Serializable; @NamedQuery(name="deleteUserRoleMappingsByRealm", query="delete from UserRoleMappingEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteUserRoleMappingsByRealmAndLink", query="delete from UserRoleMappingEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"), @NamedQuery(name="deleteUserRoleMappingsByRole", query="delete from UserRoleMappingEntity m where m.roleId = :roleId"), - @NamedQuery(name="deleteUserRoleMappingsByUser", query="delete from UserRoleMappingEntity m where m.user = :user") + @NamedQuery(name="deleteUserRoleMappingsByUser", query="delete from UserRoleMappingEntity m where m.user = :user"), + @NamedQuery(name="grantRoleToAllUsers", query="insert into UserRoleMappingEntity (roleId, user) select role.id, user from RoleEntity role, UserEntity user where role.id = :roleId AND role.realm.id = :realmId AND user.realmId = :realmId") }) @Table(name="USER_ROLE_MAPPING") diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 358e6f2d28..9fc9735559 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -368,6 +368,19 @@ public class MongoUserProvider implements UserProvider { return this.addUser(realm, null, username, true, true); } + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + DBObject query = new QueryBuilder() + .and("realmId").is(realm.getId()) + .get(); + + DBObject update = new QueryBuilder() + .and("$push").is(new BasicDBObject("roleIds", role.getId())) + .get(); + + int count = getMongoStore().updateEntities(MongoUserEntity.class, query, update, invocationContext); + } + @Override public void preRemove(RealmModel realm) { DBObject query = new QueryBuilder() 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 1d7c279542..382d01f630 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 @@ -5,9 +5,11 @@ import org.infinispan.Version; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; import org.keycloak.models.session.UserSessionPersisterProvider; @@ -19,6 +21,9 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer; import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.PostMigrationEvent; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderEventListener; /** * Uses Infinispan to store user sessions. On EAP 6.4 (Infinispan 5.2) map reduce is not supported for local caches as a work around @@ -68,13 +73,20 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider }); // Max count of worker errors. Initialization will end with exception when this number is reached - int maxErrors = config.getInt("maxErrors", 20); + final int maxErrors = config.getInt("maxErrors", 20); // Count of sessions to be computed in each segment - int sessionsPerSegment = config.getInt("sessionsPerSegment", 100); + final int sessionsPerSegment = config.getInt("sessionsPerSegment", 100); - // TODO: Possibility to run this asynchronously to not block start time - loadPersistentSessions(factory, maxErrors, sessionsPerSegment); + factory.register(new ProviderEventListener() { + + @Override + public void onEvent(ProviderEvent event) { + if (event instanceof PostMigrationEvent) { + loadPersistentSessions(factory, maxErrors, sessionsPerSegment); + } + } + }); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java index 01ddcffa7e..7fc77a81d8 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java @@ -8,9 +8,12 @@ import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.HmacOTP; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.validation.Validation; @@ -44,8 +47,11 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success(); LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId()); + setupKey(context.getClientSession()); + LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) .setClientSessionCode(context.generateCode()) + .setClientSession(context.getClientSession()) .setUser(context.getUser()); Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); context.challenge(challenge); @@ -87,4 +93,9 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor public String getId() { return UserModel.RequiredAction.VERIFY_EMAIL.name(); } + + public static void setupKey(ClientSessionModel clientSession) { + String secret = HmacOTP.generateSecret(10); + clientSession.setNote(Constants.VERIFY_EMAIL_KEY, secret); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 83b8d6e68e..a07999d6c1 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -11,6 +11,7 @@ import org.keycloak.migration.MigrationModelManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.offlineconfig.AdminRecovery; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.DefaultKeycloakSessionFactory; @@ -83,6 +84,8 @@ public class KeycloakApplication extends Application { setupDefaultRealm(context.getContextPath()); migrateModel(); + sessionFactory.publish(new PostMigrationEvent()); + new ExportImportManager().checkExportImport(this.sessionFactory, context.getContextPath()); importRealms(context); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 46bc55393e..20de78b7d8 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -23,6 +23,8 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.requiredactions.VerifyEmail; import org.keycloak.common.ClientConnection; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; @@ -49,6 +51,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.FormMessage; +import org.keycloak.models.utils.HmacOTP; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.RestartLoginCookie; @@ -533,7 +536,7 @@ public class LoginActionsService { event.event(EventType.VERIFY_EMAIL); if (key != null) { Checks checks = new Checks(); - if (!checks.verifyCode(key, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { + if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -547,11 +550,21 @@ public class LoginActionsService { UserSessionModel userSession = clientSession.getUserSession(); UserModel user = userSession.getUser(); initEvent(clientSession); + event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()); + + String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY); + clientSession.removeNote(Constants.VERIFY_EMAIL_KEY); + if (!key.equals(keyFromSession)) { + logger.error("Invalid key for email verification"); + event.error(Errors.INVALID_USER_CREDENTIALS); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + user.setEmailVerified(true); user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); - event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); + event.success(); String actionCookieValue = getActionCookie(); if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { @@ -576,8 +589,11 @@ public class LoginActionsService { createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); + VerifyEmail.setupKey(clientSession); + return session.getProvider(LoginFormsProvider.class) .setClientSessionCode(accessCode.getCode()) + .setClientSession(clientSession) .setUser(userSession.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index a9c0c58373..c7f075f4d6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -26,7 +26,9 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.models.RealmModel; @@ -130,7 +132,7 @@ public class RequiredActionEmailVerificationTest { String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); - Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]); + Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]); driver.navigate().to(verificationUrl.trim()); @@ -223,7 +225,7 @@ public class RequiredActionEmailVerificationTest { String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); - Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]); + Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]); driver.manage().deleteAllCookies(); @@ -238,6 +240,42 @@ public class RequiredActionEmailVerificationTest { assertTrue(loginPage.isCurrent()); } + + + @Test + public void verifyInvalidKeyOrCode() throws IOException, MessagingException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + Assert.assertTrue(verifyEmailPage.isCurrent()); + String resendEmailLink = verifyEmailPage.getResendEmailLink(); + String keyInsteadCodeURL = resendEmailLink.replace("code=", "key="); + + AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost"); + Event sendEvent = emailEvent.assertEvent(); + String sessionId = sendEvent.getSessionId(); + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + + driver.navigate().to(keyInsteadCodeURL); + + events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR) + .error(Errors.INVALID_CODE) + .client((String)null) + .user((String)null) + .session((String)null) + .clearDetails() + .assertEvent(); + + String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).queryParam("key", "foo").build().toString(); + driver.navigate().to(badKeyURL); + + events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR) + .error(Errors.INVALID_USER_CREDENTIALS) + .session(sessionId) + .detail("email", "test-user@localhost") + .detail(Details.CODE_ID, mailCodeId) + .assertEvent(); + } private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException { Multipart multipart = (Multipart) message.getContent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index 258dd3cbd7..66018552c5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.ClientManager; @@ -283,6 +284,36 @@ public class UserModelTest extends AbstractModelTest { Assert.assertNull(session.users().getUserByUsername("user1", realm)); } + @Test + public void testGrantToAll() { + RealmModel realm1 = realmManager.createRealm("realm1"); + RoleModel role1 = realm1.addRole("role1"); + UserModel user1 = realmManager.getSession().users().addUser(realm1, "user1"); + UserModel user2 = realmManager.getSession().users().addUser(realm1, "user2"); + + RealmModel realm2 = realmManager.createRealm("realm2"); + UserModel realm2User1 = realmManager.getSession().users().addUser(realm2, "user1"); + + commit(); + + realm1 = realmManager.getRealmByName("realm1"); + role1 = realm1.getRole("role1"); + realmManager.getSession().users().grantToAllUsers(realm1, role1); + + commit(); + + realm1 = realmManager.getRealmByName("realm1"); + role1 = realm1.getRole("role1"); + user1 = realmManager.getSession().users().getUserByUsername("user1", realm1); + user2 = realmManager.getSession().users().getUserByUsername("user2", realm1); + Assert.assertTrue(user1.hasRole(role1)); + Assert.assertTrue(user2.hasRole(role1)); + + realm2 = realmManager.getRealmByName("realm2"); + realm2User1 = realmManager.getSession().users().getUserByUsername("user1", realm2); + Assert.assertFalse(realm2User1.hasRole(role1)); + } + public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java index cfcfbb4fdb..9968ce1087 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java @@ -50,4 +50,8 @@ public class VerifyEmailPage extends AbstractPage { resendEmailLink.click(); } + public String getResendEmailLink() { + return resendEmailLink.getAttribute("href"); + } + }