keycloak-aplcache

Changes

audit/api/pom.xml 39(+39 -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)

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";
+
+}
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);
     }
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