keycloak-aplcache
Changes
audit/api/pom.xml 39(+39 -0)
audit/jboss-logging/pom.xml 33(+33 -0)
audit/jpa/pom.xml 29(+29 -0)
audit/pom.xml 23(+23 -0)
model/pom.xml 2(+1 -1)
pom.xml 1(+1 -0)
services/pom.xml 6(+6 -0)
testsuite/integration/pom.xml 10(+10 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 70(+57 -13)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java 16(+16 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java 12(+12 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java 39(+39 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java 44(+31 -13)
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java 10(+5 -5)
Details
audit/api/pom.xml 39(+39 -0)
diff --git a/audit/api/pom.xml b/audit/api/pom.xml
new file mode 100755
index 0000000..facd586
--- /dev/null
+++ b/audit/api/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<project>
+ <parent>
+ <artifactId>keycloak-audit-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-audit-api</artifactId>
+ <name>Keycloak Audit API</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
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 0000000..ad23701
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Audit {
+
+ private static final Logger log = Logger.getLogger(Audit.class);
+
+ private List<AuditListener> listeners;
+ private Event event;
+
+ public static Audit create(RealmModel realm, String ipAddress) {
+ List<AuditListener> listeners = null;
+ if (realm.getAuditListeners() != null) {
+ listeners = new LinkedList<AuditListener>();
+
+ for (String id : realm.getAuditListeners()) {
+ listeners.add(AuditLoader.load(id));
+ }
+ }
+ return new Audit(listeners, new Event()).realm(realm).ipAddress(ipAddress);
+ }
+
+ private Audit(List<AuditListener> 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<String, String>());
+ }
+ 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 0000000..e521300
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/AuditListener.java
@@ -0,0 +1,12 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..f7dfd98
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<AuditListener> 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 0000000..9cc2c0e
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/AuditProvider.java
@@ -0,0 +1,10 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..1a8df0a
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/Details.java
@@ -0,0 +1,22 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..b343714
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/Errors.java
@@ -0,0 +1,36 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..85b968a
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> 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<String, String> getDetails() {
+ return details;
+ }
+
+ public void setDetails(Map<String, String> 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<String, String>(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 0000000..75e07c0
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<Event> 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 0000000..9d1a54f
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/Events.java
@@ -0,0 +1,29 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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";
+
+}
audit/jboss-logging/pom.xml 33(+33 -0)
diff --git a/audit/jboss-logging/pom.xml b/audit/jboss-logging/pom.xml
new file mode 100755
index 0000000..f07d231
--- /dev/null
+++ b/audit/jboss-logging/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<project>
+ <parent>
+ <artifactId>keycloak-audit-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-audit-jboss-logging</artifactId>
+ <name>Keycloak Audit JBoss Logging Provider</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-audit-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
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 0000000..bec3b9d
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> 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 0000000..e7cacc5
--- /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
audit/jpa/pom.xml 29(+29 -0)
diff --git a/audit/jpa/pom.xml b/audit/jpa/pom.xml
new file mode 100755
index 0000000..747bcc0
--- /dev/null
+++ b/audit/jpa/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<project>
+ <parent>
+ <artifactId>keycloak-audit-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-audit-jpa</artifactId>
+ <name>Keycloak Audit JPA Provider</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
audit/pom.xml 23(+23 -0)
diff --git a/audit/pom.xml b/audit/pom.xml
new file mode 100755
index 0000000..8ef4451
--- /dev/null
+++ b/audit/pom.xml
@@ -0,0 +1,23 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <name>Audit Parent</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-audit-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>api</module>
+ <module>jpa</module>
+ <module>jboss-logging</module>
+ </modules>
+</project>
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 e07bdb9..827f628 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<String> getAuditListeners();
+
+ void setAuditListeners(Set<String> listeners);
}
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 04051ed..c099913 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -95,6 +97,9 @@ public class RealmEntity {
@JoinTable(name="RealmDefaultRoles")
Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
+ @ElementCollection
+ protected Set<String> auditListeners= new HashSet<String>();
+
public String getId() {
return id;
}
@@ -333,5 +338,13 @@ public class RealmEntity {
public void setNotBefore(int notBefore) {
this.notBefore = notBefore;
}
+
+ public Set<String> getAuditListeners() {
+ return auditListeners;
+ }
+
+ public void setAuditListeners(Set<String> auditListeners) {
+ this.auditListeners = auditListeners;
+ }
}
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 17c5a1a..dbb3a92 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<String> getAuditListeners() {
+ return realm.getAuditListeners();
+ }
+
+ @Override
+ public void setAuditListeners(Set<String> listeners) {
+ realm.setAuditListeners(listeners);
+ em.flush();
+ }
}
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 68a604f..fb9619a 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;
@@ -1114,6 +1115,20 @@ public class RealmAdapter extends AbstractMongoAdapter<RealmEntity> implements R
}
@Override
+ public Set<String> getAuditListeners() {
+ return realm.getAuditListeners() != null ? new HashSet<String>(realm.getAuditListeners()) : null;
+ }
+
+ @Override
+ public void setAuditListeners(Set<String> listeners) {
+ if (listeners != null) {
+ realm.setAuditListeners(new LinkedList<String>(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 cf37018..69d4b2a 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -53,6 +56,8 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
private Map<String, String> socialConfig = new HashMap<String, String>();
private Map<String, String> ldapServerConfig;
+ private List<String> auditListeners = new LinkedList<String>();
+
@MongoField
public String getName() {
return name;
@@ -287,6 +292,15 @@ public class RealmEntity extends AbstractMongoIdentifiableEntity implements Mong
this.ldapServerConfig = ldapServerConfig;
}
+ @MongoField
+ public List<String> getAuditListeners() {
+ return auditListeners;
+ }
+
+ public void setAuditListeners(List<String> auditListeners) {
+ this.auditListeners = auditListeners;
+ }
+
@Override
public void afterRemove(MongoStoreInvocationContext context) {
DBObject query = new QueryBuilder()
model/pom.xml 2(+1 -1)
diff --git a/model/pom.xml b/model/pom.xml
index ee87987..73bbbcb 100755
--- a/model/pom.xml
+++ b/model/pom.xml
@@ -6,7 +6,7 @@
<version>1.0-beta-1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
- <name>Examples</name>
+ <name>Model Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>
pom.xml 1(+1 -0)
diff --git a/pom.xml b/pom.xml
index 5246554..dab6a58 100755
--- a/pom.xml
+++ b/pom.xml
@@ -80,6 +80,7 @@
</contributors>
<modules>
+ <module>audit</module>
<module>core</module>
<module>core-jaxrs</module>
<module>model</module>
services/pom.xml 6(+6 -0)
diff --git a/services/pom.xml b/services/pom.xml
index 0571d92..7e0f250 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -38,6 +38,12 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-audit-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>
<version>${project.version}</version>
</dependency>
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 51ea351..6b9e50c 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 54d2f1f..8233a4c 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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 abe3006..d2e42f6 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 a9e6bcf..73688b8 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 8a4da96..4ac9a08 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/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index 26e4888..49ff5d7 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<RequiredAction> requiredActions = user.getRequiredActions();
if (!requiredActions.isEmpty()) {
accessCode.setRequiredActions(new HashSet<UserModel.RequiredAction>(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/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 96d2c3f..4601559 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 c070723..c6b0a6c 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<RequiredAction> requiredActions = new HashSet<RequiredAction>(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 45f9a0e..3d5bfa5 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<String, String> queryParms = new MultivaluedHashMap<String, String>();
- 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");
}
- realm.addSocialLink(authenticatedUser, socialLink);
- logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getLoginName());
-
- String redirectUri = requestData.getClientAttributes().get("redirectUri");
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());
+
+ 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 1506a60..00fbbb4 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<String, String> error = new HashMap<String, String>();
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<String, String> 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<String, String> 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<String, String> error = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
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<String, String> res = new HashMap<String, String>();
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<String, String> formData) {
+ protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> 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<String, String> error = new HashMap<String, String>();
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<String, String> error = new HashMap<String, String>();
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<String, String> error = new HashMap<String, String>();
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<String, String> 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);
}
testsuite/integration/pom.xml 10(+10 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index e784d04..ae782b3 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -38,6 +38,16 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-audit-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-audit-jboss-logging</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-ui-styles</artifactId>
<version>${project.version}</version>
</dependency>
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 d153a11..59cbd00 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 7909d83..db4c3ea 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);
@@ -75,6 +72,9 @@ public class RequiredActionEmailVerificationTest {
protected WebDriver driver;
@WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
protected AppPage appPage;
@WebResource
@@ -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 917d047..c2044ef 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,10 +63,16 @@ 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;
@WebResource
@@ -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 28197d0..00fe91e 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;
@@ -63,6 +64,9 @@ public class RequiredActionResetPasswordTest {
public WebRule webRule = new WebRule(this);
@Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
public GreenMailRule greenMail = new GreenMailRule();
@WebResource
@@ -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 bd5d700..d9e13c9 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;
@@ -60,6 +62,9 @@ public class RequiredActionTotpSetupTest {
});
@Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
public WebRule webRule = new WebRule(this);
@WebResource
@@ -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 0b66340..3c317b1 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/ApplicationServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java
index 8d910af..ab6e3b3 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
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 0000000..8113188
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<Event> events = new LinkedBlockingQueue<Event>();
+
+ 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<String> listeners = new HashSet<String>();
+ 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<String> userId;
+ private HashMap<String, Matcher<String>> 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<String> 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<String> matcher) {
+ if (details == null) {
+ details = new HashMap<String, Matcher<String>>();
+ }
+ 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<String, Matcher<String>> 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<String> isCodeId() {
+ return new TypeSafeMatcher<String>() {
+ @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<String> isUUID() {
+ return new TypeSafeMatcher<String>() {
+ @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 7ddf7ef..fe2745c 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/AuthProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AuthProvidersIntegrationTest.java
index f952046..c34e77e 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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 c46a13a..b4dc34c 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");
@@ -62,6 +66,9 @@ public class LoginTest {
});
@Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
public WebRule webRule = new WebRule(this);
@WebResource
@@ -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 a3ea179..783f07a 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -63,11 +66,15 @@ 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);
@Rule
@@ -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 cd7795e..aeea462 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;
@@ -46,6 +49,9 @@ public class RegisterTest {
public static KeycloakRule keycloakRule = new KeycloakRule();
@Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
public WebRule webRule = new WebRule(this);
@WebResource
@@ -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 96f5428..0863c1a 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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,54 +100,31 @@ public class ResetPasswordTest {
@WebResource
protected LoginPasswordUpdatePage updatePasswordPage;
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
@Test
public void resetPassword() throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- 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", "resetPassword");
-
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ 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.com");
+ 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);
@@ -155,13 +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.com", "resetPassword");
+ loginPage.login("login-test", "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 a0adaf2..c02794e 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 fa96732..4829497 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 d76d189..7bb9c68 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 cd43cb4..2072e6e 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 <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
*/
@@ -47,6 +49,9 @@ public class OAuthGrantTest {
public static KeycloakRule keycloakRule = new KeycloakRule();
@Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
public WebRule webRule = new WebRule(this);
@WebResource
@@ -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 5780874..beb8e77 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 980048a..0855be7 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 3e6e7af..e547e13 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 25e1cd5..8424550 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 0000000..7389c6f
--- /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