diff --git a/admin-ui-styles/pom.xml b/admin-ui-styles/pom.xml index b6aa4c6be11..442de9b3ea1 100755 --- a/admin-ui-styles/pom.xml +++ b/admin-ui-styles/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -21,8 +22,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/admin-ui/pom.xml b/admin-ui/pom.xml index 3e983805dfd..ddcfd21dbbc 100755 --- a/admin-ui/pom.xml +++ b/admin-ui/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -21,8 +22,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js index 8da77e8b73f..d780e60f15a 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js @@ -129,7 +129,7 @@ module.config([ '$routeProvider', function($routeProvider) { return RealmLoader(); } }, - controller : 'RealmSMTPSettingsCtrl' + controller : 'RealmLdapSettingsCtrl' }) .when('/create/user/:realm', { templateUrl : 'partials/user-detail.html', diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index 0de9f8cd748..b441e318a77 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -885,4 +885,33 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real return obj; } +}); + +module.controller('RealmLdapSettingsCtrl', function($scope, Realm, realm, $location, Notifications) { + console.log('RealmLdapSettingsCtrl'); + + $scope.realm = realm; + + var oldCopy = angular.copy($scope.realm); + $scope.changed = false; + + $scope.$watch('realm', function() { + if (!angular.equals($scope.realm, oldCopy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + var realmCopy = angular.copy($scope.realm); + $scope.changed = false; + Realm.update(realmCopy, function () { + $location.url("/realms/" + realm.realm + "/ldap-settings"); + Notifications.success("Your changes have been saved to the realm."); + }); + }; + + $scope.reset = function() { + $scope.realm = angular.copy(oldCopy); + $scope.changed = false; + }; }); \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-ldap.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-ldap.html index b08899fa795..c571eb74978 100644 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-ldap.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-ldap.html @@ -7,60 +7,39 @@
  • Settings
  • Ldap Configuration
  • -

    {{realm.realm}} Email Server Settings

    +

    {{realm.realm}} Ldap Server Settings

    * Required fields
    Required Settings
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - +
    - -
    -
    -
    -
    - Authentication -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - +
    diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html index aadf9c08e59..b0ae3869d33 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html @@ -1,7 +1,7 @@
    • Settings
    • + path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings') && path[3] != 'applications') && 'active'">Settings
    • Users
    • Applications
    • diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html index 6c53b869f15..95dac11c894 100644 --- a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html @@ -7,4 +7,5 @@
    • Token
    • Keys
    • Email
    • +
    • Ldap
    \ No newline at end of file diff --git a/audit/api/pom.xml b/audit/api/pom.xml new file mode 100755 index 00000000000..facd5866a35 --- /dev/null +++ b/audit/api/pom.xml @@ -0,0 +1,39 @@ + + + + keycloak-audit-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + + + 4.0.0 + + keycloak-audit-api + Keycloak Audit API + + + + + org.jboss.logging + jboss-logging + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-model-api + ${project.version} + provided + + + junit + junit + test + + + + diff --git a/audit/api/src/main/java/org/keycloak/audit/Audit.java b/audit/api/src/main/java/org/keycloak/audit/Audit.java new file mode 100644 index 00000000000..682ceb4a910 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java @@ -0,0 +1,145 @@ +package org.keycloak.audit; + +import org.jboss.logging.Logger; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderFactoryLoader; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class Audit { + + private static final Logger log = Logger.getLogger(Audit.class); + + private List listeners; + private Event event; + + public static Audit create(RealmModel realm, String ipAddress) { + ProviderFactoryLoader loader = ProviderFactoryLoader.load(AuditListenerFactory.class); + + List listeners = null; + if (realm.getAuditListeners() != null) { + listeners = new LinkedList(); + + for (String id : realm.getAuditListeners()) { + listeners.add(loader.find(id).create()); + } + } + + return new Audit(listeners, new Event()).realm(realm).ipAddress(ipAddress); + } + + private Audit(List listeners, Event event) { + this.listeners = listeners; + this.event = event; + } + + public Audit realm(RealmModel realm) { + event.setRealmId(realm.getId()); + return this; + } + + public Audit realm(String realmId) { + event.setRealmId(realmId); + return this; + } + + public Audit client(ClientModel client) { + event.setClientId(client.getClientId()); + return this; + } + + public Audit client(String clientId) { + event.setClientId(clientId); + return this; + } + + public Audit user(UserModel user) { + event.setUserId(user.getId()); + return this; + } + + public Audit user(String userId) { + event.setUserId(userId); + return this; + } + + public Audit ipAddress(String ipAddress) { + event.setIpAddress(ipAddress); + return this; + } + + public Audit event(String e) { + event.setEvent(e); + return this; + } + + public Audit detail(String key, String value) { + if (value == null || value.equals("")) { + return this; + } + + if (event.getDetails() == null) { + event.setDetails(new HashMap()); + } + event.getDetails().put(key, value); + return this; + } + + public Audit removeDetail(String key) { + if (event.getDetails() != null) { + event.getDetails().remove(key); + } + return this; + } + + public Event getEvent() { + return event; + } + + public void success() { + send(); + } + + public void error(String error) { + event.setError(error); + send(); + } + + public Audit clone() { + return new Audit(listeners, event.clone()); + } + + public Audit reset() { + Event old = event; + + event = new Event(); + event.setRealmId(old.getRealmId()); + event.setIpAddress(old.getIpAddress()); + event.setClientId(old.getClientId()); + event.setUserId(old.getUserId()); + + return this; + } + + private void send() { + event.setTime(System.currentTimeMillis()); + + if (listeners != null) { + for (AuditListener l : listeners) { + try { + l.onEvent(event); + } catch (Throwable t) { + log.error("Failed to send event to " + l, t); + } + } + } + } + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/AuditListener.java b/audit/api/src/main/java/org/keycloak/audit/AuditListener.java new file mode 100644 index 00000000000..1425379d8f8 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditListener.java @@ -0,0 +1,12 @@ +package org.keycloak.audit; + +import org.keycloak.provider.Provider; + +/** + * @author Stian Thorgersen + */ +public interface AuditListener extends Provider { + + public void onEvent(Event event); + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/AuditListenerFactory.java b/audit/api/src/main/java/org/keycloak/audit/AuditListenerFactory.java new file mode 100644 index 00000000000..6ffeb20abdf --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditListenerFactory.java @@ -0,0 +1,10 @@ +package org.keycloak.audit; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface AuditListenerFactory extends ProviderFactory { + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/AuditProvider.java b/audit/api/src/main/java/org/keycloak/audit/AuditProvider.java new file mode 100644 index 00000000000..738b79fb491 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditProvider.java @@ -0,0 +1,14 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface AuditProvider extends AuditListener { + + public EventQuery createQuery(); + + public void clear(); + + public void clear(long olderThan); + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/AuditProviderFactory.java b/audit/api/src/main/java/org/keycloak/audit/AuditProviderFactory.java new file mode 100644 index 00000000000..8d6331e46c2 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditProviderFactory.java @@ -0,0 +1,10 @@ +package org.keycloak.audit; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface AuditProviderFactory extends ProviderFactory { + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/Details.java b/audit/api/src/main/java/org/keycloak/audit/Details.java new file mode 100644 index 00000000000..1a8df0ad40a --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Details.java @@ -0,0 +1,22 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface Details { + + String EMAIL = "email"; + String PREVIOUS_EMAIL = "previous_email"; + String UPDATED_EMAIL = "updated_email"; + String CODE_ID = "code_id"; + String REDIRECT_URI = "redirect_uri"; + String RESPONSE_TYPE = "response_type"; + String AUTH_METHOD = "auth_method"; + String REGISTER_METHOD = "register_method"; + String USERNAME = "username"; + String REMEMBER_ME = "remember_me"; + String TOKEN_ID = "token_id"; + String REFRESH_TOKEN_ID = "refresh_token_id"; + String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id"; + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/Errors.java b/audit/api/src/main/java/org/keycloak/audit/Errors.java new file mode 100644 index 00000000000..b34371419f7 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Errors.java @@ -0,0 +1,36 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface Errors { + + String REALM_DISABLED = "realm_disabled"; + + String CLIENT_NOT_FOUND = "client_not_found"; + String CLIENT_DISABLED = "client_disabled"; + String INVALID_CLIENT_CREDENTIALS = "invalid_client_credentials"; + + String USER_NOT_FOUND = "user_not_found"; + String USER_DISABLED = "user_disabled"; + String INVALID_USER_CREDENTIALS = "invalid_user_credentials"; + + String USERNAME_MISSING = "username_missing"; + String USERNAME_IN_USE = "username_in_use"; + + String INVALID_REDIRECT_URI = "invalid_redirect_uri"; + String INVALID_CODE = "invalid_code"; + String INVALID_TOKEN = "invalid_token"; + String INVALID_REGISTRATION = "invalid_registration"; + String INVALID_FORM = "invalid_form"; + + String REGISTRATION_DISABLED = "registration_disabled"; + + String REJECTED_BY_USER = "rejected_by_user"; + + String NOT_ALLOWED = "not_allowed"; + + String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found"; + String SOCIAL_ID_IN_USE = "social_id_in_use"; + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/Event.java b/audit/api/src/main/java/org/keycloak/audit/Event.java new file mode 100644 index 00000000000..85b968a8c3d --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Event.java @@ -0,0 +1,108 @@ +package org.keycloak.audit; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class Event { + + private long time; + + private String event; + + private String realmId; + + private String clientId; + + private String userId; + + private String ipAddress; + + private String error; + + private Map details; + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public boolean isError() { + return error != null; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } + + public Event clone() { + Event clone = new Event(); + clone.time = time; + clone.event = event; + clone.realmId = realmId; + clone.clientId = clientId; + clone.userId = userId; + clone.ipAddress = ipAddress; + clone.error = error; + clone.details = details != null ? new HashMap(details) : null; + return clone; + } + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/EventQuery.java b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java new file mode 100644 index 00000000000..75e07c03cd9 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java @@ -0,0 +1,24 @@ +package org.keycloak.audit; + +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public interface EventQuery { + + public EventQuery event(String event); + + public EventQuery realm(String realmId); + + public EventQuery client(String clientId); + + public EventQuery user(String userId); + + public EventQuery firstResult(int result); + + public EventQuery maxResults(int results); + + public List getResultList(); + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/Events.java b/audit/api/src/main/java/org/keycloak/audit/Events.java new file mode 100644 index 00000000000..9d1a54f9a92 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Events.java @@ -0,0 +1,29 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface Events { + + String LOGIN = "login"; + String REGISTER = "register"; + String LOGOUT = "logout"; + String CODE_TO_TOKEN = "code_to_token"; + String REFRESH_TOKEN = "refresh_token"; + + String SOCIAL_LINK = "social_link"; + String REMOVE_SOCIAL_LINK = "remove_social_link"; + + String UPDATE_EMAIL = "update_email"; + String UPDATE_PROFILE = "update_profile"; + String UPDATE_PASSWORD = "update_password"; + String UPDATE_TOTP = "update_totp"; + + String VERIFY_EMAIL = "verify_email"; + + String REMOVE_TOTP = "remove_totp"; + + String SEND_VERIFY_EMAIL = "send_verify_email"; + String SEND_RESET_PASSWORD = "send_reset_password"; + +} diff --git a/audit/jboss-logging/pom.xml b/audit/jboss-logging/pom.xml new file mode 100755 index 00000000000..f3f11bad14c --- /dev/null +++ b/audit/jboss-logging/pom.xml @@ -0,0 +1,39 @@ + + + + keycloak-audit-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + + + 4.0.0 + + keycloak-audit-jboss-logging + Keycloak Audit JBoss Logging Provider + + + + + org.jboss.logging + jboss-logging + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-audit-api + ${project.version} + provided + + + junit + junit + test + + + + diff --git a/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListener.java b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListener.java new file mode 100644 index 00000000000..d5381e24cea --- /dev/null +++ b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListener.java @@ -0,0 +1,66 @@ +package org.keycloak.audit.log; + +import org.jboss.logging.Logger; +import org.keycloak.audit.AuditListener; +import org.keycloak.audit.Event; + +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class JBossLoggingAuditListener implements AuditListener { + + private final Logger logger; + + public JBossLoggingAuditListener(Logger logger) { + this.logger = logger; + } + + @Override + public void onEvent(Event event) { + Logger.Level level = event.isError() ? Logger.Level.WARN : Logger.Level.INFO; + + if (logger.isEnabled(level)) { + StringBuilder sb = new StringBuilder(); + + sb.append("event="); + sb.append(event.getEvent()); + sb.append(", realmId="); + sb.append(event.getRealmId()); + sb.append(", clientId="); + sb.append(event.getClientId()); + sb.append(", userId="); + sb.append(event.getUserId()); + sb.append(", ipAddress="); + sb.append(event.getIpAddress()); + + if (event.isError()) { + sb.append(", error="); + sb.append(event.getError()); + } + + if (event.getDetails() != null) { + for (Map.Entry e : event.getDetails().entrySet()) { + sb.append(", "); + sb.append(e.getKey()); + if (e.getValue() == null || e.getValue().indexOf(' ') == -1) { + sb.append("="); + sb.append(e.getValue()); + } else { + sb.append("='"); + sb.append(e.getValue()); + sb.append("'"); + } + } + } + + logger.log(level, sb.toString()); + } + } + + @Override + public void close() { + } + +} diff --git a/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java new file mode 100644 index 00000000000..420bd675859 --- /dev/null +++ b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListenerFactory.java @@ -0,0 +1,34 @@ +package org.keycloak.audit.log; + +import org.jboss.logging.Logger; +import org.keycloak.audit.AuditListener; +import org.keycloak.audit.AuditListenerFactory; + +/** + * @author Stian Thorgersen + */ +public class JBossLoggingAuditListenerFactory implements AuditListenerFactory { + + public static final String ID = "jboss-logging"; + + private static final Logger logger = Logger.getLogger("org.keycloak.audit"); + + @Override + public AuditListener create() { + return new JBossLoggingAuditListener(logger); + } + + @Override + public void init() { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + +} diff --git a/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory b/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory new file mode 100644 index 00000000000..2370355264d --- /dev/null +++ b/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory @@ -0,0 +1 @@ +org.keycloak.audit.log.JBossLoggingAuditListenerFactory \ No newline at end of file diff --git a/audit/jpa/pom.xml b/audit/jpa/pom.xml new file mode 100755 index 00000000000..e8fa5501d95 --- /dev/null +++ b/audit/jpa/pom.xml @@ -0,0 +1,61 @@ + + + + keycloak-audit-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + + + 4.0.0 + + keycloak-audit-jpa + Keycloak Audit JPA Provider + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-audit-api + ${project.version} + provided + + + org.keycloak + keycloak-audit-tests + ${project.version} + provided + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + provided + + + org.hibernate + hibernate-entitymanager + ${hibernate.entitymanager.version} + provided + + + junit + junit + test + + + com.h2database + h2 + test + + + org.json + json + + + + diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java new file mode 100644 index 00000000000..a8fd5317cf5 --- /dev/null +++ b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/EventEntity.java @@ -0,0 +1,103 @@ +package org.keycloak.audit.jpa; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * @author Stian Thorgersen + */ +@Entity +public class EventEntity { + + @Id + private String id; + + private long time; + + private String event; + + private String realmId; + + private String clientId; + + private String userId; + + private String ipAddress; + + private String error; + + private String detailsJson; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public String getEvent() { + return event; + } + + public void setEvent(String event) { + this.event = event; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getDetailsJson() { + return detailsJson; + } + + public void setDetailsJson(String detailsJson) { + this.detailsJson = detailsJson; + } + +} diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java new file mode 100644 index 00000000000..0432f178286 --- /dev/null +++ b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java @@ -0,0 +1,99 @@ +package org.keycloak.audit.jpa; + +import org.json.JSONObject; +import org.keycloak.audit.AuditProvider; +import org.keycloak.audit.Event; +import org.keycloak.audit.EventQuery; + +import javax.persistence.EntityManager; +import javax.persistence.EntityTransaction; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Stian Thorgersen + */ +public class JpaAuditProvider implements AuditProvider { + + private EntityManager em; + private EntityTransaction tx; + + public JpaAuditProvider(EntityManager em) { + this.em = em; + } + + @Override + public EventQuery createQuery() { + return new JpaEventQuery(em); + } + + @Override + public void clear() { + beginTx(); + em.createQuery("delete from EventEntity").executeUpdate(); + } + + @Override + public void clear(long olderThan) { + beginTx(); + em.createQuery("delete from EventEntity where time < :time").setParameter("time", olderThan).executeUpdate(); + } + + @Override + public void onEvent(Event event) { + beginTx(); + em.persist(convert(event)); + } + + @Override + public void close() { + if (tx != null) { + tx.commit(); + } + + em.close(); + } + + private void beginTx() { + if (tx == null) { + tx = em.getTransaction(); + tx.begin(); + } + } + + static EventEntity convert(Event o) { + EventEntity e = new EventEntity(); + e.setId(UUID.randomUUID().toString()); + e.setTime(o.getTime()); + e.setEvent(o.getEvent()); + e.setRealmId(o.getRealmId()); + e.setClientId(o.getClientId()); + e.setUserId(o.getUserId()); + e.setIpAddress(o.getIpAddress()); + e.setError(o.getError()); + e.setDetailsJson(new JSONObject(o.getDetails()).toString()); + return e; + } + + static Event convert(EventEntity o) { + Event e = new Event(); + e.setTime(o.getTime()); + e.setEvent(o.getEvent()); + e.setRealmId(o.getRealmId()); + e.setClientId(o.getClientId()); + e.setUserId(o.getUserId()); + e.setIpAddress(o.getIpAddress()); + e.setError(o.getError()); + + JSONObject object = new JSONObject(o.getDetailsJson()); + Map details = new HashMap(); + for (Object k : object.keySet()) { + details.put((String) k, object.getString((String) k)); + } + + e.setDetails(details); + return e; + } + +} diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java new file mode 100644 index 00000000000..e05ab636f98 --- /dev/null +++ b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java @@ -0,0 +1,37 @@ +package org.keycloak.audit.jpa; + +import org.keycloak.audit.AuditProvider; +import org.keycloak.audit.AuditProviderFactory; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +/** + * @author Stian Thorgersen + */ +public class JpaAuditProviderFactory implements AuditProviderFactory { + + public static final String ID = "jpa"; + private EntityManagerFactory emf; + + @Override + public AuditProvider create() { + return new JpaAuditProvider(emf.createEntityManager()); + } + + @Override + public void init() { + emf = Persistence.createEntityManagerFactory("jpa-keycloak-audit-store"); + } + + @Override + public void close() { + emf.close(); + } + + @Override + public String getId() { + return ID; + } + +} diff --git a/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java new file mode 100644 index 00000000000..f4ec884360d --- /dev/null +++ b/audit/jpa/src/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java @@ -0,0 +1,100 @@ +package org.keycloak.audit.jpa; + +import org.keycloak.audit.Event; +import org.keycloak.audit.EventQuery; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class JpaEventQuery implements EventQuery { + + private final EntityManager em; + private final CriteriaBuilder cb; + private final CriteriaQuery cq; + private final Root root; + private final ArrayList predicates; + private Integer firstResult; + private Integer maxResults; + + public JpaEventQuery(EntityManager em) { + this.em = em; + + cb = em.getCriteriaBuilder(); + cq = cb.createQuery(EventEntity.class); + root = cq.from(EventEntity.class); + predicates = new ArrayList(4); + } + + @Override + public EventQuery event(String event) { + predicates.add(cb.equal(root.get("event"), event)); + return this; + } + + @Override + public EventQuery realm(String realmId) { + predicates.add(cb.equal(root.get("realmId"), realmId)); + return this; + } + + @Override + public EventQuery client(String clientId) { + predicates.add(cb.equal(root.get("clientId"), clientId)); + return this; + } + + @Override + public EventQuery user(String userId) { + predicates.add(cb.equal(root.get("userId"), userId)); + return this; + } + + @Override + public EventQuery firstResult(int firstResult) { + this.firstResult = firstResult; + return this; + } + + @Override + public EventQuery maxResults(int maxResults) { + this.maxResults = maxResults; + return this; + } + + @Override + public List getResultList() { + if (!predicates.isEmpty()) { + cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()]))); + } + + cq.orderBy(cb.asc(root.get("time")), cb.asc(root.get("id"))); + + TypedQuery query = em.createQuery(cq); + + if (firstResult != null) { + query.setFirstResult(firstResult); + } + + if (maxResults != null) { + query.setMaxResults(maxResults); + } + + List events = new LinkedList(); + for (EventEntity e : query.getResultList()) { + events.add(JpaAuditProvider.convert(e)); + } + + return events; + } + +} diff --git a/audit/jpa/src/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory b/audit/jpa/src/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory new file mode 100644 index 00000000000..e04a02866cf --- /dev/null +++ b/audit/jpa/src/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory @@ -0,0 +1 @@ +org.keycloak.audit.jpa.JpaAuditProviderFactory \ No newline at end of file diff --git a/audit/jpa/src/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java b/audit/jpa/src/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java new file mode 100644 index 00000000000..2cabb9cb46b --- /dev/null +++ b/audit/jpa/src/src/test/java/org/keycloak/audit/jpa/JpaAuditProviderTest.java @@ -0,0 +1,15 @@ +package org.keycloak.audit.jpa; + +import org.keycloak.audit.tests.AbstractAuditProviderTest; + +/** + * @author Stian Thorgersen + */ +public class JpaAuditProviderTest extends AbstractAuditProviderTest { + + @Override + protected String getProviderId() { + return JpaAuditProviderFactory.ID; + } + +} diff --git a/audit/jpa/src/src/test/resources/META-INF/persistence.xml b/audit/jpa/src/src/test/resources/META-INF/persistence.xml new file mode 100755 index 00000000000..f537808fef2 --- /dev/null +++ b/audit/jpa/src/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,22 @@ + + + org.hibernate.ejb.HibernatePersistence + + org.keycloak.audit.jpa.EventEntity + + true + + + + + + + + + + + + diff --git a/audit/mongo/pom.xml b/audit/mongo/pom.xml new file mode 100755 index 00000000000..53f6f734b9a --- /dev/null +++ b/audit/mongo/pom.xml @@ -0,0 +1,129 @@ + + + + keycloak-audit-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + + + 4.0.0 + + keycloak-audit-mongo + Keycloak Audit Mongo Provider + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-audit-api + ${project.version} + provided + + + org.keycloak + keycloak-audit-tests + ${project.version} + test + + + org.mongodb + mongo-java-driver + provided + + + junit + junit + test + + + org.json + json + + + + + localhost + 27018 + keycloak + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + integration-test + + test + + + + ${keycloak.audit.mongo.host} + ${keycloak.audit.mongo.port} + ${keycloak.audit.mongo.db} + ${keycloak.audit.mongo.clearOnStartup} + + + org.keycloak:keycloak-model-tests + + + + + default-test + + true + + + + + + + + com.github.joelittlejohn.embedmongo + embedmongo-maven-plugin + + + start-mongodb + pre-integration-test + + start + + + ${keycloak.audit.mongo.port} + file + ${project.build.directory}/mongodb.log + + + + stop-mongodb + post-integration-test + + stop + + + + + + + + diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java new file mode 100644 index 00000000000..69586529d7a --- /dev/null +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java @@ -0,0 +1,87 @@ +package org.keycloak.audit.mongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import org.keycloak.audit.AuditProvider; +import org.keycloak.audit.Event; +import org.keycloak.audit.EventQuery; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class MongoAuditProvider implements AuditProvider { + + private DBCollection audit; + + public MongoAuditProvider(DBCollection audit) { + this.audit = audit; + } + + @Override + public EventQuery createQuery() { + return new MongoEventQuery(audit); + } + + @Override + public void clear() { + audit.remove(new BasicDBObject()); + } + + @Override + public void clear(long olderThan) { + audit.remove(new BasicDBObject("time", new BasicDBObject("$lt", olderThan))); + } + + @Override + public void onEvent(Event event) { + audit.insert(convert(event)); + } + + @Override + public void close() { + } + + static DBObject convert(Event o) { + BasicDBObject e = new BasicDBObject(); + e.put("time", o.getTime()); + e.put("event", o.getEvent()); + e.put("realmId", o.getRealmId()); + e.put("clientId", o.getClientId()); + e.put("userId", o.getUserId()); + e.put("ipAddress", o.getIpAddress()); + e.put("error", o.getError()); + + BasicDBObject details = new BasicDBObject(); + for (Map.Entry entry : o.getDetails().entrySet()) { + details.put(entry.getKey(), entry.getValue()); + } + e.put("details", details); + + return e; + } + + static Event convert(BasicDBObject o) { + Event e = new Event(); + e.setTime(o.getLong("time")); + e.setEvent(o.getString("event")); + e.setRealmId(o.getString("realmId")); + e.setClientId(o.getString("clientId")); + e.setUserId(o.getString("userId")); + e.setIpAddress(o.getString("ipAddress")); + e.setError(o.getString("error")); + + BasicDBObject d = (BasicDBObject) o.get("details"); + Map details = new HashMap(); + for (Object k : d.keySet()) { + details.put((String) k, d.getString((String) k)); + } + + e.setDetails(details); + return e; + } + +} diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java new file mode 100644 index 00000000000..5fb7a83e727 --- /dev/null +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java @@ -0,0 +1,50 @@ +package org.keycloak.audit.mongo; + +import com.mongodb.DB; +import com.mongodb.MongoClient; +import com.mongodb.WriteConcern; +import org.keycloak.audit.AuditProvider; +import org.keycloak.audit.AuditProviderFactory; + +import java.net.UnknownHostException; + +/** + * @author Stian Thorgersen + */ +public class MongoAuditProviderFactory implements AuditProviderFactory { + + private static final String MONGO_HOST = "keycloak.audit.mongo.host"; + private static final String MONGO_PORT = "keycloak.audit.mongo.port"; + private static final String MONGO_DB_NAME = "keycloak.audit.mongo.db"; + + public static final String ID = "mongo"; + private MongoClient client; + private DB db; + + @Override + public AuditProvider create() { + return new MongoAuditProvider(db.getCollection("audit")); + } + + @Override + public void init() { + try { + client = new MongoClient(System.getProperty(MONGO_HOST, "localhost"), Integer.parseInt(System.getProperty(MONGO_PORT, "27017"))); + client.setWriteConcern(WriteConcern.UNACKNOWLEDGED); + db = client.getDB(System.getProperty(MONGO_DB_NAME, "keycloak-audit")); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + client.close(); + } + + @Override + public String getId() { + return ID; + } + +} diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java new file mode 100644 index 00000000000..e425455f366 --- /dev/null +++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java @@ -0,0 +1,81 @@ +package org.keycloak.audit.mongo; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import org.keycloak.audit.Event; +import org.keycloak.audit.EventQuery; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class MongoEventQuery implements EventQuery { + + private Integer firstResult; + private Integer maxResults; + private DBCollection audit; + private final BasicDBObject query; + + public MongoEventQuery(DBCollection audit) { + this.audit = audit; + query = new BasicDBObject(); + } + + @Override + public EventQuery event(String event) { + query.put("event", event); + return this; + } + + @Override + public EventQuery realm(String realmId) { + query.put("realmId", realmId); + return this; + } + + @Override + public EventQuery client(String clientId) { + query.put("clientId", clientId); + return this; + } + + @Override + public EventQuery user(String userId) { + query.put("userId", userId); + return this; + } + + @Override + public EventQuery firstResult(int firstResult) { + this.firstResult = firstResult; + return this; + } + + @Override + public EventQuery maxResults(int maxResults) { + this.maxResults = maxResults; + return this; + } + + @Override + public List getResultList() { + DBCursor cur = audit.find(query); + if (firstResult != null) { + cur.skip(firstResult); + } + if (maxResults != null) { + cur.limit(maxResults); + } + + List events = new LinkedList(); + while (cur.hasNext()) { + events.add(MongoAuditProvider.convert((BasicDBObject) cur.next())); + } + + return events; + } + +} diff --git a/audit/mongo/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory b/audit/mongo/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory new file mode 100644 index 00000000000..ea4454d2fcb --- /dev/null +++ b/audit/mongo/src/main/resources/META-INF/services/org.keycloak.audit.AuditProviderFactory @@ -0,0 +1 @@ +org.keycloak.audit.mongo.MongoAuditProviderFactory \ No newline at end of file diff --git a/audit/mongo/src/test/java/org/keycloak/audit/mongo/MongoAuditProviderTest.java b/audit/mongo/src/test/java/org/keycloak/audit/mongo/MongoAuditProviderTest.java new file mode 100644 index 00000000000..5ef91ad9852 --- /dev/null +++ b/audit/mongo/src/test/java/org/keycloak/audit/mongo/MongoAuditProviderTest.java @@ -0,0 +1,15 @@ +package org.keycloak.audit.mongo; + +import org.keycloak.audit.tests.AbstractAuditProviderTest; + +/** + * @author Stian Thorgersen + */ +public class MongoAuditProviderTest extends AbstractAuditProviderTest { + + @Override + protected String getProviderId() { + return MongoAuditProviderFactory.ID; + } + +} diff --git a/audit/pom.xml b/audit/pom.xml new file mode 100755 index 00000000000..ed125d16144 --- /dev/null +++ b/audit/pom.xml @@ -0,0 +1,25 @@ + + + keycloak-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + ../pom.xml + + + Audit Parent + + 4.0.0 + + org.keycloak + keycloak-audit-parent + pom + + + api + jpa + jboss-logging + mongo + tests + + diff --git a/audit/tests/pom.xml b/audit/tests/pom.xml new file mode 100755 index 00000000000..1b8cac1d96d --- /dev/null +++ b/audit/tests/pom.xml @@ -0,0 +1,35 @@ + + + + keycloak-audit-parent + org.keycloak + 1.0-beta-1-SNAPSHOT + + 4.0.0 + + keycloak-audit-tests + Keycloak Audit Tests + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-audit-api + ${project.version} + provided + + + junit + junit + provided + + + diff --git a/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java new file mode 100644 index 00000000000..c5d1bb7fe11 --- /dev/null +++ b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java @@ -0,0 +1,121 @@ +package org.keycloak.audit.tests; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.audit.AuditProvider; +import org.keycloak.audit.AuditProviderFactory; +import org.keycloak.audit.Event; +import org.keycloak.provider.ProviderFactoryLoader; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public abstract class AbstractAuditProviderTest { + + private AuditProviderFactory factory; + private AuditProvider provider; + + @Before + public void before() { + ProviderFactoryLoader loader = ProviderFactoryLoader.load(AuditProviderFactory.class); + factory = loader.find(getProviderId()); + factory.init(); + + provider = factory.create(); + } + + protected abstract String getProviderId(); + + @After + public void after() { + provider.clear(); + provider.close(); + factory.close(); + } + + @Test + public void save() { + provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + } + + @Test + public void query() { + provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create("event2", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create("event", "realmId2", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create("event", "realmId", "clientId2", "userId", "127.0.0.1", "error")); + provider.onEvent(create("event", "realmId", "clientId", "userId2", "127.0.0.1", "error")); + + provider.close(); + provider = factory.create(); + + Assert.assertEquals(4, provider.createQuery().client("clientId").getResultList().size()); + Assert.assertEquals(4, provider.createQuery().realm("realmId").getResultList().size()); + Assert.assertEquals(4, provider.createQuery().event("event").getResultList().size()); + Assert.assertEquals(4, provider.createQuery().user("userId").getResultList().size()); + + Assert.assertEquals(1, provider.createQuery().user("userId").event("event2").getResultList().size()); + + Assert.assertEquals(2, provider.createQuery().maxResults(2).getResultList().size()); + Assert.assertEquals(1, provider.createQuery().firstResult(4).getResultList().size()); + } + + @Test + public void clear() { + provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + + provider.close(); + provider = factory.create(); + + provider.clear(); + + Assert.assertEquals(0, provider.createQuery().getResultList().size()); + } + + @Test + public void clearOld() { + provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error")); + + provider.close(); + provider = factory.create(); + + provider.clear(System.currentTimeMillis() - 10000); + + Assert.assertEquals(2, provider.createQuery().getResultList().size()); + } + + private Event create(String event, String realmId, String clientId, String userId, String ipAddress, String error) { + return create(System.currentTimeMillis(), event, realmId, clientId, userId, ipAddress, error); + } + + private Event create(long time, String event, String realmId, String clientId, String userId, String ipAddress, String error) { + Event e = new Event(); + e.setTime(time); + e.setEvent(event); + e.setRealmId(realmId); + e.setClientId(clientId); + e.setUserId(userId); + e.setIpAddress(ipAddress); + e.setError(error); + + Map details = new HashMap(); + details.put("key1", "value1"); + details.put("key2", "value2"); + + e.setDetails(details); + + return e; + } + +} diff --git a/core-jaxrs/pom.xml b/core-jaxrs/pom.xml index 7174d6f92af..2ca0392b71e 100755 --- a/core-jaxrs/pom.xml +++ b/core-jaxrs/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -61,8 +62,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/core/pom.xml b/core/pom.xml index 1a5363b2cf3..2d4b51f181b 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -45,8 +46,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/core/src/main/java/org/keycloak/provider/Provider.java b/core/src/main/java/org/keycloak/provider/Provider.java new file mode 100644 index 00000000000..3401266508d --- /dev/null +++ b/core/src/main/java/org/keycloak/provider/Provider.java @@ -0,0 +1,10 @@ +package org.keycloak.provider; + +/** + * @author Stian Thorgersen + */ +public interface Provider { + + public void close(); + +} diff --git a/core/src/main/java/org/keycloak/provider/ProviderFactory.java b/core/src/main/java/org/keycloak/provider/ProviderFactory.java new file mode 100644 index 00000000000..720538dc104 --- /dev/null +++ b/core/src/main/java/org/keycloak/provider/ProviderFactory.java @@ -0,0 +1,16 @@ +package org.keycloak.provider; + +/** + * @author Stian Thorgersen + */ +public interface ProviderFactory { + + public T create(); + + public void init(); + + public void close(); + + public String getId(); + +} diff --git a/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java b/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java new file mode 100644 index 00000000000..51b00dc4175 --- /dev/null +++ b/core/src/main/java/org/keycloak/provider/ProviderFactoryLoader.java @@ -0,0 +1,88 @@ +package org.keycloak.provider; + +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * @author Stian Thorgersen + */ +public class ProviderFactoryLoader

    implements Iterable

    { + + private ServiceLoader

    serviceLoader; + + private ProviderFactoryLoader(ServiceLoader

    serviceLoader) { + this.serviceLoader = serviceLoader; + } + + public static

    ProviderFactoryLoader

    load(Class

    service) { + return new ProviderFactoryLoader(ServiceLoader.load(service)); + } + + public static

    ProviderFactoryLoader

    load(Class

    service, ClassLoader loader) { + return new ProviderFactoryLoader(ServiceLoader.load(service, loader)); + } + + public P find(String id) { + Iterator

    itr = iterator(); + while (itr.hasNext()) { + P p = itr.next(); + if (p.getId() != null && p.getId().equals(id)) { + return p; + } + } + return null; + } + + @Override + public Iterator

    iterator() { + return new ProviderFactoryIterator(serviceLoader.iterator()); + } + + public void close() { + + } + + private static class ProviderFactoryIterator

    implements Iterator

    { + + private Iterator

    itr; + + private P next; + + private ProviderFactoryIterator(Iterator

    itr) { + this.itr = itr; + setNext(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public P next() { + P n = next; + setNext(); + return n; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private void setNext() { + next = null; + while (itr.hasNext()) { + if (itr.hasNext()) { + P n = itr.next(); + if (!System.getProperties().containsKey(n.getClass().getName() + ".disabled")) { + next = n; + return; + } + } + } + } + + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java deleted file mode 100644 index ecbfb8d6bbd..00000000000 --- a/core/src/main/java/org/keycloak/representations/idm/AuthenticationMappingRepresentation.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.keycloak.representations.idm; - -import java.util.List; - -/** - * @author Marek Posolda - */ -public class AuthenticationMappingRepresentation { - - protected String self; // link - protected String username; - protected List authenticationLinks; - - public String getSelf() { - return self; - } - - public void setSelf(String self) { - this.self = self; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public List getAuthenticationLinks() { - return authenticationLinks; - } - - public void setAuthenticationLinks(List authenticationLinks) { - this.authenticationLinks = authenticationLinks; - } - - -} 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 fc72ef02c7b..a4278d254fe 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -39,7 +39,6 @@ public class RealmRepresentation { protected Map> applicationRoleMappings; protected Map> applicationScopeMappings; protected List socialMappings; - protected List authenticationMappings; protected List applications; protected List oauthClients; protected Map socialProviders; @@ -181,18 +180,6 @@ public class RealmRepresentation { return mapping; } - public List getAuthenticationMappings() { - return authenticationMappings; - } - - public AuthenticationMappingRepresentation authenticationMapping(String username) { - AuthenticationMappingRepresentation mapping = new AuthenticationMappingRepresentation(); - mapping.setUsername(username); - if (authenticationMappings == null) authenticationMappings = new ArrayList(); - authenticationMappings.add(mapping); - return mapping; - } - public Set getRequiredCredentials() { return requiredCredentials; } diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 5fc55f0b7cc..43aa368f7ad 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -20,6 +20,7 @@ public class UserRepresentation { protected String firstName; protected String lastName; protected String email; + protected AuthenticationLinkRepresentation authenticationLink; protected Map attributes; protected List credentials; protected List requiredActions; @@ -96,6 +97,14 @@ public class UserRepresentation { this.emailVerified = emailVerified; } + public AuthenticationLinkRepresentation getAuthenticationLink() { + return authenticationLink; + } + + public void setAuthenticationLink(AuthenticationLinkRepresentation authenticationLink) { + this.authenticationLink = authenticationLink; + } + public Map getAttributes() { return attributes; } diff --git a/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java b/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java index 111c4a40959..1e05af473f0 100755 --- a/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java +++ b/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java @@ -620,8 +620,6 @@ public class KeycloakUriBuilder { } // don't set values if values is null if (values == null) return this; - // don't set values if values is null - if (values == null) return this; return queryParam(name, values); } diff --git a/distribution/appliance-dist/pom.xml b/distribution/appliance-dist/pom.xml index 1b20c25701e..c1a7bb45e99 100755 --- a/distribution/appliance-dist/pom.xml +++ b/distribution/appliance-dist/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/as7-adapter-zip/pom.xml b/distribution/as7-adapter-zip/pom.xml index ae9707507aa..d2379abf032 100755 --- a/distribution/as7-adapter-zip/pom.xml +++ b/distribution/as7-adapter-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/eap6-adapter-zip/pom.xml b/distribution/eap6-adapter-zip/pom.xml index 1d91b1824f3..153a4507ea9 100755 --- a/distribution/eap6-adapter-zip/pom.xml +++ b/distribution/eap6-adapter-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/examples-docs-zip/pom.xml b/distribution/examples-docs-zip/pom.xml index d1e953f2dcb..ee31f51c0ce 100755 --- a/distribution/examples-docs-zip/pom.xml +++ b/distribution/examples-docs-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/pom.xml b/distribution/pom.xml index ed51ec97521..3da5520b671 100755 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -10,7 +10,6 @@ 4.0.0 - org.keycloak distribution-pom pom diff --git a/distribution/src-dist/pom.xml b/distribution/src-dist/pom.xml index c31c69d2a81..95d7c124312 100755 --- a/distribution/src-dist/pom.xml +++ b/distribution/src-dist/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/theme-template-zip/pom.xml b/distribution/theme-template-zip/pom.xml index 86c1cd3b194..5742cc56954 100755 --- a/distribution/theme-template-zip/pom.xml +++ b/distribution/theme-template-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/war-dist/pom.xml b/distribution/war-dist/pom.xml index e4950e412e5..19a0980eb26 100755 --- a/distribution/war-dist/pom.xml +++ b/distribution/war-dist/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/war-zip/pom.xml b/distribution/war-zip/pom.xml index 599f4c8b719..3c75d9cb1bf 100755 --- a/distribution/war-zip/pom.xml +++ b/distribution/war-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/distribution/wildfly-adapter-zip/pom.xml b/distribution/wildfly-adapter-zip/pom.xml index 7e9ae0b4c38..9c55d04a899 100755 --- a/distribution/wildfly-adapter-zip/pom.xml +++ b/distribution/wildfly-adapter-zip/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 keycloak-parent diff --git a/examples/demo-template/customer-app-cli/pom.xml b/examples/demo-template/customer-app-cli/pom.xml index 363bc3ea93e..ac8ffb39897 100755 --- a/examples/demo-template/customer-app-cli/pom.xml +++ b/examples/demo-template/customer-app-cli/pom.xml @@ -43,8 +43,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/customer-app-js/pom.xml b/examples/demo-template/customer-app-js/pom.xml index 0b75bb3825c..d230c4194fb 100755 --- a/examples/demo-template/customer-app-js/pom.xml +++ b/examples/demo-template/customer-app-js/pom.xml @@ -33,8 +33,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/customer-app/pom.xml b/examples/demo-template/customer-app/pom.xml index 34cfa9fc231..f35553bac14 100755 --- a/examples/demo-template/customer-app/pom.xml +++ b/examples/demo-template/customer-app/pom.xml @@ -67,8 +67,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/database-service/pom.xml b/examples/demo-template/database-service/pom.xml index ebf0afcfeb7..f85e1c5a527 100755 --- a/examples/demo-template/database-service/pom.xml +++ b/examples/demo-template/database-service/pom.xml @@ -78,8 +78,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml index 2c98df45fc6..8736c88cd88 100755 --- a/examples/demo-template/pom.xml +++ b/examples/demo-template/pom.xml @@ -10,7 +10,6 @@ 4.0.0 - org.keycloak demo-pom pom diff --git a/examples/demo-template/product-app/pom.xml b/examples/demo-template/product-app/pom.xml index f5c5ca443cb..0b4b6f4c8b2 100755 --- a/examples/demo-template/product-app/pom.xml +++ b/examples/demo-template/product-app/pom.xml @@ -72,8 +72,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/third-party-cdi/pom.xml b/examples/demo-template/third-party-cdi/pom.xml index e98f7fe133c..ab1d5ad552c 100755 --- a/examples/demo-template/third-party-cdi/pom.xml +++ b/examples/demo-template/third-party-cdi/pom.xml @@ -83,8 +83,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/demo-template/third-party/pom.xml b/examples/demo-template/third-party/pom.xml index 5142f7b56ae..0b2b478da6f 100755 --- a/examples/demo-template/third-party/pom.xml +++ b/examples/demo-template/third-party/pom.xml @@ -65,8 +65,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/js-console/pom.xml b/examples/js-console/pom.xml index 1e8dc6a0f8a..9c1ca74971b 100755 --- a/examples/js-console/pom.xml +++ b/examples/js-console/pom.xml @@ -33,8 +33,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/examples/pom.xml b/examples/pom.xml index 7c5c1448091..15fd6e26430 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -10,7 +10,6 @@ 4.0.0 - org.keycloak examples-pom pom diff --git a/forms/account-api/pom.xml b/forms/account-api/pom.xml index c20057510e3..e153d52f0d4 100755 --- a/forms/account-api/pom.xml +++ b/forms/account-api/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -39,8 +40,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/account-freemarker/pom.xml b/forms/account-freemarker/pom.xml index 26b72c209be..c438fedd093 100755 --- a/forms/account-freemarker/pom.xml +++ b/forms/account-freemarker/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -60,8 +61,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/common-freemarker/pom.xml b/forms/common-freemarker/pom.xml index 64f4043b663..eaac9f3c9ed 100755 --- a/forms/common-freemarker/pom.xml +++ b/forms/common-freemarker/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -44,8 +45,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/common-themes/pom.xml b/forms/common-themes/pom.xml index 338fdf2dba2..cb1d6f96738 100755 --- a/forms/common-themes/pom.xml +++ b/forms/common-themes/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -44,8 +45,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/login-api/pom.xml b/forms/login-api/pom.xml index 6ba35dc17c8..9b44836f0be 100755 --- a/forms/login-api/pom.xml +++ b/forms/login-api/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -39,8 +40,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml index 763ae02feb8..d1cc8294f7a 100755 --- a/forms/login-freemarker/pom.xml +++ b/forms/login-freemarker/pom.xml @@ -1,5 +1,6 @@ - + keycloak-forms org.keycloak @@ -65,8 +66,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/forms/pom.xml b/forms/pom.xml index b658295398c..f70ac61dcb8 100755 --- a/forms/pom.xml +++ b/forms/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak diff --git a/integration/adapter-core/pom.xml b/integration/adapter-core/pom.xml index f0d9ec0efc2..9a57dbacb5d 100755 --- a/integration/adapter-core/pom.xml +++ b/integration/adapter-core/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -58,8 +59,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/as7-eap-subsystem/pom.xml b/integration/as7-eap-subsystem/pom.xml index 43a79e66cc4..8c8f472d361 100755 --- a/integration/as7-eap-subsystem/pom.xml +++ b/integration/as7-eap-subsystem/pom.xml @@ -24,22 +24,18 @@ ../../pom.xml - org.keycloak keycloak-as7-subsystem - 1.0-beta-1-SNAPSHOT - Keycloak Wildfly Subsystem - jar + org.apache.maven.plugins maven-compiler-plugin - 2.3.1 - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/as7-eap6/adapter/pom.xml b/integration/as7-eap6/adapter/pom.xml index 30b844cd298..eaa83deb1bb 100755 --- a/integration/as7-eap6/adapter/pom.xml +++ b/integration/as7-eap6/adapter/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -88,8 +89,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/installed/pom.xml b/integration/installed/pom.xml index 78860cefd5b..09f3e1a74af 100755 --- a/integration/installed/pom.xml +++ b/integration/installed/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -52,8 +53,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/jaxrs-oauth-client/pom.xml b/integration/jaxrs-oauth-client/pom.xml index 70cda06ac53..f4243d8addd 100755 --- a/integration/jaxrs-oauth-client/pom.xml +++ b/integration/jaxrs-oauth-client/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -61,8 +62,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/jboss-adapter-core/pom.xml b/integration/jboss-adapter-core/pom.xml index b37011fb3a1..42ec33603a5 100755 --- a/integration/jboss-adapter-core/pom.xml +++ b/integration/jboss-adapter-core/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -73,8 +74,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/js/pom.xml b/integration/js/pom.xml index 335f432c40c..fdfc8a30459 100755 --- a/integration/js/pom.xml +++ b/integration/js/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -21,8 +22,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/pom.xml b/integration/pom.xml index 59048230aca..366904c5719 100755 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -10,7 +10,6 @@ 4.0.0 - org.keycloak integration-pom pom diff --git a/integration/servlet-oauth-client/pom.xml b/integration/servlet-oauth-client/pom.xml index 9611cccb624..68b0f81e969 100755 --- a/integration/servlet-oauth-client/pom.xml +++ b/integration/servlet-oauth-client/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -58,8 +59,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/undertow/pom.xml b/integration/undertow/pom.xml index fe233f61b3e..44c6a0a76e6 100755 --- a/integration/undertow/pom.xml +++ b/integration/undertow/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -82,8 +83,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/wildfly-adapter/pom.xml b/integration/wildfly-adapter/pom.xml index a39d1b8a8e1..e563cefc066 100755 --- a/integration/wildfly-adapter/pom.xml +++ b/integration/wildfly-adapter/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -98,8 +99,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/integration/wildfly-subsystem/pom.xml b/integration/wildfly-subsystem/pom.xml index da6f73af44d..7c006b6cdff 100755 --- a/integration/wildfly-subsystem/pom.xml +++ b/integration/wildfly-subsystem/pom.xml @@ -24,22 +24,18 @@ ../../pom.xml - org.keycloak keycloak-wildfly-subsystem - 1.0-beta-1-SNAPSHOT - Keycloak Wildfly Subsystem - jar + org.apache.maven.plugins maven-compiler-plugin - 2.3.1 - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/model/api/pom.xml b/model/api/pom.xml index 6052c293b1a..fbcfd5336e9 100755 --- a/model/api/pom.xml +++ b/model/api/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -40,8 +41,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/model/api/src/main/java/org/keycloak/models/AuthenticationProviderModel.java b/model/api/src/main/java/org/keycloak/models/AuthenticationProviderModel.java index 6a0f1c6b38b..7dea3c96985 100644 --- a/model/api/src/main/java/org/keycloak/models/AuthenticationProviderModel.java +++ b/model/api/src/main/java/org/keycloak/models/AuthenticationProviderModel.java @@ -1,5 +1,6 @@ package org.keycloak.models; +import java.util.Collections; import java.util.Map; /** @@ -7,6 +8,8 @@ import java.util.Map; */ public class AuthenticationProviderModel { + public static final AuthenticationProviderModel DEFAULT_PROVIDER = new AuthenticationProviderModel("model", true, Collections.EMPTY_MAP); + private String providerName; private boolean passwordUpdateSupported = true; private Map config; 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 308dd7caac9..db271f25555 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -138,11 +138,9 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa boolean removeSocialLink(UserModel user, String socialProvider); - UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink); + AuthenticationLinkModel getAuthenticationLink(UserModel user); - Set getAuthenticationLinks(UserModel user); - - void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink); + void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink); boolean isSocial(); @@ -209,4 +207,8 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa void setNotBefore(int notBefore); boolean removeRoleById(String id); + + Set getAuditListeners(); + + void setAuditListeners(Set listeners); } diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index 617b8f487bf..0fec0afa0b3 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -86,8 +87,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} 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 05136508dbe..0275f40b150 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 @@ -40,6 +40,8 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -429,7 +431,9 @@ public class RealmAdapter implements RealmModel { private void removeUser(UserEntity user) { em.createQuery("delete from " + UserRoleMappingEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); em.createQuery("delete from " + SocialLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); - em.createQuery("delete from " + AuthenticationLinkEntity.class.getSimpleName() + " where user = :user").setParameter("user", user).executeUpdate(); + if (user.getAuthenticationLink() != null) { + em.remove(user.getAuthenticationLink()); + } em.remove(user); } @@ -648,43 +652,22 @@ public class RealmAdapter implements RealmModel { } @Override - public UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink) { - TypedQuery query = em.createNamedQuery("findUserByAuthLinkAndRealm", UserEntity.class); - query.setParameter("realm", realm); - query.setParameter("authProvider", authenticationLink.getAuthProvider()); - query.setParameter("authUserId", authenticationLink.getAuthUserId()); - List results = query.getResultList(); - if (results.isEmpty()) { - return null; - } else if (results.size() > 1) { - throw new IllegalStateException("More results found for authenticationProvider=" + authenticationLink.getAuthProvider() + - ", authUserId=" + authenticationLink.getAuthUserId() + ", results=" + results); - } else { - UserEntity user = results.get(0); - return new UserAdapter(user); - } + public AuthenticationLinkModel getAuthenticationLink(UserModel user) { + UserEntity userEntity = ((UserAdapter) user).getUser(); + AuthenticationLinkEntity authLinkEntity = userEntity.getAuthenticationLink(); + return authLinkEntity == null ? null : new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); } @Override - public Set getAuthenticationLinks(UserModel user) { - TypedQuery query = em.createNamedQuery("findAuthLinkByUser", AuthenticationLinkEntity.class); - query.setParameter("user", ((UserAdapter) user).getUser()); - List results = query.getResultList(); - Set set = new HashSet(); - for (AuthenticationLinkEntity entity : results) { - set.add(new AuthenticationLinkModel(entity.getAuthProvider(), entity.getAuthUserId())); - } - return set; - } - - @Override - public void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { + public void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { AuthenticationLinkEntity entity = new AuthenticationLinkEntity(); - entity.setRealm(realm); entity.setAuthProvider(authenticationLink.getAuthProvider()); entity.setAuthUserId(authenticationLink.getAuthUserId()); - entity.setUser(((UserAdapter) user).getUser()); + + UserEntity userEntity = ((UserAdapter) user).getUser(); + userEntity.setAuthenticationLink(entity); em.persist(entity); + em.persist(userEntity); em.flush(); } @@ -850,7 +833,15 @@ public class RealmAdapter implements RealmModel { @Override public List getAuthenticationProviders() { - Collection entities = realm.getAuthenticationProviders(); + List entities = realm.getAuthenticationProviders(); + Collections.sort(entities, new Comparator() { + + @Override + public int compare(AuthenticationProviderEntity o1, AuthenticationProviderEntity o2) { + return o1.getPriority() - o2.getPriority(); + } + + }); List result = new ArrayList(); for (AuthenticationProviderEntity entity : entities) { result.add(new AuthenticationProviderModel(entity.getProviderName(), entity.isPasswordUpdateSupported(), entity.getConfig())); @@ -862,11 +853,13 @@ public class RealmAdapter implements RealmModel { @Override public void setAuthenticationProviders(List authenticationProviders) { List newEntities = new ArrayList(); + int counter = 1; for (AuthenticationProviderModel model : authenticationProviders) { AuthenticationProviderEntity entity = new AuthenticationProviderEntity(); entity.setProviderName(model.getProviderName()); entity.setPasswordUpdateSupported(model.isPasswordUpdateSupported()); entity.setConfig(model.getConfig()); + entity.setPriority(counter++); newEntities.add(entity); } @@ -1190,4 +1183,15 @@ public class RealmAdapter implements RealmModel { realm.setAccountTheme(name); em.flush(); } + + @Override + public Set getAuditListeners() { + return realm.getAuditListeners(); + } + + @Override + public void setAuditListeners(Set listeners) { + realm.setAuditListeners(listeners); + em.flush(); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java index 0eecfba5be1..e6ae8d421c4 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationLinkEntity.java @@ -6,16 +6,13 @@ import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; import org.hibernate.annotations.GenericGenerator; /** * @author Marek Posolda */ -@NamedQueries({ - @NamedQuery(name="findAuthLinkByUser", query="select link from AuthenticationLinkEntity link where link.user = :user"), - @NamedQuery(name="findUserByAuthLinkAndRealm", query="select link.user from AuthenticationLinkEntity link where link.realm = :realm and link.authProvider = :authProvider and link.authUserId = :authUserId") -}) @Entity public class AuthenticationLinkEntity { @@ -24,12 +21,6 @@ public class AuthenticationLinkEntity { @GeneratedValue(generator = "keycloak_generator") private String id; - @ManyToOne - private UserEntity user; - - @ManyToOne - protected RealmEntity realm; - protected String authProvider; protected String authUserId; @@ -41,22 +32,6 @@ public class AuthenticationLinkEntity { this.id = id; } - public UserEntity getUser() { - return user; - } - - public void setUser(UserEntity user) { - this.user = user; - } - - public RealmEntity getRealm() { - return realm; - } - - public void setRealm(RealmEntity realm) { - this.realm = realm; - } - public String getAuthProvider() { return authProvider; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationProviderEntity.java index 95e113d011e..8517ad28a3f 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationProviderEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationProviderEntity.java @@ -25,6 +25,7 @@ public class AuthenticationProviderEntity { private String providerName; private boolean passwordUpdateSupported; + private int priority; @ElementCollection @MapKeyColumn(name="name") @@ -56,6 +57,14 @@ public class AuthenticationProviderEntity { this.passwordUpdateSupported = passwordUpdateSupported; } + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + public Map getConfig() { return config; } 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 255b73737cb..1e3a9d723da 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 @@ -16,7 +16,10 @@ import javax.persistence.OneToMany; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -66,7 +69,7 @@ public class RealmEntity { @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true) @JoinTable(name="AuthProviders") - Collection authenticationProviders = new ArrayList(); + List authenticationProviders = new ArrayList(); @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection applications = new ArrayList(); @@ -96,6 +99,9 @@ public class RealmEntity { @JoinTable(name="RealmDefaultRoles") Collection defaultRoles = new ArrayList(); + @ElementCollection + protected Set auditListeners= new HashSet(); + public String getId() { return id; } @@ -240,11 +246,11 @@ public class RealmEntity { this.requiredCredentials = requiredCredentials; } - public Collection getAuthenticationProviders() { + public List getAuthenticationProviders() { return authenticationProviders; } - public void setAuthenticationProviders(Collection authenticationProviders) { + public void setAuthenticationProviders(List authenticationProviders) { this.authenticationProviders = authenticationProviders; } @@ -342,5 +348,13 @@ public class RealmEntity { public void setBruteForceProtected(boolean bruteForceProtected) { this.bruteForceProtected = bruteForceProtected; } + + public Set getAuditListeners() { + return auditListeners; + } + + public void setAuditListeners(Set auditListeners) { + this.auditListeners = auditListeners; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index 156aaaa28b3..1ffba3ba806 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -15,6 +15,8 @@ import javax.persistence.MapKeyColumn; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; +import javax.persistence.OneToOne; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -65,6 +67,9 @@ public class UserEntity { @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) protected Collection credentials = new ArrayList(); + @OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true) + protected AuthenticationLinkEntity authenticationLink; + public String getId() { return id; } @@ -161,6 +166,14 @@ public class UserEntity { this.credentials = credentials; } + public AuthenticationLinkEntity getAuthenticationLink() { + return authenticationLink; + } + + public void setAuthenticationLink(AuthenticationLinkEntity authenticationLink) { + this.authenticationLink = authenticationLink; + } + public int getNotBefore() { return notBefore; } diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml index 1436419bf7a..53a46874808 100755 --- a/model/mongo/pom.xml +++ b/model/mongo/pom.xml @@ -75,8 +75,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} 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 6f078855e64..8d3943b257a 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 @@ -39,6 +39,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -968,41 +969,26 @@ public class RealmAdapter extends AbstractMongoAdapter implements R } @Override - public UserModel getUserByAuthenticationLink(AuthenticationLinkModel authenticationLink) { - DBObject query = new QueryBuilder() - .and("authenticationLinks.authProvider").is(authenticationLink.getAuthProvider()) - .and("authenticationLinks.authUserId").is(authenticationLink.getAuthUserId()) - .and("realmId").is(getId()) - .get(); - UserEntity userEntity = getMongoStore().loadSingleEntity(UserEntity.class, query, invocationContext); - return userEntity==null ? null : new UserAdapter(userEntity, invocationContext); - } - - @Override - public Set getAuthenticationLinks(UserModel user) { + public AuthenticationLinkModel getAuthenticationLink(UserModel user) { UserEntity userEntity = ((UserAdapter)user).getUser(); - List linkEntities = userEntity.getAuthenticationLinks(); + AuthenticationLinkEntity authLinkEntity = userEntity.getAuthenticationLink(); - if (linkEntities == null) { - return Collections.EMPTY_SET; + if (authLinkEntity == null) { + return null; + } else { + return new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); } - - Set result = new HashSet(); - for (AuthenticationLinkEntity authLinkEntity : linkEntities) { - AuthenticationLinkModel model = new AuthenticationLinkModel(authLinkEntity.getAuthProvider(), authLinkEntity.getAuthUserId()); - result.add(model); - } - return result; } @Override - public void addAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { + public void setAuthenticationLink(UserModel user, AuthenticationLinkModel authenticationLink) { UserEntity userEntity = ((UserAdapter)user).getUser(); AuthenticationLinkEntity authLinkEntity = new AuthenticationLinkEntity(); authLinkEntity.setAuthProvider(authenticationLink.getAuthProvider()); authLinkEntity.setAuthUserId(authenticationLink.getAuthUserId()); + userEntity.setAuthenticationLink(authLinkEntity); - getMongoStore().pushItemToList(userEntity, "authenticationLinks", authLinkEntity, true, invocationContext); + getMongoStore().updateEntity(userEntity, invocationContext); } protected void updateRealm() { @@ -1155,6 +1141,20 @@ public class RealmAdapter extends AbstractMongoAdapter implements R updateRealm(); } + @Override + public Set getAuditListeners() { + return realm.getAuditListeners() != null ? new HashSet(realm.getAuditListeners()) : null; + } + + @Override + public void setAuditListeners(Set listeners) { + if (listeners != null) { + realm.setAuditListeners(new LinkedList(listeners)); + } else { + realm.setAuditListeners(null); + } + } + @Override public RealmEntity getMongoEntity() { return realm; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java index 991bd07b860..58b9ba295d7 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java @@ -10,8 +10,11 @@ import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Marek Posolda @@ -54,6 +57,8 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong private Map socialConfig = new HashMap(); private Map ldapServerConfig; + private List auditListeners = new LinkedList(); + @MongoField public String getName() { return name; @@ -297,6 +302,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong this.ldapServerConfig = ldapServerConfig; } + @MongoField + public List getAuditListeners() { + return auditListeners; + } + + public void setAuditListeners(List auditListeners) { + this.auditListeners = auditListeners; + } + @Override public void afterRemove(MongoStoreInvocationContext context) { DBObject query = new QueryBuilder() diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java index 4468fd9f7d4..40aeae5cb20 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java @@ -38,7 +38,7 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo private List requiredActions; private List credentials = new ArrayList(); private List socialLinks; - private List authenticationLinks; + private AuthenticationLinkEntity authenticationLink; @MongoField public String getLoginName() { @@ -168,12 +168,12 @@ public class UserEntity extends AbstractMongoIdentifiableEntity implements Mongo } @MongoField - public List getAuthenticationLinks() { - return authenticationLinks; + public AuthenticationLinkEntity getAuthenticationLink() { + return authenticationLink; } - public void setAuthenticationLinks(List authenticationLinks) { - this.authenticationLinks = authenticationLinks; + public void setAuthenticationLink(AuthenticationLinkEntity authenticationLink) { + this.authenticationLink = authenticationLink; } @MongoField diff --git a/model/picketlink/pom.xml b/model/picketlink/pom.xml index ce62835045a..d2250f89137 100755 --- a/model/picketlink/pom.xml +++ b/model/picketlink/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -66,8 +67,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/model/pom.xml b/model/pom.xml index ee8798741b3..7a3ada12755 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -6,11 +6,10 @@ 1.0-beta-1-SNAPSHOT ../pom.xml - Examples + Model Parent 4.0.0 - org.keycloak keycloak-model-pom pom diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java index c0c531c3a74..05374a57655 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersExternalModelTest.java @@ -49,6 +49,8 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { realm2 = realmManager.createRealm("realm2"); realm1.addRequiredCredential(CredentialRepresentation.PASSWORD); realm2.addRequiredCredential(CredentialRepresentation.PASSWORD); + realm1.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER)); + realm2.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER)); UserModel john = realm1.addUser("john"); john.setEnabled(true); @@ -93,10 +95,10 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { Assert.assertEquals("john@email.org", john2.getEmail()); // Verify link exists - Set authLinks = realm2.getAuthenticationLinks(john2); - Assert.assertEquals(1, authLinks.size()); - AuthenticationLinkModel authLink = authLinks.iterator().next(); + AuthenticationLinkModel authLink = realm2.getAuthenticationLink(john2); + Assert.assertNotNull(authLink); Assert.assertEquals(authLink.getAuthProvider(), AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL); + Assert.assertEquals(authLink.getAuthUserId(), realm1.getUser("john").getId()); } finally { ResteasyProviderFactory.clearContextData(); } @@ -108,14 +110,19 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Add externalModel authenticationProvider into realm2 and point to realm1 setupAuthenticationProviders(); + // Add john to realm2 and set authentication link + UserModel john = realm2.addUser("john"); + john.setEnabled(true); + realm2.setAuthenticationLink(john, new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, realm1.getUser("john").getId())); + try { // this is needed for externalModel provider ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession); - // Change credential via realm2 and validate that they are changed in both realms + // Change credential via realm2 and validate that they are changed also in realm1 AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm2); try { - authProviderManager.updatePassword("john", "password-updated"); + Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); @@ -130,14 +137,14 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Change credential and validate that password is updated just for realm2 try { - authProviderManager.updatePassword("john", "password-updated2"); + Assert.assertFalse(authProviderManager.updatePassword(john, "password-updated2")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); } formData = createFormData("john", "password-updated2"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm1, formData)); - Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData)); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm2, formData)); // Allow passwordUpdate propagation again @@ -146,7 +153,7 @@ public class AuthProvidersExternalModelTest extends AbstractModelTest { // Set passwordPolicy for realm1 and verify that password update fail realm1.setPasswordPolicy(new PasswordPolicy("length(8)")); try { - authProviderManager.updatePassword("john", "passw"); + authProviderManager.updatePassword(john, "passw"); Assert.fail("Update not expected to pass"); } catch (AuthenticationProviderException ape) { diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java index 07526c2eeba..91f99dde7a4 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthProvidersLDAPTest.java @@ -95,9 +95,8 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { Assert.assertEquals("john@email.org", john.getEmail()); // Verify link exists - Set authLinks = realm.getAuthenticationLinks(john); - Assert.assertEquals(1, authLinks.size()); - AuthenticationLinkModel authLink = authLinks.iterator().next(); + AuthenticationLinkModel authLink = realm.getAuthenticationLink(john); + Assert.assertNotNull(authLink); Assert.assertEquals(authLink.getAuthProvider(), AuthProviderConstants.PROVIDER_NAME_PICKETLINK); } finally { ResteasyProviderFactory.clearContextData(); @@ -114,6 +113,7 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // Add some user and password to realm UserModel realmUser = realm.addUser("realmUser"); + realmUser.setEnabled(true); UserCredentialModel credential = new UserCredentialModel(); credential.setType(CredentialRepresentation.PASSWORD); credential.setValue("pass"); @@ -135,6 +135,11 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { realmUser.setEnabled(false); formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.ACCOUNT_DISABLED, am.authenticateForm(null, realm, formData)); + + // Successful authentication + realmUser.setEnabled(true); + formData = AuthProvidersExternalModelTest.createFormData("realmUser", "pass"); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); } finally { ResteasyProviderFactory.clearContextData(); } @@ -149,26 +154,34 @@ public class AuthProvidersLDAPTest extends AbstractModelTest { // this is needed for ldap provider ResteasyProviderFactory.pushContext(KeycloakRegistry.class, new KeycloakRegistry()); + LdapTestUtils.setLdapPassword(realm, "john", "password"); + + // First authenticate successfully to sync john into realm + MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password"); + Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); + // Change credential and validate that user can authenticate AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm); + + UserModel john = realm.getUser("john"); try { - authProviderManager.updatePassword("john", "password-updated"); + Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); } - MultivaluedMap formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); + formData = AuthProvidersExternalModelTest.createFormData("john", "password-updated"); Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm, formData)); // Password updated just in LDAP, so validating directly in realm should fail - Assert.assertFalse(realm.validatePassword(realm.getUser("john"), "password-updated")); + Assert.assertFalse(realm.validatePassword(john, "password-updated")); // Switch to not allow updating passwords in ldap AuthProvidersExternalModelTest.setPasswordUpdateForProvider(false, AuthProviderConstants.PROVIDER_NAME_PICKETLINK, realm); // Change credential and validate that password is not updated try { - authProviderManager.updatePassword("john", "password-updated2"); + Assert.assertFalse(authProviderManager.updatePassword(john, "password-updated2")); } catch (AuthenticationProviderException ape) { ape.printStackTrace(); Assert.fail("Error not expected"); diff --git a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java index 1abcf5fbd65..6c1cece290d 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java @@ -3,6 +3,7 @@ package org.keycloak.model.test; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; @@ -14,6 +15,8 @@ import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; + +import java.util.Arrays; import java.util.UUID; public class AuthenticationManagerTest extends AbstractModelTest { @@ -138,6 +141,7 @@ public class AuthenticationManagerTest extends AbstractModelTest { realm.setPublicKeyPem("0234234"); realm.setAccessTokenLifespan(1000); realm.addRequiredCredential(CredentialRepresentation.PASSWORD); + realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER)); am = new AuthenticationManager(); diff --git a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index ce4ca5363d0..fb8ebaabf9b 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -208,24 +208,9 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(authProv3.isPasswordUpdateSupported()); // Test authentication linking - Set authLinks = realm.getAuthenticationLinks(socialUser); - Assert.assertEquals(2, authLinks.size()); - boolean plFound = false; - boolean extFound = false; - for (AuthenticationLinkModel authLinkModel : authLinks) { - if (AuthProviderConstants.PROVIDER_NAME_PICKETLINK.equals(authLinkModel.getAuthProvider())) { - plFound = true; - Assert.assertEquals(authLinkModel.getAuthUserId(), "myUser1"); - } else if (AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL.equals(authLinkModel.getAuthProvider())) { - extFound = true; - Assert.assertEquals(authLinkModel.getAuthUserId(), "myUser11"); - } - } - Assert.assertTrue(plFound && extFound); - - UserModel foundAuthUser = realm.getUserByAuthenticationLink(new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, "myUser1")); - Assert.assertEquals(foundAuthUser.getLoginName(), socialUser.getLoginName()); - Assert.assertNull(realm.getUserByAuthenticationLink(new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, "not-existing"))); + AuthenticationLinkModel authLink = realm.getAuthenticationLink(socialUser); + Assert.assertEquals(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, authLink.getAuthProvider()); + Assert.assertEquals("myUser1", authLink.getAuthUserId()); commit(); diff --git a/model/tests/src/test/resources/testrealm.json b/model/tests/src/test/resources/testrealm.json index b28087d7c8d..56bf23ee715 100755 --- a/model/tests/src/test/resources/testrealm.json +++ b/model/tests/src/test/resources/testrealm.json @@ -75,7 +75,11 @@ }, { "username": "mySocialUser", - "enabled": true + "enabled": true, + "authenticationLink": { + "authProvider": "picketlink", + "authUserId": "myUser1" + } } ], "socialMappings": [ @@ -100,21 +104,6 @@ ] } ], - "authenticationMappings": [ - { - "username": "mySocialUser", - "authenticationLinks": [ - { - "authProvider": "picketlink", - "authUserId": "myUser1" - }, - { - "authProvider": "externalModel", - "authUserId": "myUser11" - } - ] - } - ], "applications": [ { "name": "Application", diff --git a/pom.xml b/pom.xml index 52465540979..344cb1924f3 100755 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,11 @@ 7.1.1.Final 8.0.0.Final 20131018 + + + 1.6 + 1.6 + http://keycloak.org @@ -80,6 +85,7 @@ + audit core core-jaxrs model @@ -437,8 +443,8 @@ maven-compiler-plugin 2.3.1 - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} utf-8 @@ -523,8 +529,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} utf-8 diff --git a/server/pom.xml b/server/pom.xml index 3a23448cdbd..fd5982d3c63 100755 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,6 @@ 4.0.0 - org.keycloak keycloak-server war Keycloak Server @@ -48,6 +47,21 @@ keycloak-model-jpa ${project.version} + + org.keycloak + keycloak-audit-api + ${project.version} + + + org.keycloak + keycloak-audit-jpa + ${project.version} + + + org.keycloak + keycloak-audit-jboss-logging + ${project.version} + org.keycloak keycloak-social-core @@ -182,6 +196,11 @@ keycloak-model-mongo ${project.version} + + org.keycloak + keycloak-audit-mongo + ${project.version} + org.mongodb mongo-java-driver @@ -226,8 +245,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/services/pom.xml b/services/pom.xml index 0571d9251ba..1c68b7124f3 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -36,6 +37,12 @@ ${project.version} provided + + org.keycloak + keycloak-audit-api + ${project.version} + provided + org.keycloak keycloak-account-api @@ -159,8 +166,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java index 51ea3510486..6b9e50c6451 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java @@ -25,6 +25,8 @@ public class AccessCodeEntry { protected String state; protected String redirectUri; protected boolean rememberMe; + protected String authMethod; + protected String username; protected int expiration; protected RealmModel realm; @@ -130,4 +132,20 @@ public class AccessCodeEntry { public void setRememberMe(boolean rememberMe) { this.rememberMe = rememberMe; } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } } 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 54d2f1f770d..ea5e5aa5000 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -1,8 +1,11 @@ package org.keycloak.services.managers; +import java.util.Arrays; + import org.jboss.resteasy.logging.Logger; import org.keycloak.models.AdminRoles; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.Config; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; @@ -13,6 +16,8 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; +import java.util.Collections; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -56,11 +61,14 @@ public class ApplianceBootstrap { realm.setSslNotRequired(true); realm.setRegistrationAllowed(false); manager.generateRealmKeys(realm); + realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER)); ApplicationModel adminConsole = new ApplicationManager(manager).createApplication(realm, Constants.ADMIN_CONSOLE_APPLICATION); adminConsole.setBaseUrl("/auth/admin/index.html"); adminConsole.setEnabled(true); + realm.setAuditListeners(Collections.singleton("jboss-logging")); + RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); adminConsole.addScope(adminRole); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index b24de1bfafc..8bb9e53be48 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -17,8 +17,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ClientConnection; import org.keycloak.services.resources.RealmsResource; import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProviderManager; import org.keycloak.util.Time; @@ -222,6 +221,25 @@ public class AuthenticationManager { protected AuthenticationStatus authenticateInternal(RealmModel realm, MultivaluedMap formData, String username) { UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); + if (user == null) { + AuthUser authUser = AuthenticationProviderManager.getManager(realm).getUser(username); + if (authUser != null) { + // Create new user and link him with authentication provider + user = realm.addUser(authUser.getUsername()); + user.setEnabled(true); + user.setFirstName(authUser.getFirstName()); + user.setLastName(authUser.getLastName()); + user.setEmail(authUser.getEmail()); + realm.setAuthenticationLink(user, new AuthenticationLinkModel(authUser.getProviderName(), authUser.getId())); + } else { + logger.warn("User " + username + " not found"); + return AuthenticationStatus.INVALID_USER; + } + } + + if (!checkEnabled(user)) { + return AuthenticationStatus.ACCOUNT_DISABLED; + } Set types = new HashSet(); @@ -236,20 +254,13 @@ public class AuthenticationManager { return AuthenticationStatus.MISSING_PASSWORD; } - if (user == null && types.contains(CredentialRepresentation.TOTP)) { - logger.warn("User doesn't exists and TOTP is required for the realm"); - return AuthenticationStatus.INVALID_USER; - } - - if (user != null && user.isTotp()) { + if (user.isTotp()) { String token = formData.getFirst(CredentialRepresentation.TOTP); if (token == null) { logger.warn("TOTP token not provided"); return AuthenticationStatus.MISSING_TOTP; } - if (!checkEnabled(user)) { - return AuthenticationStatus.ACCOUNT_DISABLED; - } + logger.debug("validating TOTP"); if (!realm.validateTOTP(user, password, token)) { return AuthenticationStatus.INVALID_CREDENTIALS; @@ -257,58 +268,12 @@ public class AuthenticationManager { } else { logger.debug("validating password for user: " + username); - AuthResult authResult = AuthenticationProviderManager.getManager(realm).validatePassword(username, password); - if (authResult.getAuthProviderStatus() == AuthProviderStatus.INVALID_CREDENTIALS) { + AuthProviderStatus authStatus = AuthenticationProviderManager.getManager(realm).validatePassword(user, password); + if (authStatus == AuthProviderStatus.INVALID_CREDENTIALS) { logger.debug("invalid password for user: " + username); return AuthenticationStatus.INVALID_CREDENTIALS; - } else if (authResult.getAuthProviderStatus() == AuthProviderStatus.USER_NOT_FOUND) { - logger.debug("User " + username + " not found in any Authentication provider"); - return AuthenticationStatus.INVALID_USER; - } - - if (authResult.getAuthenticatedUser() != null) { - AuthenticatedUser authUser = authResult.getAuthenticatedUser(); - AuthenticationLinkModel authLink = new AuthenticationLinkModel(authResult.getProviderName(), authUser.getId()); - user = realm.getUserByAuthenticationLink(authLink); - if (user == null) { - user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); - if (user != null) { - // Case when we already have user with the same username like authenticated, but he is not yet linked to current provider. - // TODO: Revisit if it's ok to link if we allow to change username. Maybe ask user? - // TODO: Update of existing account? - realm.addAuthenticationLink(user, authLink); - logger.info("User " + authUser.getUsername() + " successfully authenticated and linked with provider " + authResult.getProviderName()); - } else { - // Create new user, which has been successfully authenticated and link him with authentication provider - user = realm.addUser(authUser.getUsername()); - user.setEnabled(true); - user.setFirstName(authUser.getFirstName()); - user.setLastName(authUser.getLastName()); - user.setEmail(authUser.getEmail()); - - realm.addAuthenticationLink(user, authLink); - logger.info("User " + username + " successfully authenticated and created based on provider " + authResult.getProviderName()); - } - } else { - // Existing and linked user has been authenticated TODO: Update of existing account? - } - - // Authenticated username could be different from the "form" username. In this case, we will change it - if (!username.equals(user.getLoginName())) { - formData.putSingle(FORM_USERNAME, user.getLoginName()); - logger.debug("Existing user " + user.getLoginName() + " successfully authenticated"); - } - - } else { - // Authentication provider didn't send AuthenticatedUser. Using already retrieved user based on username from "form" - if (user == null) { - logger.warn("User '" + username + "' successfully authenticated, but he doesn't exists and don't know how to create him"); - return AuthenticationStatus.INVALID_USER; - } - } - - if (!checkEnabled(user)) { - return AuthenticationStatus.ACCOUNT_DISABLED; + } else if (authStatus == AuthProviderStatus.FAILED) { + return AuthenticationStatus.FAILED; } } diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index abe30063be8..d9d40d29264 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -21,7 +21,6 @@ import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.AuthenticationLinkRepresentation; -import org.keycloak.representations.idm.AuthenticationMappingRepresentation; import org.keycloak.representations.idm.AuthenticationProviderRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.OAuthClientRepresentation; @@ -37,6 +36,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -81,6 +81,8 @@ public class RealmManager { setupAdminManagement(realm); setupAccountManagement(realm); + realm.setAuditListeners(Collections.singleton("jboss-logging")); + return realm; } @@ -385,15 +387,6 @@ public class RealmManager { } } } - if (rep.getAuthenticationMappings() != null) { - for (AuthenticationMappingRepresentation authMapping : rep.getAuthenticationMappings()) { - UserModel user = userMap.get(authMapping.getUsername()); - for (AuthenticationLinkRepresentation link : authMapping.getAuthenticationLinks()) { - AuthenticationLinkModel mappingModel = new AuthenticationLinkModel(link.getAuthProvider(), link.getAuthUserId()); - newRealm.addAuthenticationLink(user, mappingModel); - } - } - } if (rep.getSmtpServer() != null) { newRealm.setSmtpConfig(new HashMap(rep.getSmtpServer())); @@ -409,6 +402,9 @@ public class RealmManager { if (rep.getAuthenticationProviders() != null) { List authProviderModels = convertAuthenticationProviders(rep.getAuthenticationProviders()); newRealm.setAuthenticationProviders(authProviderModels); + } else { + List authProviderModels = Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER); + newRealm.setAuthenticationProviders(authProviderModels); } } @@ -472,6 +468,11 @@ public class RealmManager { newRealm.updateCredential(user, credential); } } + if (userRep.getAuthenticationLink() != null) { + AuthenticationLinkRepresentation link = userRep.getAuthenticationLink(); + AuthenticationLinkModel authLink = new AuthenticationLinkModel(link.getAuthProvider(), link.getAuthUserId()); + newRealm.setAuthenticationLink(user, authLink); + } return user; } diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index a9e6bcfea18..73688b875bc 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -2,6 +2,8 @@ package org.keycloak.services.managers; import org.jboss.resteasy.logging.Logger; import org.keycloak.OAuthErrorException; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; @@ -98,7 +100,7 @@ public class TokenManager { return code; } - public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken) throws OAuthErrorException { + public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException { JWSInput jws = new JWSInput(encodedRefreshToken); RefreshToken refreshToken = null; try { @@ -117,6 +119,8 @@ public class TokenManager { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token"); } + audit.user(refreshToken.getSubject()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()); + UserModel user = realm.getUserById(refreshToken.getSubject()); if (user == null) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); @@ -320,8 +324,8 @@ public class TokenManager { return encodedToken; } - public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client) { - return new AccessTokenResponseBuilder(realm, client); + public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, Audit audit) { + return new AccessTokenResponseBuilder(realm, client, audit); } public class AccessTokenResponseBuilder { @@ -330,10 +334,12 @@ public class TokenManager { AccessToken accessToken; RefreshToken refreshToken; IDToken idToken; + Audit audit; - public AccessTokenResponseBuilder(RealmModel realm, ClientModel client) { + public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, Audit audit) { this.realm = realm; this.client = client; + this.audit = audit; } public AccessTokenResponseBuilder accessToken(AccessToken accessToken) { @@ -402,7 +408,21 @@ public class TokenManager { return this; } + + public AccessTokenResponse build() { + if (accessToken != null) { + audit.detail(Details.TOKEN_ID, accessToken.getId()); + } + + if (refreshToken != null) { + if (audit.getEvent().getDetails().containsKey(Details.REFRESH_TOKEN_ID)) { + audit.detail(Details.UPDATED_REFRESH_TOKEN_ID, refreshToken.getId()); + } else { + audit.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()); + } + } + AccessTokenResponse res = new AccessTokenResponse(); if (idToken != null) { String encodedToken = new JWSBuilder().jsonContent(idToken).rsa256(realm.getPrivateKey()); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 8a4da96177c..b937674b585 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -27,10 +27,14 @@ import org.keycloak.OAuth2Constants; import org.keycloak.account.Account; import org.keycloak.account.AccountLoader; import org.keycloak.account.AccountPages; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; +import org.keycloak.audit.Events; import org.keycloak.jaxrs.JaxrsOAuthClient; import org.keycloak.models.*; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; @@ -75,11 +79,13 @@ public class AccountService { private final AppAuthManager authManager; private final ApplicationModel application; + private Audit audit; private final SocialRequestManager socialRequestManager; - public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager) { + public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager, SocialRequestManager socialRequestManager, Audit audit) { this.realm = realm; this.application = application; + this.audit = audit; this.authManager = new AppAuthManager(KEYCLOAK_ACCOUNT_IDENTITY_COOKIE, tokenManager); this.socialRequestManager = socialRequestManager; } @@ -170,8 +176,20 @@ public class AccountService { user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); + + String email = formData.getFirst("email"); + String oldEmail = user.getEmail(); + boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; + user.setEmail(formData.getFirst("email")); + audit.event(Events.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).success(); + + if (emailChanged) { + user.setEmailVerified(false); + audit.clone().event(Events.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); + } + return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT); } @@ -184,6 +202,8 @@ public class AccountService { UserModel user = auth.getUser(); user.setTotp(false); + audit.event(Events.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); + Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP); } @@ -215,6 +235,8 @@ public class AccountService { user.setTotp(true); + audit.event(Events.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); + return account.setSuccess("successTotp").createResponse(AccountPages.TOTP); } @@ -242,17 +264,21 @@ public class AccountService { AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm); if (Validation.isEmpty(password)) { return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); - // TODO: This may not work in some cases. For example if ldap username is "foo" but actual loginName of user is "bar", which could theoretically happen... - } else if (authProviderManager.validatePassword(user.getLoginName(), password).getAuthProviderStatus() != AuthProviderStatus.SUCCESS) { + } else if (authProviderManager.validatePassword(user, password) != AuthProviderStatus.SUCCESS) { return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD); } try { - authProviderManager.updatePassword(user.getLoginName(), passwordNew); + boolean passwordUpdateSuccess = authProviderManager.updatePassword(user, passwordNew); + if (!passwordUpdateSuccess) { + return account.setError("Password update failed").createResponse(AccountPages.PASSWORD); + } } catch (AuthenticationProviderException ape) { return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD); } + audit.event(Events.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success(); + return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD); } @@ -298,8 +324,16 @@ public class AccountService { return account.setError(Messages.SOCIAL_REDIRECT_ERROR).createResponse(AccountPages.SOCIAL); } case REMOVE: - if (realm.removeSocialLink(user, providerId)) { + SocialLinkModel link = realm.getSocialLink(user, providerId); + if (link != null) { + realm.removeSocialLink(user, providerId); + logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName()); + + audit.event(Events.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser()) + .detail(Details.USERNAME, link.getSocialUserId() + "@" + link.getSocialProvider()) + .success(); + return account.setSuccess(Messages.SOCIAL_PROVIDER_REMOVED).createResponse(AccountPages.SOCIAL); } else { return account.setError(Messages.SOCIAL_LINK_NOT_ACTIVE).createResponse(AccountPages.SOCIAL); diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 96d2c3f0e2b..46015599223 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -1,6 +1,7 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; +import org.keycloak.audit.Audit; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; @@ -9,6 +10,7 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -38,6 +40,9 @@ public class RealmsResource { @Context protected KeycloakSession session; + @Context + protected HttpServletRequest servletRequest; + protected TokenManager tokenManager; protected SocialRequestManager socialRequestManager; @@ -54,7 +59,8 @@ public class RealmsResource { public TokenService getTokenService(final @PathParam("realm") String name) { RealmManager realmManager = new RealmManager(session); RealmModel realm = locateRealm(name, realmManager); - TokenService tokenService = new TokenService(realm, tokenManager); + Audit audit = Audit.create(realm, servletRequest.getRemoteAddr()); + TokenService tokenService = new TokenService(realm, tokenManager, audit); resourceContext.initResource(tokenService); return tokenService; } @@ -78,7 +84,9 @@ public class RealmsResource { throw new NotFoundException(); } - AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager); + Audit audit = Audit.create(realm, servletRequest.getRemoteAddr()); + + AccountService accountService = new AccountService(realm, application, tokenManager, socialRequestManager, audit); resourceContext.initResource(accountService); return accountService; } @@ -92,5 +100,4 @@ public class RealmsResource { return realmResource; } - } diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index c0707236432..d77ba7e6eec 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -24,6 +24,10 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; +import org.keycloak.audit.Errors; +import org.keycloak.audit.Events; import org.keycloak.login.LoginForms; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; @@ -84,9 +88,12 @@ public class RequiredActionsService { private TokenManager tokenManager; - public RequiredActionsService(RealmModel realm, TokenManager tokenManager) { + private Audit audit; + + public RequiredActionsService(RealmModel realm, TokenManager tokenManager, Audit audit) { this.realm = realm; this.tokenManager = tokenManager; + this.audit = audit; } @Path("profile") @@ -100,6 +107,8 @@ public class RequiredActionsService { UserModel user = getUser(accessCode); + initAudit(accessCode); + String error = Validation.validateUpdateProfileForm(formData); if (error != null) { return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); @@ -107,11 +116,22 @@ public class RequiredActionsService { user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); - user.setEmail(formData.getFirst("email")); + + String email = formData.getFirst("email"); + String oldEmail = user.getEmail(); + boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; + + user.setEmail(email); user.removeRequiredAction(RequiredAction.UPDATE_PROFILE); accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PROFILE); + audit.clone().event(Events.UPDATE_PROFILE).success(); + if (emailChanged) { + user.setEmailVerified(false); + audit.clone().event(Events.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); + } + return redirectOauth(user, accessCode); } @@ -126,6 +146,8 @@ public class RequiredActionsService { UserModel user = getUser(accessCode); + initAudit(accessCode); + String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); @@ -146,6 +168,8 @@ public class RequiredActionsService { user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP); accessCode.getRequiredActions().remove(RequiredAction.CONFIGURE_TOTP); + audit.clone().event(Events.UPDATE_TOTP).success(); + return redirectOauth(user, accessCode); } @@ -163,6 +187,8 @@ public class RequiredActionsService { UserModel user = getUser(accessCode); + initAudit(accessCode); + String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); @@ -174,7 +200,10 @@ public class RequiredActionsService { } try { - AuthenticationProviderManager.getManager(realm).updatePassword(user.getLoginName(), passwordNew); + boolean updateSuccessful = AuthenticationProviderManager.getManager(realm).updatePassword(user, passwordNew); + if (!updateSuccessful) { + return loginForms.setError("Password update failed").createResponse(RequiredAction.UPDATE_PASSWORD); + } } catch (AuthenticationProviderException ape) { return loginForms.setError(ape.getMessage()).createResponse(RequiredAction.UPDATE_PASSWORD); } @@ -186,6 +215,8 @@ public class RequiredActionsService { accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD); } + audit.clone().event(Events.UPDATE_PASSWORD).success(); + return redirectOauth(user, accessCode); } @@ -201,11 +232,16 @@ public class RequiredActionsService { } UserModel user = getUser(accessCode); + + initAudit(accessCode); + user.setEmailVerified(true); user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL); + audit.clone().event(Events.VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); + return redirectOauth(user, accessCode); } else { AccessCodeEntry accessCode = getAccessCodeEntry(RequiredAction.VERIFY_EMAIL); @@ -213,6 +249,9 @@ public class RequiredActionsService { return unauthorized(); } + initAudit(accessCode); + //audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); } @@ -223,10 +262,12 @@ public class RequiredActionsService { public Response passwordReset() { if (uriInfo.getQueryParameters().containsKey("key")) { AccessCodeEntry accessCode = tokenManager.getAccessCode(uriInfo.getQueryParameters().getFirst("key")); + accessCode.setAuthMethod("form"); if (accessCode == null || accessCode.isExpired() || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) { return unauthorized(); } + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); } else { return Flows.forms(realm, request, uriInfo).createPasswordReset(); @@ -254,6 +295,12 @@ public class RequiredActionsService { "Login requester not enabled."); } + audit.event(Events.SEND_RESET_PASSWORD).client(clientId) + .detail(Details.REDIRECT_URI, redirect) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, "form") + .detail(Details.USERNAME, username); + UserModel user = realm.getUser(username); if (user == null && username.contains("@")) { user = realm.getUserByEmail(username); @@ -261,6 +308,7 @@ public class RequiredActionsService { if (user == null) { logger.warn("Failed to send password reset email: user not found"); + audit.error(Errors.USER_NOT_FOUND); } else { Set requiredActions = new HashSet(user.getRequiredActions()); requiredActions.add(RequiredAction.UPDATE_PASSWORD); @@ -268,9 +316,12 @@ public class RequiredActionsService { AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); accessCode.setRequiredActions(requiredActions); accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); + accessCode.setAuthMethod("form"); + accessCode.setUsername(username); try { new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); + audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getId()).success(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage(); @@ -339,11 +390,27 @@ public class RequiredActionsService { } else { logger.debug("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan()); + + audit.success(); return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, accessCode.getState(), accessCode.getRedirectUri()); } } + private void initAudit(AccessCodeEntry accessCode) { + audit.event(Events.LOGIN).client(accessCode.getClient()) + .user(accessCode.getUser()) + .detail(Details.CODE_ID, accessCode.getId()) + .detail(Details.REDIRECT_URI, accessCode.getRedirectUri()) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, accessCode.getAuthMethod()) + .detail(Details.USERNAME, accessCode.getUsername()); + + if (accessCode.isRememberMe()) { + audit.detail(Details.REMEMBER_ME, "true"); + } + } + private Response unauthorized() { return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").createErrorPage(); } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 45f9a0e4078..3d5bfa52c19 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -24,6 +24,10 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; +import org.keycloak.audit.Errors; +import org.keycloak.audit.Events; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -48,6 +52,7 @@ import org.keycloak.social.SocialProviderConfig; import org.keycloak.social.SocialProviderException; import org.keycloak.social.SocialUser; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -89,6 +94,8 @@ public class SocialResource { @Context protected KeycloakSession session; + @Context + protected HttpServletRequest servletRequest; private SocialRequestManager socialRequestManager; @@ -114,19 +121,33 @@ public class SocialResource { RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); + Audit audit = Audit.create(realm, servletRequest.getRemoteAddr()) + .event(Events.LOGIN) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, "social"); + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!realm.isEnabled()) { + audit.error(Errors.REALM_DISABLED); return oauth.forwardToSecurityFailure("Realm not enabled."); } String clientId = requestData.getClientAttributes().get("clientId"); + String redirectUri = requestData.getClientAttribute("redirectUri"); + String scope = requestData.getClientAttributes().get(OAuth2Constants.SCOPE); + String state = requestData.getClientAttributes().get(OAuth2Constants.STATE); + String responseType = requestData.getClientAttribute("responseType"); + + audit.client(clientId).detail(Details.REDIRECT_URI, redirectUri); ClientModel client = realm.findClient(clientId); if (client == null) { + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Unknown login requester."); } if (!client.isEnabled()) { + audit.error(Errors.CLIENT_DISABLED); return oauth.forwardToSecurityFailure("Login requester not enabled."); } @@ -142,17 +163,21 @@ public class SocialResource { socialUser = provider.processCallback(config, callback); } catch (SocialAccessDeniedException e) { MultivaluedHashMap queryParms = new MultivaluedHashMap(); - queryParms.putSingle(OAuth2Constants.CLIENT_ID, requestData.getClientAttribute("clientId")); - queryParms.putSingle(OAuth2Constants.STATE, requestData.getClientAttribute(OAuth2Constants.STATE)); - queryParms.putSingle(OAuth2Constants.SCOPE, requestData.getClientAttribute(OAuth2Constants.SCOPE)); - queryParms.putSingle(OAuth2Constants.REDIRECT_URI, requestData.getClientAttribute("redirectUri")); - queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, requestData.getClientAttribute("responseType")); + queryParms.putSingle(OAuth2Constants.CLIENT_ID, clientId); + queryParms.putSingle(OAuth2Constants.STATE, state); + queryParms.putSingle(OAuth2Constants.SCOPE, scope); + queryParms.putSingle(OAuth2Constants.REDIRECT_URI, redirectUri); + queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType); + + audit.error(Errors.REJECTED_BY_USER); return Flows.forms(realm, request, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); } catch (SocialProviderException e) { - logger.warn("Failed to process social callback", e); + logger.error("Failed to process social callback", e); return oauth.forwardToSecurityFailure("Failed to process social callback"); } + audit.detail(Details.USERNAME, socialUser.getId() + "@" + provider.getId()); + SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getId(), socialUser.getUsername()); UserModel user = realm.getUserBySocialLink(socialLink); @@ -161,30 +186,39 @@ public class SocialResource { if (userId != null) { UserModel authenticatedUser = realm.getUserById(userId); + audit.event(Events.SOCIAL_LINK).user(userId); + if (user != null) { + audit.error(Errors.SOCIAL_ID_IN_USE); return oauth.forwardToSecurityFailure("This social account is already linked to other user"); } if (!authenticatedUser.isEnabled()) { + audit.error(Errors.USER_DISABLED); return oauth.forwardToSecurityFailure("User is disabled"); } + if (!realm.hasRole(authenticatedUser, realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP).getRole(AccountRoles.MANAGE_ACCOUNT))) { + audit.error(Errors.NOT_ALLOWED); return oauth.forwardToSecurityFailure("Insufficient permissions to link social account"); } + if (redirectUri == null) { + audit.error(Errors.INVALID_REDIRECT_URI); + return oauth.forwardToSecurityFailure("Unknown redirectUri"); + } + realm.addSocialLink(authenticatedUser, socialLink); logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getLoginName()); - String redirectUri = requestData.getClientAttributes().get("redirectUri"); - if (redirectUri == null) { - return oauth.forwardToSecurityFailure("Unknown redirectUri"); - } - + audit.success(); return Response.status(Status.FOUND).location(UriBuilder.fromUri(redirectUri).build()).build(); } if (user == null) { + if (!realm.isRegistrationAllowed()) { + audit.error(Errors.REGISTRATION_DISABLED); return oauth.forwardToSecurityFailure("Registration not allowed"); } @@ -199,17 +233,22 @@ public class SocialResource { } realm.addSocialLink(user, socialLink); + + audit.clone().user(user).event(Events.REGISTER) + .detail(Details.REGISTER_METHOD, "social") + .detail(Details.EMAIL, socialUser.getEmail()) + .removeDetail("auth_method") + .success(); } + audit.user(user); + if (!user.isEnabled()) { + audit.error(Errors.USER_DISABLED); return oauth.forwardToSecurityFailure("Your account is not enabled."); } - String scope = requestData.getClientAttributes().get(OAuth2Constants.SCOPE); - String state = requestData.getClientAttributes().get(OAuth2Constants.STATE); - String redirectUri = requestData.getClientAttributes().get("redirectUri"); - - return oauth.processAccessCode(scope, state, redirectUri, client, user); + return oauth.processAccessCode(scope, state, redirectUri, client, user, socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider(), false, "social", audit); } @GET @@ -221,23 +260,33 @@ public class SocialResource { RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); + Audit audit = Audit.create(realm, servletRequest.getRemoteAddr()) + .event(Events.LOGIN).client(clientId) + .detail(Details.REDIRECT_URI, redirectUri) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, "social"); + SocialProvider provider = SocialLoader.load(providerId); if (provider == null) { + audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND); return Flows.forms(realm, request, uriInfo).setError("Social provider not found").createErrorPage(); } ClientModel client = realm.findClient(clientId); if (client == null) { + audit.error(Errors.CLIENT_NOT_FOUND); logger.warn("Unknown login requester: " + clientId); return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").createErrorPage(); } if (!client.isEnabled()) { + audit.error(Errors.CLIENT_DISABLED); logger.warn("Login requester not enabled."); return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").createErrorPage(); } redirectUri = TokenService.verifyRedirectUri(redirectUri, client); if (redirectUri == null) { + audit.error(Errors.INVALID_REDIRECT_URI); return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); } @@ -248,6 +297,7 @@ public class SocialResource { .putClientAttribute(OAuth2Constants.STATE, state).putClientAttribute("redirectUri", redirectUri) .putClientAttribute("responseType", responseType).redirectToSocialProvider(); } catch (Throwable t) { + logger.error("Failed to redirect to social auth", t); return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index 63f8d981f31..b326216889a 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -6,6 +6,10 @@ import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; +import org.keycloak.audit.Errors; +import org.keycloak.audit.Events; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ClientModel; @@ -74,6 +78,7 @@ public class TokenService { protected RealmModel realm; protected TokenManager tokenManager; + private Audit audit; protected AuthenticationManager authManager = new AuthenticationManager(); @Context @@ -100,9 +105,10 @@ public class TokenService { private ResourceAdminManager resourceAdminManager = new ResourceAdminManager(); - public TokenService(RealmModel realm, TokenManager tokenManager) { + public TokenService(RealmModel realm, TokenManager tokenManager, Audit audit) { this.realm = realm; this.tokenManager = tokenManager; + this.audit = audit; } public static UriBuilder tokenServiceBaseUrl(UriInfo uriInfo) { @@ -146,31 +152,42 @@ public class TokenService { throw new NotAcceptableException("HTTPS required"); } - ClientModel client = authorizeClient(authorizationHeader, form); + audit.event(Events.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); + + ClientModel client = authorizeClient(authorizationHeader, form, audit); if (client.isPublicClient()) { // we don't allow public clients to invoke grants/access to prevent phishing attacks + audit.error(Errors.NOT_ALLOWED); throw new ForbiddenException("Public clients are not allowed to invoke grants/access"); } - - if (form.getFirst(AuthenticationManager.FORM_USERNAME) == null) { + String username = form.getFirst(AuthenticationManager.FORM_USERNAME); + if (username == null) { + audit.error(Errors.USERNAME_MISSING); throw new NotAuthorizedException("No username"); } + audit.detail(Details.USERNAME, username); if (!realm.isEnabled()) { + audit.error(Errors.REALM_DISABLED); throw new NotAuthorizedException("Disabled realm"); } if (authManager.authenticateForm(clientConnection, realm, form) != AuthenticationStatus.SUCCESS) { + audit.error(Errors.INVALID_USER_CREDENTIALS); throw new NotAuthorizedException("Auth failed"); } UserModel user = realm.getUser(form.getFirst(AuthenticationManager.FORM_USERNAME)); String scope = form.getFirst(OAuth2Constants.SCOPE); - AccessTokenResponse res = tokenManager.responseBuilder(realm, client) + + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) .generateAccessToken(scope, client, user) .generateIDToken() .build(); + + audit.success(); + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); } @@ -185,22 +202,28 @@ public class TokenService { throw new NotAcceptableException("HTTPS required"); } - ClientModel client = authorizeClient(authorizationHeader, form); + audit.event(Events.REFRESH_TOKEN); + + ClientModel client = authorizeClient(authorizationHeader, form, audit); String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); AccessToken accessToken = null; try { - accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken); + accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken, audit); } catch (OAuthErrorException e) { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, e.getError()); if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); + audit.error(Errors.INVALID_TOKEN); throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(), e); } - AccessTokenResponse res = tokenManager.responseBuilder(realm, client) + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) .accessToken(accessToken) .generateIDToken() .generateRefreshToken().build(); + + audit.success(); + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); } @@ -211,6 +234,23 @@ public class TokenService { @QueryParam("state") final String state, @QueryParam("redirect_uri") String redirect, final MultivaluedMap formData) { logger.debug("TokenService.processLogin"); + + String username = formData.getFirst(AuthenticationManager.FORM_USERNAME); + + String rememberMe = formData.getFirst("rememberMe"); + boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); + logger.debug("*** Remember me: " + remember); + + audit.event(Events.LOGIN).client(clientId) + .detail(Details.REDIRECT_URI, redirect) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, "form") + .detail(Details.USERNAME, username); + + if (remember) { + audit.detail(Details.REMEMBER_ME, "true"); + } + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { @@ -218,30 +258,32 @@ public class TokenService { } if (!realm.isEnabled()) { + audit.error(Errors.REALM_DISABLED); return oauth.forwardToSecurityFailure("Realm not enabled."); } ClientModel client = realm.findClient(clientId); if (client == null) { + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Unknown login requester."); } if (!client.isEnabled()) { + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Login requester not enabled."); } redirect = verifyRedirectUri(redirect, client); if (redirect == null) { + audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); } if (formData.containsKey("cancel")) { + audit.error(Errors.REJECTED_BY_USER); return oauth.redirectError(client, "access_denied", state, redirect); } AuthenticationStatus status = authManager.authenticateForm(clientConnection, realm, formData); - String rememberMe = formData.getFirst("rememberMe"); - boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); - logger.debug("*** Remember me: " + remember); if (remember) { NewCookie cookie = authManager.createRememberMeCookie(realm, uriInfo); response.addNewCookie(cookie); @@ -252,20 +294,26 @@ public class TokenService { switch (status) { case SUCCESS: case ACTIONS_REQUIRED: - UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, formData.getFirst(AuthenticationManager.FORM_USERNAME)); - return oauth.processAccessCode(scopeParam, state, redirect, client, user, remember); + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); + audit.user(user); + return oauth.processAccessCode(scopeParam, state, redirect, client, user, username, remember, "form", audit); case ACCOUNT_DISABLED: + audit.error(Errors.USER_DISABLED); return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case MISSING_TOTP: return Flows.forms(realm, request, uriInfo).setFormData(formData).createLoginTotp(); + case INVALID_USER: + audit.error(Errors.USER_NOT_FOUND); + return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); default: + audit.error(Errors.INVALID_USER_CREDENTIALS); return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } } @Path("auth/request/login-actions") public RequiredActionsService getRequiredActionsService() { - RequiredActionsService service = new RequiredActionsService(realm, tokenManager); + RequiredActionsService service = new RequiredActionsService(realm, tokenManager, audit); resourceContext.initResource(service); return service; } @@ -276,30 +324,46 @@ public class TokenService { public Response processRegister(@QueryParam("client_id") final String clientId, @QueryParam("scope") final String scopeParam, @QueryParam("state") final String state, @QueryParam("redirect_uri") String redirect, final MultivaluedMap formData) { + + String username = formData.getFirst("username"); + String email = formData.getFirst("email"); + + audit.event(Events.REGISTER).client(clientId) + .detail(Details.REDIRECT_URI, redirect) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.USERNAME, username) + .detail(Details.EMAIL, email) + .detail(Details.REGISTER_METHOD, "form"); + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!realm.isEnabled()) { logger.warn("Realm not enabled"); + audit.error(Errors.REALM_DISABLED); return oauth.forwardToSecurityFailure("Realm not enabled"); } ClientModel client = realm.findClient(clientId); if (client == null) { logger.warn("Unknown login requester."); + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Unknown login requester."); } if (!client.isEnabled()) { logger.warn("Login requester not enabled."); + audit.error(Errors.CLIENT_DISABLED); return oauth.forwardToSecurityFailure("Login requester not enabled."); } redirect = verifyRedirectUri(redirect, client); if (redirect == null) { + audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); } if (!realm.isRegistrationAllowed()) { logger.warn("Registration not allowed"); + audit.error(Errors.REGISTRATION_DISABLED); return oauth.forwardToSecurityFailure("Registration not allowed"); } @@ -315,13 +379,13 @@ public class TokenService { } if (error != null) { + audit.error(Errors.INVALID_REGISTRATION); return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration(); } - String username = formData.getFirst("username"); - UserModel user = realm.getUser(username); if (user != null) { + audit.error(Errors.USERNAME_IN_USE); return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); } @@ -330,21 +394,33 @@ public class TokenService { user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); - user.setEmail(formData.getFirst("email")); + user.setEmail(email); if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(formData.getFirst("password")); + + boolean passwordUpdateSuccessful; + String passwordUpdateError = null; try { - AuthenticationProviderManager.getManager(realm).updatePassword(username, formData.getFirst("password")); + passwordUpdateSuccessful = AuthenticationProviderManager.getManager(realm).updatePassword(user, formData.getFirst("password")); + passwordUpdateError = "Password update failed"; } catch (AuthenticationProviderException ape) { - // User already registered, but force him to update password + passwordUpdateSuccessful = false; + passwordUpdateError = ape.getMessage(); + } + + // User already registered, but force him to update password + if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(realm, request, uriInfo).setError(ape.getMessage()).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + return Flows.forms(realm, request, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } + audit.user(user).success(); + audit.reset(); + return processLogin(clientId, scopeParam, state, redirect, formData); } @@ -365,19 +441,20 @@ public class TokenService { throw new NotAcceptableException("HTTPS required"); } + audit.event(Events.CODE_TO_TOKEN); + if (!realm.isEnabled()) { + audit.error(Errors.REALM_DISABLED); throw new NotAuthorizedException("Realm not enabled"); } - ClientModel client = authorizeClient(authorizationHeader, formData); - String code = formData.getFirst(OAuth2Constants.CODE); if (code == null) { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, "invalid_request"); error.put(OAuth2Constants.ERROR_DESCRIPTION, "code not specified"); + audit.error(Errors.INVALID_CODE); throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } JWSInput input = new JWSInput(code); @@ -391,22 +468,33 @@ public class TokenService { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Unable to verify code signature"); + audit.error(Errors.INVALID_CODE); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } String key = input.readContentAsString(); + + audit.detail(Details.CODE_ID, key); + AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); if (accessCode == null) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code not found"); + audit.error(Errors.INVALID_CODE); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } + + audit.user(accessCode.getUser()); + + ClientModel client = authorizeClient(authorizationHeader, formData, audit); + if (accessCode.isExpired()) { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code is expired"); + audit.error(Errors.INVALID_CODE); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } @@ -414,6 +502,7 @@ public class TokenService { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Token expired"); + audit.error(Errors.INVALID_CODE); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } @@ -421,19 +510,24 @@ public class TokenService { Map res = new HashMap(); res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error"); + audit.error(Errors.INVALID_CODE); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) .build(); } + logger.debug("accessRequest SUCCESS"); - AccessTokenResponse res = tokenManager.responseBuilder(realm, client) + + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) .accessToken(accessCode.getToken()) .generateIDToken() .generateRefreshToken().build(); + audit.success(); + return Cors.add(request, Response.ok(res)).auth().allowedOrigins(client).allowedMethods("POST").build(); } - protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData) { + protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, Audit audit) { String client_id = null; String clientSecret = null; if (authorizationHeader != null) { @@ -456,11 +550,14 @@ public class TokenService { throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } + audit.client(client_id); + ClientModel client = realm.findClient(client_id); if (client == null) { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, "invalid_client"); error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); + audit.error(Errors.CLIENT_NOT_FOUND); throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } @@ -468,6 +565,7 @@ public class TokenService { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, "invalid_client"); error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled"); + audit.error(Errors.CLIENT_DISABLED); throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } @@ -475,6 +573,7 @@ public class TokenService { if (!client.validateSecret(clientSecret)) { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, "unauthorized_client"); + audit.error(Errors.INVALID_CLIENT_CREDENTIALS); throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); } } @@ -487,6 +586,9 @@ public class TokenService { @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state, final @QueryParam("prompt") String prompt) { logger.info("TokenService.loginPage"); + + audit.event(Events.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { @@ -495,20 +597,24 @@ public class TokenService { if (!realm.isEnabled()) { logger.warn("Realm not enabled"); + audit.error(Errors.REALM_DISABLED); return oauth.forwardToSecurityFailure("Realm not enabled"); } ClientModel client = realm.findClient(clientId); if (client == null) { logger.warn("Unknown login requester: " + clientId); + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Unknown login requester."); } if (!client.isEnabled()) { logger.warn("Login requester not enabled."); + audit.error(Errors.CLIENT_DISABLED); return oauth.forwardToSecurityFailure("Login requester not enabled."); } redirect = verifyRedirectUri(redirect, client); if (redirect == null) { + audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); } @@ -516,7 +622,8 @@ public class TokenService { UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers); if (user != null) { logger.debug(user.getLoginName() + " already logged in."); - return oauth.processAccessCode(scopeParam, state, redirect, client, user); + audit.user(user).detail(Details.AUTH_METHOD, "sso"); + return oauth.processAccessCode(scopeParam, state, redirect, client, user, null, false, "sso", audit); } if (prompt != null && prompt.equals("none")) { @@ -532,6 +639,9 @@ public class TokenService { @QueryParam("redirect_uri") String redirect, final @QueryParam("client_id") String clientId, final @QueryParam("scope") String scopeParam, final @QueryParam("state") String state) { logger.info("**********registerPage()"); + + audit.event(Events.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { @@ -540,26 +650,31 @@ public class TokenService { if (!realm.isEnabled()) { logger.warn("Realm not enabled"); + audit.error(Errors.REALM_DISABLED); return oauth.forwardToSecurityFailure("Realm not enabled"); } ClientModel client = realm.findClient(clientId); if (client == null) { logger.warn("Unknown login requester."); + audit.error(Errors.CLIENT_NOT_FOUND); return oauth.forwardToSecurityFailure("Unknown login requester."); } if (!client.isEnabled()) { logger.warn("Login requester not enabled."); + audit.error(Errors.CLIENT_DISABLED); return oauth.forwardToSecurityFailure("Login requester not enabled."); } redirect = verifyRedirectUri(redirect, client); if (redirect == null) { + audit.error(Errors.INVALID_REDIRECT_URI); return oauth.forwardToSecurityFailure("Invalid redirect_uri."); } if (!realm.isRegistrationAllowed()) { logger.warn("Registration not allowed"); + audit.error(Errors.REGISTRATION_DISABLED); return oauth.forwardToSecurityFailure("Registration not allowed"); } @@ -574,6 +689,8 @@ public class TokenService { public Response logout(final @QueryParam("redirect_uri") String redirectUri) { // todo do we care if anybody can trigger this? + audit.event(Events.LOGOUT).detail(Details.REDIRECT_URI, redirectUri); + // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways. UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers, false); if (user != null) { @@ -581,6 +698,8 @@ public class TokenService { authManager.expireIdentityCookie(realm, uriInfo); authManager.expireRememberMeCookie(realm, uriInfo); resourceAdminManager.logoutUser(realm, user); + + audit.user(user).success(); } else { logger.info("No user logged in for logout"); } @@ -592,6 +711,8 @@ public class TokenService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processOAuth(final MultivaluedMap formData) { + audit.event(Events.LOGIN).detail(Details.RESPONSE_TYPE, "code"); + OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { @@ -607,21 +728,39 @@ public class TokenService { logger.debug("Failed to verify signature", ignored); } if (!verifiedCode) { + audit.error(Errors.INVALID_CODE); return oauth.forwardToSecurityFailure("Illegal access code."); } String key = input.readContentAsString(); + audit.detail(Details.CODE_ID, key); + AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key); if (accessCodeEntry == null) { + audit.error(Errors.INVALID_CODE); return oauth.forwardToSecurityFailure("Unknown access code."); } String redirect = accessCodeEntry.getRedirectUri(); String state = accessCodeEntry.getState(); + audit.client(accessCodeEntry.getClient()) + .user(accessCodeEntry.getUser()) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, accessCodeEntry.getAuthMethod()) + .detail(Details.REDIRECT_URI, redirect) + .detail(Details.USERNAME, accessCodeEntry.getUsername()); + + if (accessCodeEntry.isRememberMe()) { + audit.detail(Details.REMEMBER_ME, "true"); + } + if (formData.containsKey("cancel")) { + audit.error(Errors.REJECTED_BY_USER); return redirectAccessDenied(redirect, state); } + audit.success(); + accessCodeEntry.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan()); return oauth.redirectAccessCode(accessCodeEntry, state, redirect); } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 26e4888c808..49ff5d707c6 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -24,23 +24,22 @@ package org.keycloak.services.resources.flows; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Audit; +import org.keycloak.audit.Details; +import org.keycloak.audit.Events; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; -import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; -import org.keycloak.services.resources.TokenService; import org.keycloak.util.Time; -import javax.ws.rs.Path; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -56,15 +55,15 @@ public class OAuthFlows { private static final Logger log = Logger.getLogger(OAuthFlows.class); - private RealmModel realm; + private final RealmModel realm; - private HttpRequest request; + private final HttpRequest request; - private UriInfo uriInfo; + private final UriInfo uriInfo; - private AuthenticationManager authManager; + private final AuthenticationManager authManager; - private TokenManager tokenManager; + private final TokenManager tokenManager; OAuthFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, TokenManager tokenManager) { @@ -110,28 +109,40 @@ public class OAuthFlows { } } - public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user) { - return processAccessCode(scopeParam, state, redirect, client, user, false); + public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, Audit audit) { + return processAccessCode(scopeParam, state, redirect, client, user, null, false, "form", audit); } - public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, boolean rememberMe) { + public Response processAccessCode(String scopeParam, String state, String redirect, ClientModel client, UserModel user, String username, boolean rememberMe, String authMethod, Audit audit) { isTotpConfigurationRequired(user); isEmailVerificationRequired(user); boolean isResource = client instanceof ApplicationModel; AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); + accessCode.setUsername(username); + accessCode.setRememberMe(rememberMe); + accessCode.setAuthMethod(authMethod); + log.debug("processAccessCode: isResource: {0}", isResource); log.debug("processAccessCode: go to oauth page?: {0}", (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested() .size() > 0))); + audit.detail(Details.CODE_ID, accessCode.getId()); + Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { accessCode.setRequiredActions(new HashSet(requiredActions)); accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); + + RequiredAction action = user.getRequiredActions().iterator().next(); + if (action.equals(RequiredAction.VERIFY_EMAIL)) { + audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); + } + return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) - .createResponse(user.getRequiredActions().iterator().next()); + .createResponse(action); } if (!isResource @@ -143,6 +154,7 @@ public class OAuthFlows { } if (redirect != null) { + audit.success(); return redirectAccessCode(accessCode, state, redirect, rememberMe); } else { return null; diff --git a/social/core/pom.xml b/social/core/pom.xml index e201e0c1bc8..09a459263ea 100755 --- a/social/core/pom.xml +++ b/social/core/pom.xml @@ -1,5 +1,6 @@ - + keycloak-social-parent org.keycloak diff --git a/social/google/pom.xml b/social/google/pom.xml index 273f457961a..925965b5f77 100755 --- a/social/google/pom.xml +++ b/social/google/pom.xml @@ -1,5 +1,6 @@ - + keycloak-social-parent org.keycloak diff --git a/social/pom.xml b/social/pom.xml index 05b3fc12c20..9e046459738 100755 --- a/social/pom.xml +++ b/social/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak diff --git a/social/twitter/pom.xml b/social/twitter/pom.xml index 985994a6bd7..eb907c6b87a 100755 --- a/social/twitter/pom.xml +++ b/social/twitter/pom.xml @@ -1,5 +1,6 @@ - + keycloak-social-parent org.keycloak diff --git a/spi/authentication-model/pom.xml b/spi/authentication-model/pom.xml index 9a30d3a109f..fa521a4f74f 100644 --- a/spi/authentication-model/pom.xml +++ b/spi/authentication-model/pom.xml @@ -64,8 +64,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java index 58bf42390f0..66166ab0e04 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/AbstractModelAuthenticationProvider.java @@ -9,8 +9,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProvider; import org.keycloak.spi.authentication.AuthenticationProviderException; @@ -24,22 +23,19 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat private static final Logger logger = Logger.getLogger(AbstractModelAuthenticationProvider.class); @Override - public AuthResult validatePassword(RealmModel currentRealm, Map config, String username, String password) throws AuthenticationProviderException { + public AuthUser getUser(RealmModel currentRealm, Map config, String username) throws AuthenticationProviderException { RealmModel realm = getRealm(currentRealm, config); + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); + return user == null ? null : createAuthenticatedUserInstance(user); + } + @Override + public AuthProviderStatus validatePassword(RealmModel currentRealm, Map config, String username, String password) throws AuthenticationProviderException { + RealmModel realm = getRealm(currentRealm, config); UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username); - if (user == null) { - return new AuthResult(AuthProviderStatus.USER_NOT_FOUND); - } - boolean result = realm.validatePassword(user, password); - if (!result) { - return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS); - } - - AuthenticatedUser authUser = createAuthenticatedUserInstance(user); - return new AuthResult(AuthProviderStatus.SUCCESS).setProviderName(getName()).setUser(authUser); + return result ? AuthProviderStatus.SUCCESS : AuthProviderStatus.INVALID_CREDENTIALS; } @Override @@ -54,7 +50,7 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat UserModel user = realm.getUser(username); if (user == null) { - logger.debugf("User '%s' doesn't exists. Skip password update", username); + logger.warnf("User '%s' doesn't exists. Skip password update", username); return false; } @@ -68,5 +64,9 @@ public abstract class AbstractModelAuthenticationProvider implements Authenticat protected abstract RealmModel getRealm(RealmModel currentRealm, Map config) throws AuthenticationProviderException; - protected abstract AuthenticatedUser createAuthenticatedUserInstance(UserModel user); + protected AuthUser createAuthenticatedUserInstance(UserModel user) { + return new AuthUser(user.getId(), user.getLoginName(), getName()) + .setName(user.getFirstName(), user.getLastName()) + .setEmail(user.getEmail()); + } } diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java index a24a4952d79..a4d129bdc31 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java @@ -7,7 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; import org.keycloak.spi.authentication.AuthenticationProviderException; /** @@ -40,11 +40,4 @@ public class ExternalModelAuthenticationProvider extends AbstractModelAuthentica } return realm; } - - @Override - protected AuthenticatedUser createAuthenticatedUserInstance(UserModel user) { - return new AuthenticatedUser(user.getId(), user.getLoginName()) - .setName(user.getFirstName(), user.getLastName()) - .setEmail(user.getEmail()); - } } diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java index 516948920a2..29ab43aafe4 100644 --- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java +++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java @@ -5,7 +5,7 @@ import java.util.Map; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; +import org.keycloak.spi.authentication.AuthUser; /** * AbstractModelAuthenticationProvider, which uses current realm to call operations on @@ -23,10 +23,4 @@ public class ModelAuthenticationProvider extends AbstractModelAuthenticationProv protected RealmModel getRealm(RealmModel currentRealm, Map config) { return currentRealm; } - - @Override - protected AuthenticatedUser createAuthenticatedUserInstance(UserModel user) { - // We don't want AuthenticatedUser instance. Auto-registration won't never happen with this provider - return null; - } } diff --git a/spi/authentication-picketlink/pom.xml b/spi/authentication-picketlink/pom.xml index ce78e8d5dc3..d0b5dcf2035 100644 --- a/spi/authentication-picketlink/pom.xml +++ b/spi/authentication-picketlink/pom.xml @@ -85,8 +85,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java index 3a4da3e70ee..ffc1ab6b373 100755 --- a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java +++ b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java @@ -1,101 +1,99 @@ -package org.keycloak.spi.authentication.picketlink; - -import java.util.Map; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.spi.ResteasyProviderFactory; -import org.keycloak.models.RealmModel; -import org.keycloak.spi.authentication.AuthProviderStatus; -import org.keycloak.spi.authentication.AuthResult; -import org.keycloak.spi.authentication.AuthProviderConstants; -import org.keycloak.spi.authentication.AuthenticatedUser; -import org.keycloak.spi.authentication.AuthenticationProvider; -import org.keycloak.spi.authentication.AuthenticationProviderException; -import org.keycloak.spi.picketlink.PartitionManagerProvider; -import org.keycloak.util.ProviderLoader; -import org.picketlink.idm.IdentityManager; -import org.picketlink.idm.PartitionManager; -import org.picketlink.idm.credential.Credentials; -import org.picketlink.idm.credential.Password; -import org.picketlink.idm.credential.UsernamePasswordCredentials; -import org.picketlink.idm.model.basic.BasicModel; -import org.picketlink.idm.model.basic.User; - -/** - * AuthenticationProvider, which delegates authentication to picketlink - * - * @author Marek Posolda - */ -public class PicketlinkAuthenticationProvider implements AuthenticationProvider { - - private static final Logger logger = Logger.getLogger(PicketlinkAuthenticationProvider.class); - - @Override - public String getName() { - return AuthProviderConstants.PROVIDER_NAME_PICKETLINK; - } - - @Override - public AuthResult validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { - IdentityManager identityManager = getIdentityManager(realm); - - User picketlinkUser = BasicModel.getUser(identityManager, username); - if (picketlinkUser == null) { - return new AuthResult(AuthProviderStatus.USER_NOT_FOUND); - } - - UsernamePasswordCredentials credential = new UsernamePasswordCredentials(); - credential.setUsername(username); - credential.setPassword(new Password(password.toCharArray())); - identityManager.validateCredentials(credential); - if (credential.getStatus() == Credentials.Status.VALID) { - AuthResult result = new AuthResult(AuthProviderStatus.SUCCESS); - - AuthenticatedUser authenticatedUser = new AuthenticatedUser(picketlinkUser.getId(), picketlinkUser.getLoginName()) - .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) - .setEmail(picketlinkUser.getEmail()); - result.setUser(authenticatedUser).setProviderName(getName()); - return result; - } else { - return new AuthResult(AuthProviderStatus.INVALID_CREDENTIALS); - } - } - - @Override - public boolean updateCredential(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { - IdentityManager identityManager = getIdentityManager(realm); - - User picketlinkUser = BasicModel.getUser(identityManager, username); - if (picketlinkUser == null) { - logger.debugf("User '%s' doesn't exists. Skip password update", username); - return false; - } - - identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray())); - return true; - } - - public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException { - IdentityManager identityManager = ResteasyProviderFactory.getContextData(IdentityManager.class); - if (identityManager == null) { - Iterable providers = ProviderLoader.load(PartitionManagerProvider.class); - - // TODO: Priority? - PartitionManager partitionManager = null; - for (PartitionManagerProvider provider : providers) { - partitionManager = provider.getPartitionManager(realm); - if (partitionManager != null) { - break; - } - } - - if (partitionManager == null) { - throw new AuthenticationProviderException("Not able to locate PartitionManager with any PartitionManagerProvider"); - } - - identityManager = partitionManager.createIdentityManager(); - ResteasyProviderFactory.pushContext(IdentityManager.class, identityManager); - } - return identityManager; - } -} +package org.keycloak.spi.authentication.picketlink; + +import java.util.Map; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.spi.authentication.AuthProviderStatus; +import org.keycloak.spi.authentication.AuthProviderConstants; +import org.keycloak.spi.authentication.AuthUser; +import org.keycloak.spi.authentication.AuthenticationProvider; +import org.keycloak.spi.authentication.AuthenticationProviderException; +import org.keycloak.spi.picketlink.PartitionManagerProvider; +import org.keycloak.util.ProviderLoader; +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.PartitionManager; +import org.picketlink.idm.credential.Credentials; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.UsernamePasswordCredentials; +import org.picketlink.idm.model.basic.BasicModel; +import org.picketlink.idm.model.basic.User; + +/** + * AuthenticationProvider, which delegates authentication to picketlink + * + * @author Marek Posolda + */ +public class PicketlinkAuthenticationProvider implements AuthenticationProvider { + + private static final Logger logger = Logger.getLogger(PicketlinkAuthenticationProvider.class); + + @Override + public String getName() { + return AuthProviderConstants.PROVIDER_NAME_PICKETLINK; + } + + @Override + public AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException { + IdentityManager identityManager = getIdentityManager(realm); + User picketlinkUser = BasicModel.getUser(identityManager, username); + return picketlinkUser == null ? null : new AuthUser(picketlinkUser.getId(), picketlinkUser.getLoginName(), getName()) + .setName(picketlinkUser.getFirstName(), picketlinkUser.getLastName()) + .setEmail(picketlinkUser.getEmail()) + .setProviderName(getName()); + } + + @Override + public AuthProviderStatus validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { + IdentityManager identityManager = getIdentityManager(realm); + + UsernamePasswordCredentials credential = new UsernamePasswordCredentials(); + credential.setUsername(username); + credential.setPassword(new Password(password.toCharArray())); + identityManager.validateCredentials(credential); + if (credential.getStatus() == Credentials.Status.VALID) { + return AuthProviderStatus.SUCCESS; + } else { + return AuthProviderStatus.INVALID_CREDENTIALS; + } + } + + @Override + public boolean updateCredential(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException { + IdentityManager identityManager = getIdentityManager(realm); + + User picketlinkUser = BasicModel.getUser(identityManager, username); + if (picketlinkUser == null) { + logger.debugf("User '%s' doesn't exists. Skip password update", username); + return false; + } + + identityManager.updateCredential(picketlinkUser, new Password(password.toCharArray())); + return true; + } + + public IdentityManager getIdentityManager(RealmModel realm) throws AuthenticationProviderException { + IdentityManager identityManager = ResteasyProviderFactory.getContextData(IdentityManager.class); + if (identityManager == null) { + Iterable providers = ProviderLoader.load(PartitionManagerProvider.class); + + // TODO: Priority? + PartitionManager partitionManager = null; + for (PartitionManagerProvider provider : providers) { + partitionManager = provider.getPartitionManager(realm); + if (partitionManager != null) { + break; + } + } + + if (partitionManager == null) { + throw new AuthenticationProviderException("Not able to locate PartitionManager with any PartitionManagerProvider"); + } + + identityManager = partitionManager.createIdentityManager(); + ResteasyProviderFactory.pushContext(IdentityManager.class, identityManager); + } + return identityManager; + } +} diff --git a/spi/authentication-spi/pom.xml b/spi/authentication-spi/pom.xml index 1c822e4d0bb..cbaee59e8f8 100644 --- a/spi/authentication-spi/pom.xml +++ b/spi/authentication-spi/pom.xml @@ -38,8 +38,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderConstants.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderConstants.java index cae2cb6c5b1..d736da7a871 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderConstants.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderConstants.java @@ -5,12 +5,11 @@ package org.keycloak.spi.authentication; */ public class AuthProviderConstants { + // Model is default provider. See AuthenticationProviderModel.DEFAULT_PROVIDER public static final String PROVIDER_NAME_MODEL = "model"; public static final String PROVIDER_NAME_EXTERNAL_MODEL = "externalModel"; public static final String PROVIDER_NAME_PICKETLINK = "picketlink"; - public static final String DEFAULT_PROVIDER = PROVIDER_NAME_MODEL; - // Used in external-model provider public static final String EXTERNAL_REALM_ID = "externalRealmId"; } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java index 56d05d05434..c782dfc707f 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthProviderStatus.java @@ -7,6 +7,6 @@ package org.keycloak.spi.authentication; */ public enum AuthProviderStatus { - SUCCESS, INVALID_CREDENTIALS, USER_NOT_FOUND + SUCCESS, INVALID_CREDENTIALS, FAILED } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java deleted file mode 100755 index 297e5fe048a..00000000000 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.keycloak.spi.authentication; - -/** - * @author Marek Posolda - */ -public class AuthResult { - - // Status of authentication - private final AuthProviderStatus authProviderStatus; - - // Provider, which authenticated user - private String providerName; - - // filled usually only in case of successful authentication and just with some Authentication providers - private AuthenticatedUser authenticatedUser; - - public AuthResult(AuthProviderStatus authProviderStatus) { - this.authProviderStatus = authProviderStatus; - } - - public AuthResult setProviderName(String providerName) { - this.providerName = providerName; - return this; - } - - public AuthResult setUser(AuthenticatedUser user) { - this.authenticatedUser = user; - return this; - } - - public AuthProviderStatus getAuthProviderStatus() { - return authProviderStatus; - } - - public String getProviderName() { - return providerName; - } - - public AuthenticatedUser getAuthenticatedUser() { - return authenticatedUser; - } - - -} diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java similarity index 64% rename from spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java rename to spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java index 760f99ce2f2..e0e6f76ca6c 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticatedUser.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthUser.java @@ -3,7 +3,7 @@ package org.keycloak.spi.authentication; /** * @author Marek Posolda */ -public class AuthenticatedUser { +public class AuthUser { private String id; private String username; @@ -11,16 +11,19 @@ public class AuthenticatedUser { private String lastName; private String email; - public AuthenticatedUser(String id, String username) { + private String providerName; + + public AuthUser(String id, String username, String providerName) { this.id = id; this.username = username; + this.providerName = providerName; } public String getId() { return id; } - public AuthenticatedUser setId(String id) { + public AuthUser setId(String id) { this.id = id; return this; } @@ -29,7 +32,7 @@ public class AuthenticatedUser { return username; } - public AuthenticatedUser setUsername(String username) { + public AuthUser setUsername(String username) { this.username = username; return this; } @@ -38,7 +41,7 @@ public class AuthenticatedUser { return firstName; } - public AuthenticatedUser setName(String name) { + public AuthUser setName(String name) { int i = name.lastIndexOf(' '); if (i != -1) { firstName = name.substring(0, i); @@ -50,7 +53,7 @@ public class AuthenticatedUser { return this; } - public AuthenticatedUser setName(String firstName, String lastName) { + public AuthUser setName(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; return this; @@ -64,8 +67,17 @@ public class AuthenticatedUser { return email; } - public AuthenticatedUser setEmail(String email) { + public AuthUser setEmail(String email) { this.email = email; return this; } + + public String getProviderName() { + return providerName; + } + + public AuthUser setProviderName(String providerName) { + this.providerName = providerName; + return this; + } } diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java index 1ab1836ef0a..014d432ba7c 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java @@ -11,6 +11,17 @@ public interface AuthenticationProvider { String getName(); + /** + * Get user by given username or email. Return user instance or null if user doesn't exists in this authentication provider + * + * @param realm + * @param configuration + * @param username or email + * @return found user or null if user with given username doesn't exists + * @throws AuthenticationProviderException + */ + AuthUser getUser(RealmModel realm, Map configuration, String username) throws AuthenticationProviderException; + /** * Standard Authentication flow * @@ -18,7 +29,7 @@ public interface AuthenticationProvider { * @param password * @return result of authentication, which might eventually encapsulate info about authenticated user and provider which successfully authenticated */ - AuthResult validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException; + AuthProviderStatus validatePassword(RealmModel realm, Map configuration, String username, String password) throws AuthenticationProviderException; /** diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java index 669153e4b85..c6a59a1a8f3 100644 --- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java +++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java @@ -1,14 +1,15 @@ package org.keycloak.spi.authentication; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jboss.logging.Logger; +import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.util.ProviderLoader; /** @@ -22,7 +23,6 @@ import org.keycloak.util.ProviderLoader; public class AuthenticationProviderManager { private static final Logger logger = Logger.getLogger(AuthenticationProviderManager.class); - private static final AuthenticationProviderModel DEFAULT_PROVIDER = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_MODEL, true, Collections.EMPTY_MAP); private final RealmModel realm; private final Map delegates; @@ -47,85 +47,138 @@ public class AuthenticationProviderManager { this.delegates = delegates; } - public AuthResult validatePassword(String username, String password) { - List configuredProviders = getConfiguredProviders(realm); - boolean userExists = false; - - for (AuthenticationProviderModel authProviderConfig : configuredProviders) { - String providerName = authProviderConfig.getProviderName(); - - AuthenticationProvider delegate = getDelegate(providerName); + public AuthUser getUser(String username) { + List authProviderModels = getConfiguredProviderModels(realm); + for (AuthenticationProviderModel providerModel : authProviderModels) { + AuthenticationProvider delegate = getProvider(providerModel.getProviderName()); if (delegate == null) { continue; } try { - AuthResult currentResult = delegate.validatePassword(realm, authProviderConfig.getConfig(), username, password); - logger.debugf("Authentication provider '%s' finished with '%s' for authentication of '%s'", delegate.getName(), currentResult.getAuthProviderStatus().toString(), username); - - if (currentResult.getAuthProviderStatus() == AuthProviderStatus.SUCCESS) { - return currentResult; - } else if (currentResult.getAuthProviderStatus() == AuthProviderStatus.INVALID_CREDENTIALS) { - userExists = true; + AuthUser authUser = delegate.getUser(realm, providerModel.getConfig(), username); + if (authUser != null) { + logger.debugf("User '%s' found with provider '%s'", username, providerModel.getProviderName()); + return authUser; } } catch (AuthenticationProviderException ape) { logger.warn(ape.getMessage(), ape); } } - AuthProviderStatus status = userExists ? AuthProviderStatus.INVALID_CREDENTIALS : AuthProviderStatus.USER_NOT_FOUND; - logger.debugf("Not able to authenticate '%s' with any authentication provider. Status: '%s'", username, status.toString()); - - return new AuthResult(status); + logger.debugf("User '%s' not found with any provider", username); + return null; } - public void updatePassword(String username, String password) throws AuthenticationProviderException { - List configuredProviders = getConfiguredProviders(realm); + public AuthProviderStatus validatePassword(UserModel user, String password) { + AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); + if (authLink == null) { + authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); + } - for (AuthenticationProviderModel authProviderConfig : configuredProviders) { + String providerName = authLink.getAuthProvider(); - // Update just those, which support password update - if (authProviderConfig.isPasswordUpdateSupported()) { - String providerName = authProviderConfig.getProviderName(); - AuthenticationProvider delegate = getDelegate(providerName); - if (delegate == null) { - continue; - } + AuthenticationProviderModel providerModel = getConfiguredProviderModel(realm, providerName); + AuthenticationProvider delegate = getProvider(providerName); + if (delegate == null || providerModel == null) { + return AuthProviderStatus.FAILED; + } - try { - if (delegate.updateCredential(realm, authProviderConfig.getConfig(), username, password)) { - logger.debugf("Updated password in authentication provider '%s' for user '%s'", delegate.getName(), username); - } else { - logger.debugf("Password not updated in authentication provider '%s' for user '%s'", delegate.getName(), username); - } - } catch (AuthenticationProviderException ape) { - // Rethrow it to upper layer - logger.warn("Failed to update password: " + ape.getMessage()); - throw ape; - } - } else { - logger.debugf("Skip password update for authentication provider '%s' for user '%s'", authProviderConfig.getProviderName(), username); - } + try { + checkCorrectAuthLink(delegate, providerModel, authLink, user.getLoginName()); + + AuthProviderStatus currentResult = delegate.validatePassword(realm, providerModel.getConfig(), user.getLoginName(), password); + logger.debugf("Authentication provider '%s' finished with '%s' for authentication of '%s'", delegate.getName(), currentResult.toString(), user.getLoginName()); + return currentResult; + } catch (AuthenticationProviderException ape) { + logger.warn(ape.getMessage(), ape); + return AuthProviderStatus.FAILED; } } - private AuthenticationProvider getDelegate(String providerName) { + public boolean updatePassword(UserModel user, String password) throws AuthenticationProviderException { + AuthenticationLinkModel authLink = realm.getAuthenticationLink(user); + if (authLink == null) { + authLink = new AuthenticationLinkModel(AuthenticationProviderModel.DEFAULT_PROVIDER.getProviderName(), user.getId()); + } + + String providerName = authLink.getAuthProvider(); + + AuthenticationProviderModel providerModel = getConfiguredProviderModel(realm, providerName); + if (providerModel == null) { + return false; + } + + String username = user.getLoginName(); + + // Update just those, which support password update + if (providerModel.isPasswordUpdateSupported()) { + try { + AuthenticationProvider delegate = getProvider(providerName); + if (delegate == null) { + return false; + } + + checkCorrectAuthLink(delegate, providerModel, authLink, username); + + if (delegate.updateCredential(realm,providerModel.getConfig(), user.getLoginName(), password)) { + logger.debugf("Updated password in authentication provider '%s' for user '%s'", providerName, username); + return true; + } else { + logger.warnf("Password not updated in authentication provider '%s' for user '%s'", providerName, username); + return false; + } + } catch (AuthenticationProviderException ape) { + // Rethrow it to upper layer + logger.warn("Failed to update password: " + ape.getMessage()); + throw ape; + } + } else { + logger.warnf("Skip password update for authentication provider '%s' for user '%s'", providerName, username); + return false; + } + } + + private AuthenticationProvider getProvider(String providerName) { AuthenticationProvider delegate = delegates.get(providerName); if (delegate == null) { - logger.warnf("Configured provider with name '%s' not found", providerName); + logger.warnf("Provider '%s' not available on classpath", providerName); } return delegate; } - private List getConfiguredProviders(RealmModel realm) { + private List getConfiguredProviderModels(RealmModel realm) { List configuredProviders = realm.getAuthenticationProviders(); // Use model based authentication of current realm by default if (configuredProviders == null || configuredProviders.isEmpty()) { - configuredProviders = new ArrayList(); - configuredProviders.add(DEFAULT_PROVIDER); + configuredProviders = Collections.EMPTY_LIST; + logger.warnf("No authentication providers found"); } return configuredProviders; } + + private AuthenticationProviderModel getConfiguredProviderModel(RealmModel realm, String providerName) { + List providers = getConfiguredProviderModels(realm); + for (AuthenticationProviderModel provider : providers) { + if (providerName.equals(provider.getProviderName())) { + return provider; + } + } + + logger.warnf("Provider '%s' not configured in realm", providerName); + return null; + } + + // Check if ID of linked AuthUser is same as expected ID from authenticationLink . It should catch the case when for example user "john" was deleted in LDAP + // and then user "john" has been created again, but it's actually different user with different ID + private void checkCorrectAuthLink(AuthenticationProvider authProvider, AuthenticationProviderModel providerModel, + AuthenticationLinkModel authLinkModel, String username) throws AuthenticationProviderException { + AuthUser authUser = authProvider.getUser(realm, providerModel.getConfig(), username); + String userExternalId = authUser.getId(); + if (!userExternalId.equals(authLinkModel.getAuthUserId())) { + throw new AuthenticationProviderException("ID did not match! ID from provider: " + userExternalId + ", ID from authentication link: " + authLinkModel.getAuthUserId()); + } + } } diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index e784d043660..115c618b326 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak @@ -36,6 +37,16 @@ keycloak-admin-ui ${project.version} + + org.keycloak + keycloak-audit-api + ${project.version} + + + org.keycloak + keycloak-audit-jboss-logging + ${project.version} + org.keycloak keycloak-admin-ui-styles @@ -321,8 +332,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java index 8d910af0f53..ab6e3b3a0f5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java @@ -21,18 +21,12 @@ */ package org.keycloak.testsuite; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; /** * @author Stian Thorgersen diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java new file mode 100644 index 00000000000..5a582e9f3d6 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -0,0 +1,328 @@ +package org.keycloak.testsuite; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.jboss.logging.Logger; +import org.junit.Assert; +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import org.keycloak.audit.AuditListener; +import org.keycloak.audit.AuditListenerFactory; +import org.keycloak.audit.Details; +import org.keycloak.audit.Event; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.KeycloakRule; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author Stian Thorgersen + */ +public class AssertEvents implements TestRule, AuditListenerFactory { + + private static final Logger log = Logger.getLogger(AssertEvents.class); + + public static String DEFAULT_CLIENT_ID = "test-app"; + public static String DEFAULT_REDIRECT_URI = "http://localhost:8081/app/auth"; + public static String DEFAULT_IP_ADDRESS = "127.0.0.1"; + public static String DEFAULT_REALM = "test"; + public static String DEFAULT_USERNAME = "test-user@localhost"; + + private KeycloakRule keycloak; + + private static BlockingQueue events = new LinkedBlockingQueue(); + + public AssertEvents() { + } + + public AssertEvents(KeycloakRule keycloak) { + this.keycloak = keycloak; + } + + @Override + public String getId() { + return "assert-events"; + } + + @Override + public Statement apply(final Statement base, org.junit.runner.Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + events.clear(); + + keycloak.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + Set listeners = new HashSet(); + listeners.add("jboss-logging"); + listeners.add("assert-events"); + appRealm.setAuditListeners(listeners); + } + }); + + try { + base.evaluate(); + + Event event = events.peek(); + if (event != null) { + Assert.fail("Unexpected event after test: " + event.getEvent()); + } + } finally { + keycloak.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setAuditListeners(null); + } + }); + } + } + }; + } + + public void assertEmpty() { + Assert.assertTrue(events.isEmpty()); + } + + public Event poll() { + try { + return events.poll(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return null; + } + } + + public void clear() { + events.clear(); + } + + public ExpectedEvent expectRequiredAction(String event) { + return expectLogin().event(event); + } + + public ExpectedEvent expectLogin() { + return expect("login") + .detail(Details.CODE_ID, isCodeId()) + .detail(Details.USERNAME, DEFAULT_USERNAME) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.AUTH_METHOD, "form") + .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI); + } + + public ExpectedEvent expectCodeToToken(String codeId) { + return expect("code_to_token") + .detail(Details.CODE_ID, codeId) + .detail(Details.TOKEN_ID, isUUID()) + .detail(Details.REFRESH_TOKEN_ID, isUUID()); + } + + public ExpectedEvent expectRefresh(String refreshTokenId) { + return expect("refresh_token") + .detail(Details.TOKEN_ID, isUUID()) + .detail(Details.REFRESH_TOKEN_ID, refreshTokenId) + .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID()); + } + + public ExpectedEvent expectLogout() { + return expect("logout").client((String) null) + .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI); + } + + public ExpectedEvent expectRegister(String username, String email) { + UserRepresentation user = keycloak.getUser("test", username); + return expect("register") + .user(user != null ? user.getId() : null) + .detail(Details.USERNAME, username) + .detail(Details.EMAIL, email) + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.REGISTER_METHOD, "form") + .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI); + } + + public ExpectedEvent expectAccount(String event) { + return expect(event).client("account"); + } + + public ExpectedEvent expect(String event) { + return new ExpectedEvent().realm(DEFAULT_REALM).client(DEFAULT_CLIENT_ID).user(keycloak.getUser(DEFAULT_REALM, DEFAULT_USERNAME).getId()).ipAddress(DEFAULT_IP_ADDRESS).event(event); + } + + @Override + public AuditListener create() { + return new AuditListener() { + @Override + public void onEvent(Event event) { + events.add(event); + } + + @Override + public void close() { + } + }; + } + + @Override + public void init() { + } + + @Override + public void close() { + } + + public static class ExpectedEvent { + private Event expected = new Event(); + private Matcher userId; + private HashMap> details; + + public ExpectedEvent realm(RealmModel realm) { + expected.setRealmId(realm.getId()); + return this; + } + + public ExpectedEvent realm(String realmId) { + expected.setRealmId(realmId); + return this; + } + + public ExpectedEvent client(ClientModel client) { + expected.setClientId(client.getClientId()); + return this; + } + + public ExpectedEvent client(String clientId) { + expected.setClientId(clientId); + return this; + } + + public ExpectedEvent user(UserModel user) { + return user(CoreMatchers.equalTo(user.getId())); + } + + public ExpectedEvent user(String userId) { + return user(CoreMatchers.equalTo(userId)); + } + + public ExpectedEvent user(Matcher userId) { + this.userId = userId; + return this; + } + + public ExpectedEvent ipAddress(String ipAddress) { + expected.setIpAddress(ipAddress); + return this; + } + + public ExpectedEvent event(String e) { + expected.setEvent(e); + return this; + } + + public ExpectedEvent detail(String key, String value) { + return detail(key, CoreMatchers.equalTo(value)); + } + + public ExpectedEvent detail(String key, Matcher matcher) { + if (details == null) { + details = new HashMap>(); + } + details.put(key, matcher); + return this; + } + + public ExpectedEvent removeDetail(String key) { + if (details != null) { + details.remove(key); + } + return this; + } + + public ExpectedEvent error(String error) { + expected.setError(error); + return this; + } + + public Event assertEvent() { + try { + return assertEvent(events.poll(10, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + throw new AssertionError("No event received within timeout"); + } + } + + public Event assertEvent(Event actual) { + Assert.assertEquals(expected.getEvent(), actual.getEvent()); + Assert.assertEquals(expected.getRealmId(), actual.getRealmId()); + Assert.assertEquals(expected.getClientId(), actual.getClientId()); + Assert.assertEquals(expected.getError(), actual.getError()); + Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress()); + Assert.assertThat(actual.getUserId(), userId); + + if (details == null) { + Assert.assertNull(actual.getDetails()); + } else { + Assert.assertNotNull(actual.getDetails()); + for (Map.Entry> d : details.entrySet()) { + String actualValue = actual.getDetails().get(d.getKey()); + if (!actual.getDetails().containsKey(d.getKey())) { + Assert.fail(d.getKey() + " missing"); + } + + if (!d.getValue().matches(actualValue)) { + Assert.fail(d.getKey() + " doesn't match"); + } + } + + for (String k : actual.getDetails().keySet()) { + if (!details.containsKey(k)) { + Assert.fail(k + " was not expected"); + } + } + } + + return actual; + } + } + + public static Matcher isCodeId() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(String item) { + return (UUID.randomUUID().toString() + System.currentTimeMillis()).length() == item.length(); + } + + @Override + public void describeTo(Description description) { + description.appendText("Not an Code ID"); + } + }; + } + + public static Matcher isUUID() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(String item) { + return KeycloakModelUtils.generateId().length() == item.length(); + } + + @Override + public void describeTo(Description description) { + description.appendText("Not an UUID"); + } + }; + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java index 7ddf7efcb30..fe2745c413a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java @@ -11,7 +11,6 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.net.URI; -import java.nio.charset.Charset; import java.util.List; import java.util.UUID; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java similarity index 77% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 27e013b6462..c2ad75ee085 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -19,24 +19,38 @@ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ -package org.keycloak.testsuite.forms; +package org.keycloak.testsuite.account; -import org.junit.*; -import org.keycloak.models.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.audit.Details; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.*; +import org.keycloak.testsuite.pages.AccountPasswordPage; +import org.keycloak.testsuite.pages.AccountTotpPage; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; -import static org.junit.Assert.assertEquals; - /** * @author Stian Thorgersen */ @@ -62,6 +76,11 @@ public class AccountTest { } }); + public static String ACCOUNT_REDIRECT = "http://localhost:8081/auth/rest/realms/test/account/login-redirect"; + + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -90,6 +109,12 @@ public class AccountTest { protected ErrorPage errorPage; private TimeBasedOTP totp = new TimeBasedOTP(); + private String userId; + + @Before + public void before() { + userId = keycloakRule.getUser("test", "test-user@localhost").getId(); + } @After public void after() { @@ -122,6 +147,8 @@ public class AccountTest { Assert.assertTrue(appPage.isCurrent()); Assert.assertEquals(appPage.baseUrl + "?test", driver.getCurrentUrl()); + + events.clear(); } @Test @@ -129,6 +156,8 @@ public class AccountTest { changePasswordPage.open(); loginPage.login("test-user@localhost", "password"); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); + changePasswordPage.changePassword("", "new-password", "new-password"); Assert.assertEquals("Please specify password.", profilePage.getError()); @@ -141,6 +170,8 @@ public class AccountTest { Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); + events.expectAccount("update_password").assertEvent(); + changePasswordPage.logout(); loginPage.open(); @@ -148,10 +179,14 @@ public class AccountTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); + events.expectLogin().user((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent(); + loginPage.open(); loginPage.login("test-user@localhost", "new-password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } @Test @@ -167,6 +202,8 @@ public class AccountTest { changePasswordPage.open(); loginPage.login("test-user@localhost", "password"); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); + changePasswordPage.changePassword("", "new", "new"); Assert.assertEquals("Please specify password.", profilePage.getError()); @@ -174,6 +211,8 @@ public class AccountTest { changePasswordPage.changePassword("password", "new-password", "new-password"); Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); + + events.expectAccount("update_password").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -189,6 +228,8 @@ public class AccountTest { profilePage.open(); loginPage.login("test-user@localhost", "password"); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); + Assert.assertEquals("", profilePage.getFirstName()); Assert.assertEquals("", profilePage.getLastName()); Assert.assertEquals("test-user@localhost", profilePage.getEmail()); @@ -201,6 +242,8 @@ public class AccountTest { Assert.assertEquals("", profilePage.getLastName()); Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + events.assertEmpty(); + profilePage.updateProfile("New first", "", "new@email.com"); Assert.assertEquals("Please specify last name", profilePage.getError()); @@ -208,6 +251,8 @@ public class AccountTest { Assert.assertEquals("", profilePage.getLastName()); Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + events.assertEmpty(); + profilePage.updateProfile("New first", "New last", ""); Assert.assertEquals("Please specify email", profilePage.getError()); @@ -215,12 +260,17 @@ public class AccountTest { Assert.assertEquals("", profilePage.getLastName()); Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + events.assertEmpty(); + profilePage.updateProfile("New first", "New last", "new@email.com"); Assert.assertEquals("Your account has been updated", profilePage.getSuccess()); Assert.assertEquals("New first", profilePage.getFirstName()); Assert.assertEquals("New last", profilePage.getLastName()); Assert.assertEquals("new@email.com", profilePage.getEmail()); + + events.expectAccount("update_profile").assertEvent(); + events.expectAccount("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); } @Test @@ -228,6 +278,8 @@ public class AccountTest { totpPage.open(); loginPage.login("test-user@localhost", "password"); + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=totp").assertEvent(); + Assert.assertTrue(totpPage.isCurrent()); Assert.assertFalse(driver.getPageSource().contains("Remove Google")); @@ -241,7 +293,13 @@ public class AccountTest { Assert.assertEquals("Google authenticator configured.", profilePage.getSuccess()); + events.expectAccount("update_totp").assertEvent(); + Assert.assertTrue(driver.getPageSource().contains("pficon-delete")); + + totpPage.removeTotp(); + + events.expectAccount("remove_totp").assertEvent(); } @Test @@ -249,6 +307,10 @@ public class AccountTest { profilePage.open(); loginPage.login("test-user-no-access@localhost", "password"); + events.expectLogin().client("account").user(keycloakRule.getUser("test", "test-user-no-access@localhost").getId()) + .detail(Details.USERNAME, "test-user-no-access@localhost") + .detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); + Assert.assertTrue(errorPage.isCurrent()); Assert.assertEquals("No access", errorPage.getError()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java index d153a111f17..59cbd00cade 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java @@ -36,7 +36,6 @@ import java.io.IOException; import java.net.URI; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** 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 7909d8378bc..db4c3eaa1c4 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 @@ -22,13 +22,17 @@ package org.keycloak.testsuite.actions; import org.junit.Assert; +import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; +import org.keycloak.audit.Event; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -53,17 +57,10 @@ import java.util.regex.Pattern; public class RequiredActionEmailVerificationTest { @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { + public static KeycloakRule keycloakRule = new KeycloakRule(); - @Override - public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { - appRealm.setVerifyEmail(true); - - UserModel user = appRealm.getUser("test-user@localhost"); - user.addRequiredAction(RequiredAction.VERIFY_EMAIL); - } - - }); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); @Rule public WebRule webRule = new WebRule(this); @@ -74,6 +71,9 @@ public class RequiredActionEmailVerificationTest { @WebResource protected WebDriver driver; + @WebResource + protected OAuthClient oauth; + @WebResource protected AppPage appPage; @@ -86,6 +86,21 @@ public class RequiredActionEmailVerificationTest { @WebResource protected RegisterPage registerPage; + @Before + public void before() { + keycloakRule.configure(new KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { + appRealm.setVerifyEmail(true); + + UserModel user = appRealm.getUser("test-user@localhost"); + user.setEmailVerified(false); + } + + }); + } + @Test public void verifyEmailExisting() throws IOException, MessagingException { loginPage.open(); @@ -105,9 +120,19 @@ public class RequiredActionEmailVerificationTest { String verificationUrl = m.group(1); + Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(); + + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + + Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1]); + driver.navigate().to(verificationUrl.trim()); + events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().detail(Details.CODE_ID, mailCodeId).assertEvent(); } @Test @@ -116,6 +141,8 @@ public class RequiredActionEmailVerificationTest { loginPage.clickRegister(); registerPage.register("firstName", "lastName", "email", "verifyEmail", "password", "password"); + String userId = events.expectRegister("verifyEmail", "email").assertEvent().getUserId(); + Assert.assertTrue(verifyEmailPage.isCurrent()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -128,23 +155,34 @@ public class RequiredActionEmailVerificationTest { Matcher m = p.matcher(body); m.matches(); + Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent(); + + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + String verificationUrl = m.group(1); driver.navigate().to(verificationUrl.trim()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectRequiredAction("verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent(); + + events.expectLogin().user(userId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent(); } @Test public void verifyEmailResend() throws IOException, MessagingException { loginPage.open(); - loginPage.clickRegister(); - registerPage.register("firstName2", "lastName2", "email2", "verifyEmail2", "password2", "password2"); + loginPage.login("test-user@localhost", "password"); Assert.assertTrue(verifyEmailPage.isCurrent()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); + Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(); + + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + verifyEmailPage.clickResendEmail(); Assert.assertEquals(2, greenMail.getReceivedMessages().length); @@ -157,11 +195,17 @@ public class RequiredActionEmailVerificationTest { Matcher m = p.matcher(body); m.matches(); + events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(sendEvent); + String verificationUrl = m.group(1); driver.navigate().to(verificationUrl.trim()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectRequiredAction("verify_email").detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + + events.expectLogin().assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 917d047786e..c2044ef5e87 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -25,10 +25,13 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -60,9 +63,15 @@ public class RequiredActionMultipleActionsTest { @Rule public WebRule webRule = new WebRule(this); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @WebResource protected WebDriver driver; + @WebResource + protected OAuthClient oauth; + @WebResource protected AppPage appPage; @@ -95,14 +104,21 @@ public class RequiredActionMultipleActionsTest { } Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } public void updatePassword() { changePasswordPage.changePassword("new-password", "new-password"); + + events.expectRequiredAction("update_password").assertEvent(); } public void updateProfile() { updateProfilePage.update("New first", "New last", "new@email.com"); + + events.expectRequiredAction("update_profile").assertEvent(); + events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java index 28197d0ca40..00fe91e175a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java @@ -29,6 +29,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -62,6 +63,9 @@ public class RequiredActionResetPasswordTest { @Rule public WebRule webRule = new WebRule(this); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public GreenMailRule greenMail = new GreenMailRule(); @@ -88,12 +92,20 @@ public class RequiredActionResetPasswordTest { changePasswordPage.assertCurrent(); changePasswordPage.changePassword("new-password", "new-password"); + events.expectRequiredAction("update_password").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().assertEvent(); + oauth.openLogout(); + events.expectLogout().assertEvent(); + loginPage.open(); loginPage.login("test-user@localhost", "new-password"); + + events.expectLogin().assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java index bd5d7004b6a..d9e13c9870a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java @@ -25,10 +25,12 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AccountTotpPage; import org.keycloak.testsuite.pages.AppPage; @@ -59,6 +61,9 @@ public class RequiredActionTotpSetupTest { }); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -94,11 +99,17 @@ public class RequiredActionTotpSetupTest { loginPage.clickRegister(); registerPage.register("firstName", "lastName", "email", "setupTotp", "password", "password"); + String userId = events.expectRegister("setupTotp", "email").assertEvent().getUserId(); + totpPage.assertCurrent(); totpPage.configure(totp.generate(totpPage.getTotpSecret())); + events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp").assertEvent(); } @Test @@ -112,15 +123,23 @@ public class RequiredActionTotpSetupTest { totpPage.configure(totp.generate(totpSecret)); + events.expectRequiredAction("update_totp").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().assertEvent(); + oauth.openLogout(); + events.expectLogout().assertEvent(); + loginPage.open(); loginPage.login("test-user@localhost", "password"); loginTotpPage.login(totp.generate(totpSecret)); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } @Test @@ -130,6 +149,8 @@ public class RequiredActionTotpSetupTest { loginPage.clickRegister(); registerPage.register("firstName2", "lastName2", "email2", "setupTotp2", "password2", "password2"); + String userId = events.expectRegister("setupTotp2", "email2").assertEvent().getUserId(); + // Configure totp totpPage.assertCurrent(); @@ -139,8 +160,13 @@ public class RequiredActionTotpSetupTest { // After totp config, user should be on the app page Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + + events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + // Logout oauth.openLogout(); + events.expectLogout().user(userId).assertEvent(); // Try to login after logout loginPage.open(); @@ -153,15 +179,24 @@ public class RequiredActionTotpSetupTest { // Login with one-time password loginTotpPage.login(totp.generate(totpCode)); + events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + // Open account page accountTotpPage.open(); accountTotpPage.assertCurrent(); + events.expectLogin().user(userId).detail(Details.AUTH_METHOD, "sso").client("account") + .detail(Details.REDIRECT_URI, "http://localhost:8081/auth/rest/realms/test/account/login-redirect?path=totp") + .removeDetail(Details.USERNAME).assertEvent(); + // Remove google authentificator accountTotpPage.removeTotp(); + events.expectAccount("remove_totp").user(userId).assertEvent(); + // Logout oauth.openLogout(); + events.expectLogout().user(userId).assertEvent(); // Try to login loginPage.open(); @@ -171,7 +206,11 @@ public class RequiredActionTotpSetupTest { totpPage.assertCurrent(); totpPage.configure(totp.generate(totpPage.getTotpSecret())); + events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 0b66340928b..3c317b15e7e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -22,18 +22,20 @@ package org.keycloak.testsuite.actions; import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.keycloak.testsuite.rule.KeycloakRule; -import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; @@ -43,20 +45,15 @@ import org.openqa.selenium.WebDriver; */ public class RequiredActionUpdateProfileTest { - @Rule - public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { - - @Override - public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { - UserModel user = appRealm.getUser("test-user@localhost"); - user.addRequiredAction(RequiredAction.UPDATE_PROFILE); - } - - }); + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); @Rule public WebRule webRule = new WebRule(this); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @WebResource protected WebDriver driver; @@ -69,6 +66,17 @@ public class RequiredActionUpdateProfileTest { @WebResource protected LoginUpdateProfilePage updateProfilePage; + @Before + public void before() { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { + UserModel user = appRealm.getUser("test-user@localhost"); + user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + } + }); + } + @Test public void updateProfile() { loginPage.open(); @@ -79,7 +87,12 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.update("New first", "New last", "new@email.com"); + events.expectRequiredAction("update_profile").assertEvent(); + events.expectRequiredAction("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } @Test @@ -95,6 +108,8 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); Assert.assertEquals("Please specify first name", updateProfilePage.getError()); + + events.assertEmpty(); } @Test @@ -110,6 +125,8 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); Assert.assertEquals("Please specify last name", updateProfilePage.getError()); + + events.assertEmpty(); } @Test @@ -125,7 +142,8 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); Assert.assertEquals("Please specify email", updateProfilePage.getError()); + + events.assertEmpty(); } - } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java index 3ddf5659b4a..c971525e48c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java @@ -27,6 +27,7 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.AuthenticationProviderModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -44,6 +45,7 @@ import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; import java.security.PublicKey; +import java.util.Arrays; /** * @author Stian Thorgersen @@ -66,6 +68,7 @@ public class CompositeRoleTest { realm.setSslNotRequired(true); realm.setEnabled(true); realm.addRequiredCredential(UserCredentialModel.PASSWORD); + realm.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER)); final RoleModel realmRole1 = realm.addRole("REALM_ROLE_1"); final RoleModel realmRole2 = realm.addRole("REALM_ROLE_2"); final RoleModel realmRole3 = realm.addRole("REALM_ROLE_3"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java index f9520461ec3..37cde859581 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java @@ -1,10 +1,5 @@ package org.keycloak.testsuite.forms; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import org.junit.Assert; import org.junit.ClassRule; import org.junit.FixMethodOrder; @@ -34,6 +29,11 @@ import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + /** * @author Marek Posolda */ @@ -47,7 +47,7 @@ public class AuthProvidersIntegrationTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { addUser(appRealm, "mary", "mary@test.com", "password-app"); - addUser(adminstrationRealm, "mary", "mary@admin.com", "password-admin"); + addUser(adminstrationRealm, "mary-admin", "mary@admin.com", "password-admin"); AuthenticationProviderModel modelProvider = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_MODEL, false, Collections.EMPTY_MAP); AuthenticationProviderModel picketlinkProvider = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_PICKETLINK, true, Collections.EMPTY_MAP); @@ -116,7 +116,7 @@ public class AuthProvidersIntegrationTest { @Test public void loginExternalModel() { loginPage.open(); - loginPage.login("mary", "password-admin"); + loginPage.login("mary-admin", "password-admin"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -148,18 +148,18 @@ public class AuthProvidersIntegrationTest { try { changePasswordPage.open(); - loginPage.login("mary", "password-admin"); + loginPage.login("mary-admin", "password-admin"); // Can't update to "pass" due to passwordPolicy changePasswordPage.changePassword("password-admin", "pass", "pass"); Assert.assertEquals("Invalid password: minimum length 6", profilePage.getError()); - changePasswordPage.changePassword("password-app", "password-updated", "password-updated"); + changePasswordPage.changePassword("password-admin", "password-updated", "password-updated"); Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); changePasswordPage.logout(); loginPage.open(); - loginPage.login("mary", "password-updated"); + loginPage.login("mary-admin", "password-updated"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); } finally { 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 c46a13a6e99..b4dc34c4bf4 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -26,11 +26,13 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -53,6 +55,8 @@ public class LoginTest { user.setEmail("login@test.com"); user.setEnabled(true); + userId = user.getId(); + UserCredentialModel creds = new UserCredentialModel(); creds.setType(CredentialRepresentation.PASSWORD); creds.setValue("password"); @@ -61,6 +65,9 @@ public class LoginTest { } }); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -76,6 +83,8 @@ public class LoginTest { @WebResource protected LoginPage loginPage; + private static String userId; + @Test public void loginInvalidPassword() { loginPage.open(); @@ -84,6 +93,8 @@ public class LoginTest { loginPage.assertCurrent(); Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + events.expectLogin().user((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent(); } @Test @@ -94,6 +105,8 @@ public class LoginTest { loginPage.assertCurrent(); Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + events.expectLogin().user((String) null).error("user_not_found").detail(Details.USERNAME, "invalid").removeDetail(Details.CODE_ID).assertEvent(); } @Test @@ -103,6 +116,8 @@ public class LoginTest { 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 @@ -112,6 +127,8 @@ public class LoginTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + events.expectLogin().user(userId).detail(Details.USERNAME, "login@test.com").assertEvent(); } @Test @@ -120,8 +137,9 @@ public class LoginTest { loginPage.cancel(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); + + events.expectLogin().error("rejected_by_user").user((String) null).removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index a3ea179ecfa..783f07a790e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -26,12 +26,14 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -44,6 +46,7 @@ import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; import java.net.MalformedURLException; +import java.util.Collections; /** * @author Stian Thorgersen @@ -63,10 +66,14 @@ public class LoginTotpTest { appRealm.updateCredential(user, credentials); user.setTotp(true); + appRealm.setAuditListeners(Collections.singleton("dummy")); } }); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -83,7 +90,7 @@ public class LoginTotpTest { protected LoginPage loginPage; @WebResource - private LoginTotpPage loginTotpPage; + protected LoginTotpPage loginTotpPage; private TimeBasedOTP totp = new TimeBasedOTP(); @@ -103,6 +110,8 @@ public class LoginTotpTest { loginPage.assertCurrent(); Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).user((String) null).assertEvent(); } @Test @@ -115,6 +124,8 @@ public class LoginTotpTest { loginTotpPage.login(totp.generate("totpSecret")); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index cd7795e7a17..aeea46283bb 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -25,9 +25,12 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -45,6 +48,9 @@ public class RegisterTest { @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule(); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -60,6 +66,9 @@ public class RegisterTest { @WebResource protected RegisterPage registerPage; + @WebResource + protected OAuthClient oauth; + @Test public void registerExistingUser() { loginPage.open(); @@ -70,6 +79,8 @@ public class RegisterTest { registerPage.assertCurrent(); Assert.assertEquals("Username already exists", registerPage.getError()); + + events.expectRegister("test-user@localhost", "email").user((String) null).error("username_in_use").assertEvent(); } @Test @@ -82,6 +93,8 @@ public class RegisterTest { registerPage.assertCurrent(); Assert.assertEquals("Password confirmation doesn't match", registerPage.getError()); + + events.expectRegister("registerUserInvalidPasswordConfirm", "email").user((String) null).error("invalid_registration").assertEvent(); } @Test @@ -94,6 +107,8 @@ public class RegisterTest { registerPage.assertCurrent(); Assert.assertEquals("Please specify password.", registerPage.getError()); + + events.expectRegister("registerUserMissingPassword", "email").user((String) null).error("invalid_registration").assertEvent(); } @Test @@ -115,8 +130,14 @@ public class RegisterTest { registerPage.assertCurrent(); Assert.assertEquals("Invalid password: minimum length 8", registerPage.getError()); + events.expectRegister("registerPasswordPolicy", "email").user((String) null).error("invalid_registration").assertEvent(); + registerPage.register("firstName", "lastName", "email", "registerPasswordPolicy", "password", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + String userId = events.expectRegister("registerPasswordPolicy", "email").assertEvent().getUserId(); + + events.expectLogin().user(userId).detail(Details.USERNAME, "registerPasswordPolicy").assertEvent(); } finally { keycloakRule.configure(new KeycloakRule.KeycloakSetup() { @Override @@ -137,6 +158,8 @@ public class RegisterTest { registerPage.assertCurrent(); Assert.assertEquals("Please specify username", registerPage.getError()); + + events.expectRegister(null, "email").removeDetail("username").error("invalid_registration").assertEvent(); } @Test @@ -148,6 +171,9 @@ public class RegisterTest { registerPage.register("firstName", "lastName", "email", "registerUserSuccess", "password", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + String userId = events.expectRegister("registerUserSuccess", "email").assertEvent().getUserId(); + events.expectLogin().detail("username", "registerUserSuccess").user(userId).assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 96f54283af6..0863c1abf80 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -25,12 +25,14 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.audit.Details; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -46,6 +48,7 @@ import org.openqa.selenium.WebDriver; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; +import java.util.Collections; /** * @author Stian Thorgersen @@ -60,14 +63,19 @@ public class ResetPasswordTest { user.setEmail("login@test.com"); user.setEnabled(true); + userId = user.getId(); + UserCredentialModel creds = new UserCredentialModel(); creds.setType(CredentialRepresentation.PASSWORD); creds.setValue("password"); appRealm.updateCredential(user, creds); + appRealm.setAuditListeners(Collections.singleton("dummy")); } })); + private static String userId; + @Rule public WebRule webRule = new WebRule(this); @@ -92,17 +100,31 @@ public class ResetPasswordTest { @WebResource protected LoginPasswordUpdatePage updatePasswordPage; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Test public void resetPassword() throws IOException, MessagingException { + resetPassword("login-test"); + } + + @Test + public void resetPasswordByEmail() throws IOException, MessagingException { + resetPassword("login@test.com"); + } + + private void resetPassword(String username) throws IOException, MessagingException { loginPage.open(); loginPage.resetPassword(); resetPasswordPage.assertCurrent(); - resetPasswordPage.changePassword("login-test"); + resetPasswordPage.changePassword(username); resetPasswordPage.assertCurrent(); + events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); + Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -118,50 +140,21 @@ public class ResetPasswordTest { updatePasswordPage.changePassword("resetPassword", "resetPassword"); + events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, username).assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent(); + oauth.openLogout(); + events.expectLogout().user(userId).assertEvent(); + loginPage.open(); loginPage.login("login-test", "resetPassword"); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - } - - @Test - public void resetPasswordByEmail() throws IOException, MessagingException { - loginPage.open(); - loginPage.resetPassword(); - - resetPasswordPage.assertCurrent(); - - resetPasswordPage.changePassword("login@test.com"); - - resetPasswordPage.assertCurrent(); - - Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - - Assert.assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String body = (String) message.getContent(); - String changePasswordUrl = body.split("\n")[3]; - - driver.navigate().to(changePasswordUrl.trim()); - - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("resetPassword", "resetPassword"); - - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - oauth.openLogout(); - - loginPage.open(); - - loginPage.login("login@test.com", "resetPassword"); + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } @@ -182,6 +175,8 @@ public class ResetPasswordTest { Thread.sleep(1000); Assert.assertEquals(0, greenMail.getReceivedMessages().length); + + events.expectRequiredAction("send_reset_password").user((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent(); } @Test @@ -211,6 +206,8 @@ public class ResetPasswordTest { String body = (String) message.getContent(); String changePasswordUrl = body.split("\n")[3]; + events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); + driver.navigate().to(changePasswordUrl.trim()); updatePasswordPage.assertCurrent(); @@ -221,14 +218,23 @@ public class ResetPasswordTest { updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); + events.expectRequiredAction("update_password").user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); + oauth.openLogout(); + events.expectLogout().user(userId).assertEvent(); + loginPage.open(); loginPage.login("login-test", "resetPasswordWithPasswordPolicy"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java index a0adaf280d7..c02794ef099 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java @@ -26,6 +26,8 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; @@ -63,6 +65,9 @@ public class SSOTest { @WebResource protected AccountUpdateProfilePage profilePage; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Test public void loginSuccess() { loginPage.open(); @@ -71,6 +76,8 @@ public class SSOTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + events.expectLogin().assertEvent(); + appPage.open(); oauth.openLoginForm(); @@ -80,6 +87,9 @@ public class SSOTest { profilePage.open(); Assert.assertTrue(profilePage.isCurrent()); + + events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent(); + events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("account").detail(Details.REDIRECT_URI, "http://localhost:8081/auth/rest/realms/test/account/login-redirect").assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index fa96732da06..4829497da16 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -26,7 +26,10 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; +import org.keycloak.audit.Event; import org.keycloak.representations.AccessToken; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.pages.LoginPage; @@ -59,10 +62,15 @@ public class AccessTokenTest { @WebResource protected LoginPage loginPage; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Test public void accessTokenRequest() throws Exception { oauth.doLogin("test-user@localhost", "password"); + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); @@ -82,6 +90,28 @@ public class AccessTokenTest { Assert.assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size()); Assert.assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user")); + + Event event = events.expectCodeToToken(codeId).assertEvent(); + Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); + Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); + + response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(400, response.getStatusCode()); + + events.expectCodeToToken(codeId).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).client((String) null).user((String) null).assertEvent(); + } + + @Test + public void accessTokenInvalidClientCredentials() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + AccessTokenResponse response = oauth.doAccessTokenRequest(code, "invalid"); + Assert.assertEquals(400, response.getStatusCode()); + + events.expectCodeToToken(codeId).error("invalid_client_credentials").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index d76d189a8e4..7bb9c689fca 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -26,13 +26,14 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; -import org.keycloak.models.ApplicationModel; +import org.keycloak.audit.Details; +import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AuthorizationCodeResponse; -import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; @@ -62,8 +63,8 @@ public class AuthorizationCodeTest { @WebResource protected LoginPage loginPage; - @WebResource - protected ErrorPage errorPage; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); @Test public void authorizationRequest() throws IOException { @@ -77,6 +78,9 @@ public class AuthorizationCodeTest { Assert.assertNull(response.getError()); oauth.verifyCode(response.getCode()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString()); } @Test @@ -90,6 +94,9 @@ public class AuthorizationCodeTest { String code = driver.findElement(By.id(OAuth2Constants.CODE)).getText(); oauth.verifyCode(code); + + String codeId = events.expectLogin().detail(Details.REDIRECT_URI, Constants.INSTALLED_APP_URN).assertEvent().getDetails().get(Details.CODE_ID); + Assert.assertEquals(codeId, new JWSInput(code).readContentAsString()); } @Test @@ -109,6 +116,9 @@ public class AuthorizationCodeTest { Assert.assertNotNull(response.getCode()); oauth.verifyCode(response.getCode()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString()); } @Test @@ -121,6 +131,9 @@ public class AuthorizationCodeTest { Assert.assertNull(response.getError()); oauth.verifyCode(response.getCode()); + + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + Assert.assertEquals(codeId, new JWSInput(response.getCode()).readContentAsString()); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java index cd43cb40751..2072e6e0c84 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -21,15 +21,14 @@ */ package org.keycloak.testsuite.oauth; -import java.io.IOException; -import java.util.Map; - import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; import org.keycloak.representations.AccessToken; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.OAuthGrantPage; @@ -38,6 +37,9 @@ import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; +import java.io.IOException; +import java.util.Map; + /** * @author Viliam Rockai */ @@ -46,6 +48,9 @@ public class OAuthGrantTest { @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule(); + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Rule public WebRule webRule = new WebRule(this); @@ -76,6 +81,9 @@ public class OAuthGrantTest { grantPage.accept(); Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE)); + + String codeId = events.expectLogin().client("third-party").assertEvent().getDetails().get(Details.CODE_ID); + OAuthClient.AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password"); AccessToken token = oauth.verifyToken(accessToken.getAccessToken()); @@ -88,6 +96,8 @@ public class OAuthGrantTest { Assert.assertEquals(1, resourceAccess.size()); Assert.assertEquals(1, resourceAccess.get("test-app").getRoles().size()); Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user")); + + events.expectCodeToToken(codeId).client("third-party").assertEvent(); } @Test @@ -103,5 +113,8 @@ public class OAuthGrantTest { Assert.assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.ERROR)); Assert.assertEquals("access_denied", oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); + + events.expectLogin().client("third-party").error("rejected_by_user").assertEvent(); } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 5780874081a..beb8e77b7f2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -26,8 +26,11 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; +import org.keycloak.audit.Details; +import org.keycloak.audit.Event; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.pages.LoginPage; @@ -61,10 +64,15 @@ public class RefreshTokenTest { @WebResource protected LoginPage loginPage; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @Test public void refreshTokenRequest() throws Exception { oauth.doLogin("test-user@localhost", "password"); + String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); @@ -72,6 +80,8 @@ public class RefreshTokenTest { String refreshTokenString = tokenResponse.getRefreshToken(); RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + Event tokenEvent = events.expectCodeToToken(codeId).assertEvent(); + Assert.assertNotNull(refreshTokenString); Assert.assertEquals("bearer", tokenResponse.getTokenType()); @@ -106,6 +116,10 @@ public class RefreshTokenTest { Assert.assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size()); Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user")); + + Event refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID)).assertEvent(); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID)); + Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID)); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java index 980048ab379..0855be70f42 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java @@ -7,9 +7,9 @@ import io.undertow.servlet.api.ServletInfo; import io.undertow.servlet.api.WebResourceCollection; import org.junit.rules.ExternalResource; import org.keycloak.models.Config; -import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.ModelToRepresentation; @@ -40,7 +40,8 @@ public abstract class AbstractKeycloakRule extends ExternalResource { public UserRepresentation getUser(String realm, String name) { KeycloakSession session = server.getKeycloakSessionFactory().createSession(); try { - return ModelToRepresentation.toRepresentation(session.getRealmByName(realm).getUser(name)); + UserModel user = session.getRealmByName(realm).getUser(name); + return user != null ? ModelToRepresentation.toRepresentation(user) : null; } finally { session.close(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java index 3e6e7af1e25..e547e13c073 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java @@ -22,12 +22,8 @@ package org.keycloak.testsuite.rule; import org.keycloak.models.Config; -import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.ApplicationServlet; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java index 25e1cd5309d..84245509973 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java @@ -27,13 +27,12 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; -import org.keycloak.models.AccountRoles; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.Constants; +import org.keycloak.audit.Details; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.DummySocialServlet; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; @@ -87,6 +86,9 @@ public class SocialLoginTest { @WebResource protected OAuthClient oauth; + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + @BeforeClass public static void before() { keycloakRule.deployServlet("dummy-social", "/dummy-social", DummySocialServlet.class); @@ -107,8 +109,21 @@ public class SocialLoginTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + String userId = events.expect("register") + .user(AssertEvents.isUUID()) + .detail(Details.EMAIL, "bob@builder.com") + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.REGISTER_METHOD, "social") + .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) + .detail(Details.USERNAME, "1@dummy") + .assertEvent().getUserId(); + + String codeId = events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social").assertEvent().getDetails().get(Details.CODE_ID); + AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password"); + events.expectCodeToToken(codeId).user(userId).assertEvent(); + AccessToken token = oauth.verifyToken(response.getAccessToken()); Assert.assertEquals(36, token.getSubject().length()); @@ -118,8 +133,21 @@ public class SocialLoginTest { Assert.assertEquals("Bob", profile.getFirstName()); Assert.assertEquals("Builder", profile.getLastName()); Assert.assertEquals("bob@builder.com", profile.getEmail()); - } + oauth.openLogout(); + + events.expectLogout().user(userId).assertEvent(); + + loginPage.open(); + + loginPage.clickSocial("dummy"); + + driver.findElement(By.id("id")).sendKeys("1"); + driver.findElement(By.id("username")).sendKeys("dummy-user1"); + driver.findElement(By.id("login")).click(); + + events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social").assertEvent(); + } @Test public void loginCancelled() throws Exception { @@ -132,9 +160,13 @@ public class SocialLoginTest { Assert.assertTrue(loginPage.isCurrent()); Assert.assertEquals("Access denied", loginPage.getWarning()); + events.expectLogin().error("rejected_by_user").user((String) null).detail(Details.AUTH_METHOD, "social").removeDetail(Details.USERNAME).removeDetail(Details.CODE_ID).assertEvent(); + loginPage.login("test-user@localhost", "password"); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); } @Test @@ -164,13 +196,29 @@ public class SocialLoginTest { Assert.assertEquals("Builder", profilePage.getLastName()); Assert.assertEquals("bob@builder.com", profilePage.getEmail()); + String userId = events.expect("register") + .user(AssertEvents.isUUID()) + .detail(Details.EMAIL, "bob@builder.com") + .detail(Details.RESPONSE_TYPE, "code") + .detail(Details.REGISTER_METHOD, "social") + .detail(Details.REDIRECT_URI, AssertEvents.DEFAULT_REDIRECT_URI) + .detail(Details.USERNAME, "2@dummy") + .assertEvent().getUserId(); + profilePage.update("Dummy", "User", "dummy-user-reg@dummy-social"); + events.expectRequiredAction("update_profile").user(userId).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").assertEvent(); + events.expectRequiredAction("update_email").user(userId).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").detail(Details.PREVIOUS_EMAIL, "bob@builder.com").detail(Details.UPDATED_EMAIL, "dummy-user-reg@dummy-social").assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + String codeId = events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "social").detail(Details.USERNAME, "2@dummy").assertEvent().getDetails().get(Details.CODE_ID); + AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password"); AccessToken token = oauth.verifyToken(response.getAccessToken()); + events.expectCodeToToken(codeId).user(userId).assertEvent(); + UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject()); Assert.assertEquals("Dummy", profile.getFirstName()); diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory new file mode 100644 index 00000000000..7389c6fbab2 --- /dev/null +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.AssertEvents \ No newline at end of file diff --git a/testsuite/performance/pom.xml b/testsuite/performance/pom.xml index da21ebc8f5f..8480c5981e1 100755 --- a/testsuite/performance/pom.xml +++ b/testsuite/performance/pom.xml @@ -78,8 +78,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + ${maven.compiler.source} + ${maven.compiler.target} diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 6d9ccb018a1..b5bc8e61420 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -1,5 +1,6 @@ - + keycloak-parent org.keycloak