From 225307e855a3a5f1fe52a8d17f4b2a9939c6d2fb Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 25 Mar 2014 10:36:15 +0000 Subject: [PATCH] KEYCLOAK-389 Added AuditListener SPI KEYCLOAK-390 Added JBoss Logging AuditListener KEYCLOAK-391 Audit Token events --- audit/api/pom.xml | 39 +++ .../main/java/org/keycloak/audit/Audit.java | 141 ++++++++ .../org/keycloak/audit/AuditListener.java | 12 + .../java/org/keycloak/audit/AuditLoader.java | 31 ++ .../org/keycloak/audit/AuditProvider.java | 10 + .../main/java/org/keycloak/audit/Details.java | 22 ++ .../main/java/org/keycloak/audit/Errors.java | 36 ++ .../main/java/org/keycloak/audit/Event.java | 108 ++++++ .../java/org/keycloak/audit/EventQuery.java | 24 ++ .../main/java/org/keycloak/audit/Events.java | 29 ++ audit/jboss-logging/pom.xml | 33 ++ .../audit/log/JBossLoggingAuditListener.java | 63 ++++ .../services/org.keycloak.audit.AuditListener | 1 + audit/jpa/pom.xml | 29 ++ audit/pom.xml | 23 ++ .../java/org/keycloak/models/RealmModel.java | 4 + .../org/keycloak/models/jpa/RealmAdapter.java | 11 + .../models/jpa/entities/RealmEntity.java | 13 + .../mongo/keycloak/adapters/RealmAdapter.java | 15 + .../mongo/keycloak/entities/RealmEntity.java | 14 + model/pom.xml | 2 +- pom.xml | 1 + services/pom.xml | 6 + .../services/managers/AccessCodeEntry.java | 18 + .../services/managers/ApplianceBootstrap.java | 4 + .../services/managers/RealmManager.java | 2 + .../services/managers/TokenManager.java | 28 +- .../services/resources/AccountService.java | 36 +- .../services/resources/RealmsResource.java | 13 +- .../resources/RequiredActionsService.java | 68 +++- .../services/resources/SocialResource.java | 82 ++++- .../services/resources/TokenService.java | 176 ++++++++-- .../services/resources/flows/OAuthFlows.java | 38 ++- testsuite/integration/pom.xml | 10 + .../testsuite/ApplicationServlet.java | 6 - .../org/keycloak/testsuite/AssertEvents.java | 310 ++++++++++++++++++ .../testsuite/DummySocialServlet.java | 1 - .../{forms => account}/AccountTest.java | 74 ++++- .../testsuite/account/ProfileTest.java | 1 - .../RequiredActionEmailVerificationTest.java | 70 +++- .../RequiredActionMultipleActionsTest.java | 16 + .../RequiredActionResetPasswordTest.java | 12 + .../actions/RequiredActionTotpSetupTest.java | 39 +++ .../RequiredActionUpdateProfileTest.java | 44 ++- .../forms/AuthProvidersIntegrationTest.java | 10 +- .../keycloak/testsuite/forms/LoginTest.java | 20 +- .../testsuite/forms/LoginTotpTest.java | 13 +- .../testsuite/forms/RegisterTest.java | 26 ++ .../testsuite/forms/ResetPasswordTest.java | 80 ++--- .../org/keycloak/testsuite/forms/SSOTest.java | 10 + .../testsuite/oauth/AccessTokenTest.java | 30 ++ .../oauth/AuthorizationCodeTest.java | 21 +- .../testsuite/oauth/OAuthGrantTest.java | 19 +- .../testsuite/oauth/RefreshTokenTest.java | 14 + .../testsuite/rule/AbstractKeycloakRule.java | 5 +- .../keycloak/testsuite/rule/KeycloakRule.java | 4 - .../testsuite/social/SocialLoginTest.java | 56 +++- .../services/org.keycloak.audit.AuditListener | 1 + 58 files changed, 1859 insertions(+), 165 deletions(-) create mode 100755 audit/api/pom.xml create mode 100644 audit/api/src/main/java/org/keycloak/audit/Audit.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/AuditListener.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/AuditLoader.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/AuditProvider.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/Details.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/Errors.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/Event.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/EventQuery.java create mode 100644 audit/api/src/main/java/org/keycloak/audit/Events.java create mode 100755 audit/jboss-logging/pom.xml create mode 100644 audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListener.java create mode 100644 audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListener create mode 100755 audit/jpa/pom.xml create mode 100755 audit/pom.xml create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java rename testsuite/integration/src/test/java/org/keycloak/testsuite/{forms => account}/AccountTest.java (77%) create mode 100644 testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListener diff --git a/audit/api/pom.xml b/audit/api/pom.xml new file mode 100755 index 0000000000..facd5866a3 --- /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 0000000000..ad237011ae --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java @@ -0,0 +1,141 @@ +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 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) { + List listeners = null; + if (realm.getAuditListeners() != null) { + listeners = new LinkedList(); + + for (String id : realm.getAuditListeners()) { + listeners.add(AuditLoader.load(id)); + } + } + 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 0000000000..e5213006c9 --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditListener.java @@ -0,0 +1,12 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface AuditListener { + + public String getId(); + + public void onEvent(Event event); + +} diff --git a/audit/api/src/main/java/org/keycloak/audit/AuditLoader.java b/audit/api/src/main/java/org/keycloak/audit/AuditLoader.java new file mode 100644 index 0000000000..f7dfd9855d --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditLoader.java @@ -0,0 +1,31 @@ +package org.keycloak.audit; + +import org.keycloak.util.ProviderLoader; + +/** + * @author Stian Thorgersen + */ +public class AuditLoader { + + private AuditLoader() { + } + + public static AuditListener load(String id) { + if (id == null) { + throw new NullPointerException(); + } + + for (AuditListener l : load()) { + if (id.equals(l.getId())) { + return l; + } + } + + return null; + } + + public static Iterable load() { + return ProviderLoader.load(AuditListener.class); + } + +} 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 0000000000..9cc2c0e32e --- /dev/null +++ b/audit/api/src/main/java/org/keycloak/audit/AuditProvider.java @@ -0,0 +1,10 @@ +package org.keycloak.audit; + +/** + * @author Stian Thorgersen + */ +public interface AuditProvider extends AuditListener { + + public EventQuery createQuery(); + +} 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 0000000000..1a8df0ad40 --- /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 0000000000..b34371419f --- /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 0000000000..85b968a8c3 --- /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 0000000000..75e07c03cd --- /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 0000000000..9d1a54f9a9 --- /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 0000000000..f07d231620 --- /dev/null +++ b/audit/jboss-logging/pom.xml @@ -0,0 +1,33 @@ + + + + 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-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 0000000000..bec3b9dcfc --- /dev/null +++ b/audit/jboss-logging/src/main/java/org/keycloak/audit/log/JBossLoggingAuditListener.java @@ -0,0 +1,63 @@ +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 static final Logger logger = Logger.getLogger("org.keycloak.audit"); + + @Override + public String getId() { + return "jboss-logging"; + } + + @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()); + } + } + +} diff --git a/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListener b/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListener new file mode 100644 index 0000000000..e7cacc5f38 --- /dev/null +++ b/audit/jboss-logging/src/main/resources/META-INF/services/org.keycloak.audit.AuditListener @@ -0,0 +1 @@ +org.keycloak.audit.log.JBossLoggingAuditListener \ No newline at end of file diff --git a/audit/jpa/pom.xml b/audit/jpa/pom.xml new file mode 100755 index 0000000000..747bcc06be --- /dev/null +++ b/audit/jpa/pom.xml @@ -0,0 +1,29 @@ + + + + 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 + + + junit + junit + test + + + + diff --git a/audit/pom.xml b/audit/pom.xml new file mode 100755 index 0000000000..8ef44513bc --- /dev/null +++ b/audit/pom.xml @@ -0,0 +1,23 @@ + + + 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 + + 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 e07bdb95c5..827f628048 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -202,4 +202,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/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 17c5a1a8f8..dbb3a92c88 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 @@ -1154,4 +1154,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/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 04051edddd..c0999130c8 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,9 @@ import javax.persistence.OneToMany; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -95,6 +97,9 @@ public class RealmEntity { @JoinTable(name="RealmDefaultRoles") Collection defaultRoles = new ArrayList(); + @ElementCollection + protected Set auditListeners= new HashSet(); + public String getId() { return id; } @@ -333,5 +338,13 @@ public class RealmEntity { public void setNotBefore(int notBefore) { this.notBefore = notBefore; } + + public Set getAuditListeners() { + return auditListeners; + } + + public void setAuditListeners(Set auditListeners) { + this.auditListeners = auditListeners; + } } 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 68a604f8b1..fb9619a70f 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 @@ -38,6 +38,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; @@ -1113,6 +1114,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 cf37018970..69d4b2a743 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 @@ -53,6 +56,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; @@ -287,6 +292,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/pom.xml b/model/pom.xml index ee8798741b..73bbbcb66d 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -6,7 +6,7 @@ 1.0-beta-1-SNAPSHOT ../pom.xml - Examples + Model Parent 4.0.0 diff --git a/pom.xml b/pom.xml index 5246554097..dab6a584f3 100755 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ + audit core core-jaxrs model diff --git a/services/pom.xml b/services/pom.xml index 0571d9251b..7e0f2507fc 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -36,6 +36,12 @@ ${project.version} provided + + org.keycloak + keycloak-audit-api + ${project.version} + provided + org.keycloak keycloak-account-api 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 51ea351048..6b9e50c645 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 54d2f1f770..8233a4c166 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -13,6 +13,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 $ @@ -61,6 +63,8 @@ public class ApplianceBootstrap { 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/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index abe30063be..d2e42f6462 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -81,6 +81,8 @@ public class RealmManager { setupAdminManagement(realm); setupAccountManagement(realm); + realm.setAuditListeners(Collections.singleton("jboss-logging")); + return realm; } 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 a9e6bcfea1..73688b875b 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 8a4da96177..4ac9a08cfd 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); } @@ -253,6 +275,8 @@ public class AccountService { 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 +322,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 96d2c3f0e2..4601559922 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 c070723643..c6b0a6ca85 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"); @@ -186,6 +212,8 @@ public class RequiredActionsService { accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD); } + audit.clone().event(Events.UPDATE_PASSWORD).success(); + return redirectOauth(user, accessCode); } @@ -201,11 +229,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 +246,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 +259,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 +292,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 +305,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 +313,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 +387,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 45f9a0e407..3d5bfa52c1 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 1506a60c85..00fbbb400d 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; @@ -73,6 +77,7 @@ public class TokenService { protected RealmModel realm; protected TokenManager tokenManager; + private Audit audit; protected AuthenticationManager authManager = new AuthenticationManager(); @Context @@ -97,9 +102,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) { @@ -143,31 +149,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(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(); } @@ -182,22 +199,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(); } @@ -208,6 +231,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()) { @@ -215,30 +255,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(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); @@ -249,20 +291,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; } @@ -273,30 +321,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"); } @@ -312,13 +376,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(); } @@ -327,7 +391,7 @@ 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(); @@ -342,6 +406,9 @@ public class TokenService { } } + audit.user(user).success(); + audit.reset(); + return processLogin(clientId, scopeParam, state, redirect, formData); } @@ -362,19 +429,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); @@ -388,22 +456,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(); } @@ -411,6 +490,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(); } @@ -418,19 +498,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) { @@ -453,11 +538,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()); } @@ -465,6 +553,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()); } @@ -472,6 +561,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()); } } @@ -484,6 +574,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()) { @@ -492,20 +585,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."); } @@ -513,7 +610,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")) { @@ -529,6 +627,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()) { @@ -537,26 +638,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"); } @@ -571,6 +677,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) { @@ -578,6 +686,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"); } @@ -589,6 +699,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()) { @@ -604,21 +716,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 26e4888c80..49ff5d707c 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/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index e784d04366..ae782b360b 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -36,6 +36,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 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 8d910af0f5..ab6e3b3a0f 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 0000000000..8113188fcc --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -0,0 +1,310 @@ +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.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, AuditListener{ + + 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 void onEvent(Event event) { + events.add(event); + } + + @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); + } + + 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 7ddf7efcb3..fe2745c413 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 27e013b646..c2ad75ee08 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 d153a111f1..59cbd00cad 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 7909d8378b..db4c3eaa1c 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 917d047786..c2044ef5e8 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 28197d0ca4..00fe91e175 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 bd5d7004b6..d9e13c9870 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 0b66340928..3c317b15e7 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/forms/AuthProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java index f9520461ec..c34e77e9cd 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 */ 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 c46a13a6e9..b4dc34c4bf 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 a3ea179ecf..783f07a790 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 cd7795e7a1..aeea46283b 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 96f54283af..0863c1abf8 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 a0adaf280d..c02794ef09 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 fa96732da0..4829497da1 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 d76d189a8e..7bb9c689fc 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 cd43cb4075..2072e6e0c8 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 5780874081..beb8e77b7f 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 980048ab37..0855be70f4 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 3e6e7af1e2..e547e13c07 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 25e1cd5309..8424550997 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.AuditListener b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListener new file mode 100644 index 0000000000..7389c6fbab --- /dev/null +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.audit.AuditListener @@ -0,0 +1 @@ +org.keycloak.testsuite.AssertEvents \ No newline at end of file