keycloak-uncached

Email audit listener

5/20/2014 10:09:20 AM

Changes

audit/api/src/main/java/org/keycloak/audit/Events.java 29(+0 -29)

audit/email/pom.xml 52(+52 -0)

audit/pom.xml 1(+1 -0)

server/pom.xml 5(+5 -0)

Details

diff --git a/audit/api/src/main/java/org/keycloak/audit/Audit.java b/audit/api/src/main/java/org/keycloak/audit/Audit.java
index 625074f..985fefa 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Audit.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Audit.java
@@ -75,7 +75,7 @@ public class Audit {
         return this;
     }
 
-    public Audit event(String e) {
+    public Audit event(EventType e) {
         event.setEvent(e);
         return this;
     }
@@ -108,6 +108,7 @@ public class Audit {
     }
 
     public void error(String error) {
+        event.setEvent(EventType.valueOf(event.getEvent().name() + "_ERROR"));
         event.setError(error);
         send();
     }
diff --git a/audit/api/src/main/java/org/keycloak/audit/Event.java b/audit/api/src/main/java/org/keycloak/audit/Event.java
index 8141acb..3a72fd1 100644
--- a/audit/api/src/main/java/org/keycloak/audit/Event.java
+++ b/audit/api/src/main/java/org/keycloak/audit/Event.java
@@ -10,7 +10,7 @@ public class Event {
 
     private long time;
 
-    private String event;
+    private EventType event;
 
     private String realmId;
 
@@ -34,11 +34,11 @@ public class Event {
         this.time = time;
     }
 
-    public String getEvent() {
+    public EventType getEvent() {
         return event;
     }
 
-    public void setEvent(String event) {
+    public void setEvent(EventType event) {
         this.event = event;
     }
 
diff --git a/audit/api/src/main/java/org/keycloak/audit/EventQuery.java b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java
index 3e5cf60..56ed79f 100644
--- a/audit/api/src/main/java/org/keycloak/audit/EventQuery.java
+++ b/audit/api/src/main/java/org/keycloak/audit/EventQuery.java
@@ -7,7 +7,7 @@ import java.util.List;
  */
 public interface EventQuery {
 
-    public EventQuery event(String... events);
+    public EventQuery event(EventType... events);
 
     public EventQuery realm(String realmId);
 
diff --git a/audit/api/src/main/java/org/keycloak/audit/EventType.java b/audit/api/src/main/java/org/keycloak/audit/EventType.java
new file mode 100644
index 0000000..707f5fd
--- /dev/null
+++ b/audit/api/src/main/java/org/keycloak/audit/EventType.java
@@ -0,0 +1,42 @@
+package org.keycloak.audit;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public enum EventType {
+
+    LOGIN,
+    LOGIN_ERROR,
+    REGISTER,
+    REGISTER_ERROR,
+    LOGOUT,
+    LOGOUT_ERROR,
+    CODE_TO_TOKEN,
+    CODE_TO_TOKEN_ERROR,
+    REFRESH_TOKEN,
+    REFRESH_TOKEN_ERROR,
+    SOCIAL_LINK,
+    SOCIAL_LINK_ERROR,
+    REMOVE_SOCIAL_LINK,
+    REMOVE_SOCIAL_LINK_ERROR,
+
+    UPDATE_EMAIL,
+    UPDATE_EMAIL_ERROR,
+    UPDATE_PROFILE,
+    UPDATE_PROFILE_ERROR,
+    UPDATE_PASSWORD,
+    UPDATE_PASSWORD_ERROR,
+    UPDATE_TOTP,
+    UPDATE_TOTP_ERROR,
+    VERIFY_EMAIL,
+    VERIFY_EMAIL_ERROR,
+
+    REMOVE_TOTP,
+    REMOVE_TOTP_ERROR,
+
+    SEND_VERIFY_EMAIL,
+    SEND_VERIFY_EMAIL_ERROR,
+    SEND_RESET_PASSWORD,
+    SEND_RESET_PASSWORD_ERROR
+
+}

audit/email/pom.xml 52(+52 -0)

diff --git a/audit/email/pom.xml b/audit/email/pom.xml
new file mode 100755
index 0000000..9aa4dd1
--- /dev/null
+++ b/audit/email/pom.xml
@@ -0,0 +1,52 @@
+<?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-email</artifactId>
+    <name>Keycloak Audit Email Provider</name>
+    <description/>
+
+    <dependencies>
+        <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>org.keycloak</groupId>
+            <artifactId>keycloak-audit-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+   </dependencies>
+
+</project>
diff --git a/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListener.java b/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListener.java
new file mode 100644
index 0000000..0ef510a
--- /dev/null
+++ b/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListener.java
@@ -0,0 +1,57 @@
+package org.keycloak.audit.email;
+
+import org.jboss.logging.Logger;
+import org.keycloak.audit.AuditListener;
+import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class EmailAuditListener implements AuditListener {
+
+    private static final Logger log = Logger.getLogger(EmailAuditListener.class);
+
+    private KeycloakSession keycloakSession;
+    private EmailProvider emailProvider;
+    private Set<EventType> includedEvents;
+
+    public EmailAuditListener(KeycloakSession keycloakSession, EmailProvider emailProvider, Set<EventType> includedEvents) {
+        this.keycloakSession = keycloakSession;
+        this.emailProvider = emailProvider;
+        this.includedEvents = includedEvents;
+    }
+
+    @Override
+    public void onEvent(Event event) {
+        if (includedEvents.contains(event.getEvent())) {
+            if (event.getRealmId() != null && event.getUserId() != null) {
+                RealmModel realm = keycloakSession.getRealm(event.getRealmId());
+                UserModel user = realm.getUserById(event.getUserId());
+                if (user != null && user.getEmail() != null && user.isEmailVerified()) {
+                    try {
+                        emailProvider.setRealm(realm).setUser(user).sendEvent(event);
+                    } catch (EmailException e) {
+                        log.error("Failed to send event mail", e);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListenerFactory.java b/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListenerFactory.java
new file mode 100644
index 0000000..7459a06
--- /dev/null
+++ b/audit/email/src/main/java/org/keycloak/audit/email/EmailAuditListenerFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.audit.email;
+
+import org.keycloak.Config;
+import org.keycloak.audit.AuditListener;
+import org.keycloak.audit.AuditListenerFactory;
+import org.keycloak.audit.EventType;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderSession;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class EmailAuditListenerFactory implements AuditListenerFactory {
+
+    private static final Set<EventType> SUPPORTED_EVENTS = new HashSet<EventType>();
+    static {
+        Collections.addAll(SUPPORTED_EVENTS, EventType.LOGIN_ERROR, EventType.UPDATE_PASSWORD, EventType.REMOVE_TOTP, EventType.UPDATE_TOTP);
+    }
+
+    private Set<EventType> includedEvents = new HashSet<EventType>();
+
+    @Override
+    public AuditListener create(ProviderSession providerSession) {
+        KeycloakSession keycloakSession = providerSession.getProvider(KeycloakSession.class);
+        EmailProvider emailProvider = providerSession.getProvider(EmailProvider.class);
+        return new EmailAuditListener(keycloakSession, emailProvider, includedEvents);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+        String[] include = config.getArray("include-events");
+        if (include != null) {
+            for (String i : include) {
+                includedEvents.add(EventType.valueOf(i.toUpperCase()));
+            }
+        } else {
+            includedEvents.addAll(SUPPORTED_EVENTS);
+        }
+
+        String[] exclude = config.getArray("exclude-events");
+        if (exclude != null) {
+            for (String e : exclude) {
+                includedEvents.remove(EventType.valueOf(e.toUpperCase()));
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public String getId() {
+        return "email";
+    }
+
+}
diff --git a/audit/email/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory b/audit/email/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory
new file mode 100644
index 0000000..bdc9c2e
--- /dev/null
+++ b/audit/email/src/main/resources/META-INF/services/org.keycloak.audit.AuditListenerFactory
@@ -0,0 +1 @@
+org.keycloak.audit.email.EmailAuditListenerFactory
\ No newline at end of file
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
index 0640086..d6366b3 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProvider.java
@@ -6,11 +6,13 @@ import org.jboss.logging.Logger;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.EventQuery;
+import org.keycloak.audit.EventType;
 
 import javax.persistence.EntityManager;
 import javax.persistence.EntityTransaction;
 import java.io.IOException;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 /**
@@ -25,9 +27,11 @@ public class JpaAuditProvider implements AuditProvider {
 
     private EntityManager em;
     private EntityTransaction tx;
+    private Set<EventType> includedEvents;
 
-    public JpaAuditProvider(EntityManager em) {
+    public JpaAuditProvider(EntityManager em, Set<EventType> includedEvents) {
         this.em = em;
+        this.includedEvents = includedEvents;
     }
 
     @Override
@@ -55,8 +59,10 @@ public class JpaAuditProvider implements AuditProvider {
 
     @Override
     public void onEvent(Event event) {
-        beginTx();
-        em.persist(convert(event));
+        if (includedEvents.contains(event.getEvent())) {
+            beginTx();
+            em.persist(convert(event));
+        }
     }
 
     @Override
@@ -79,7 +85,7 @@ public class JpaAuditProvider implements AuditProvider {
         EventEntity e = new EventEntity();
         e.setId(UUID.randomUUID().toString());
         e.setTime(o.getTime());
-        e.setEvent(o.getEvent());
+        e.setEvent(o.getEvent().toString());
         e.setRealmId(o.getRealmId());
         e.setClientId(o.getClientId());
         e.setUserId(o.getUserId());
@@ -97,7 +103,7 @@ public class JpaAuditProvider implements AuditProvider {
     static Event convert(EventEntity o) {
         Event e = new Event();
         e.setTime(o.getTime());
-        e.setEvent(o.getEvent());
+        e.setEvent(EventType.valueOf(o.getEvent()));
         e.setRealmId(o.getRealmId());
         e.setClientId(o.getClientId());
         e.setUserId(o.getUserId());
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java
index 01f72e2..58ff08e 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaAuditProviderFactory.java
@@ -3,10 +3,13 @@ package org.keycloak.audit.jpa;
 import org.keycloak.Config;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.AuditProviderFactory;
+import org.keycloak.audit.EventType;
 import org.keycloak.provider.ProviderSession;
 
 import javax.persistence.EntityManagerFactory;
 import javax.persistence.Persistence;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -16,14 +19,34 @@ public class JpaAuditProviderFactory implements AuditProviderFactory {
     public static final String ID = "jpa";
     private EntityManagerFactory emf;
 
+    private Set<EventType> includedEvents = new HashSet<EventType>();
+
     @Override
     public AuditProvider create(ProviderSession providerSession) {
-        return new JpaAuditProvider(emf.createEntityManager());
+        return new JpaAuditProvider(emf.createEntityManager(), includedEvents);
     }
 
     @Override
     public void init(Config.Scope config) {
         emf = Persistence.createEntityManagerFactory("jpa-keycloak-audit-store");
+
+        String[] include = config.getArray("include-events");
+        if (include != null) {
+            for (String i : include) {
+                includedEvents.add(EventType.valueOf(i.toUpperCase()));
+            }
+        } else {
+            for (EventType i : EventType.values()) {
+                includedEvents.add(i);
+            }
+        }
+
+        String[] exclude = config.getArray("exclude-events");
+        if (exclude != null) {
+            for (String e : exclude) {
+                includedEvents.remove(EventType.valueOf(e.toUpperCase()));
+            }
+        }
     }
 
     @Override
diff --git a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java
index f69fe4b..16d6b52 100644
--- a/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java
+++ b/audit/jpa/src/main/java/org/keycloak/audit/jpa/JpaEventQuery.java
@@ -2,6 +2,7 @@ package org.keycloak.audit.jpa;
 
 import org.keycloak.audit.Event;
 import org.keycloak.audit.EventQuery;
+import org.keycloak.audit.EventType;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -36,8 +37,12 @@ public class JpaEventQuery implements EventQuery {
     }
 
     @Override
-    public EventQuery event(String... events) {
-        predicates.add(root.get("event").in(events));
+    public EventQuery event(EventType... events) {
+        List<String> eventStrings = new LinkedList<String>();
+        for (EventType e : events) {
+            eventStrings.add(e.toString());
+        }
+        predicates.add(root.get("event").in(eventStrings));
         return this;
     }
 
diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
index 3114628..5fcdc41 100644
--- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
+++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProvider.java
@@ -6,9 +6,11 @@ import com.mongodb.DBObject;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.EventQuery;
+import org.keycloak.audit.EventType;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -16,9 +18,11 @@ import java.util.Map;
 public class MongoAuditProvider implements AuditProvider {
 
     private DBCollection audit;
+    private Set<EventType> includedEvents;
 
-    public MongoAuditProvider(DBCollection audit) {
+    public MongoAuditProvider(DBCollection audit, Set<EventType> includedEvents) {
         this.audit = audit;
+        this.includedEvents = includedEvents;
     }
 
     @Override
@@ -46,7 +50,9 @@ public class MongoAuditProvider implements AuditProvider {
 
     @Override
     public void onEvent(Event event) {
-        audit.insert(convert(event));
+        if (includedEvents.contains(event.getEvent())) {
+            audit.insert(convert(event));
+        }
     }
 
     @Override
@@ -56,7 +62,7 @@ public class MongoAuditProvider implements AuditProvider {
     static DBObject convert(Event o) {
         BasicDBObject e = new BasicDBObject();
         e.put("time", o.getTime());
-        e.put("event", o.getEvent());
+        e.put("event", o.getEvent().toString());
         e.put("realmId", o.getRealmId());
         e.put("clientId", o.getClientId());
         e.put("userId", o.getUserId());
@@ -78,7 +84,7 @@ public class MongoAuditProvider implements AuditProvider {
     static Event convert(BasicDBObject o) {
         Event e = new Event();
         e.setTime(o.getLong("time"));
-        e.setEvent(o.getString("event"));
+        e.setEvent(EventType.valueOf(o.getString("event")));
         e.setRealmId(o.getString("realmId"));
         e.setClientId(o.getString("clientId"));
         e.setUserId(o.getString("userId"));
diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java
index 09cb6fd..b81afc5 100644
--- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java
+++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoAuditProviderFactory.java
@@ -8,10 +8,13 @@ import com.mongodb.WriteConcern;
 import org.keycloak.Config;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.AuditProviderFactory;
+import org.keycloak.audit.EventType;
 import org.keycloak.provider.ProviderSession;
 
 import java.net.UnknownHostException;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -22,9 +25,11 @@ public class MongoAuditProviderFactory implements AuditProviderFactory {
     private MongoClient client;
     private DB db;
 
+    private Set<EventType> includedEvents = new HashSet<EventType>();
+
     @Override
     public AuditProvider create(ProviderSession providerSession) {
-        return new MongoAuditProvider(db.getCollection("audit"));
+        return new MongoAuditProvider(db.getCollection("audit"), includedEvents);
     }
 
     @Override
@@ -53,6 +58,24 @@ public class MongoAuditProviderFactory implements AuditProviderFactory {
         } catch (UnknownHostException e) {
             throw new RuntimeException(e);
         }
+
+        String[] include = config.getArray("include-events");
+        if (include != null) {
+            for (String i : include) {
+                includedEvents.add(EventType.valueOf(i.toUpperCase()));
+            }
+        } else {
+            for (EventType i : EventType.values()) {
+                includedEvents.add(i);
+            }
+        }
+
+        String[] exclude = config.getArray("exclude-events");
+        if (exclude != null) {
+            for (String e : exclude) {
+                includedEvents.remove(EventType.valueOf(e.toUpperCase()));
+            }
+        }
     }
 
     @Override
diff --git a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java
index d220f0e..668c907 100644
--- a/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java
+++ b/audit/mongo/src/main/java/org/keycloak/audit/mongo/MongoEventQuery.java
@@ -5,9 +5,11 @@ import com.mongodb.DBCollection;
 import com.mongodb.DBCursor;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.EventQuery;
+import org.keycloak.audit.EventType;
 
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -25,8 +27,12 @@ public class MongoEventQuery implements EventQuery {
     }
 
     @Override
-    public EventQuery event(String... events) {
-        query.put("event", new BasicDBObject("$in", events));
+    public EventQuery event(EventType... events) {
+        List<String> eventStrings = new LinkedList<String>();
+        for (EventType e : events) {
+            eventStrings.add(e.toString());
+        }
+        query.put("event", new BasicDBObject("$in", eventStrings));
         return this;
     }
 

audit/pom.xml 1(+1 -0)

diff --git a/audit/pom.xml b/audit/pom.xml
index ed125d1..21e5d29 100755
--- a/audit/pom.xml
+++ b/audit/pom.xml
@@ -17,6 +17,7 @@
 
     <modules>
         <module>api</module>
+        <module>email</module>
         <module>jpa</module>
         <module>jboss-logging</module>
         <module>mongo</module>
diff --git a/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java
index b24ee37..82ecc28 100644
--- a/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java
+++ b/audit/tests/src/main/java/org/keycloak/audit/tests/AbstractAuditProviderTest.java
@@ -8,6 +8,7 @@ import org.keycloak.Config;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.AuditProviderFactory;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.provider.ProviderFactory;
 
 import java.util.HashMap;
@@ -47,7 +48,7 @@ public abstract class AbstractAuditProviderTest {
 
     @Test
     public void save() {
-        provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
     }
 
     @Test
@@ -55,23 +56,23 @@ public abstract class AbstractAuditProviderTest {
         long oldest = System.currentTimeMillis() - 30000;
         long newest = System.currentTimeMillis() + 30000;
 
-        provider.onEvent(create("event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(newest, "event2", "realmId", "clientId", "userId2", "127.0.0.1", "error"));
-        provider.onEvent(create("event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(oldest, "event", "realmId", "clientId2", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create("event", "realmId", "clientId", "userId2", "127.0.0.1", "error"));
+        provider.onEvent(create(EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(newest, EventType.REGISTER, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(newest, EventType.REGISTER, "realmId", "clientId", "userId2", "127.0.0.1", "error"));
+        provider.onEvent(create(EventType.LOGIN, "realmId2", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(oldest, EventType.LOGIN, "realmId", "clientId2", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(EventType.LOGIN, "realmId", "clientId", "userId2", "127.0.0.1", "error"));
 
         provider.close();
         provider = factory.create(null);
 
         Assert.assertEquals(5, provider.createQuery().client("clientId").getResultList().size());
         Assert.assertEquals(5, provider.createQuery().realm("realmId").getResultList().size());
-        Assert.assertEquals(4, provider.createQuery().event("event").getResultList().size());
-        Assert.assertEquals(6, provider.createQuery().event("event", "event2").getResultList().size());
+        Assert.assertEquals(4, provider.createQuery().event(EventType.LOGIN).getResultList().size());
+        Assert.assertEquals(6, provider.createQuery().event(EventType.LOGIN, EventType.REGISTER).getResultList().size());
         Assert.assertEquals(4, provider.createQuery().user("userId").getResultList().size());
 
-        Assert.assertEquals(1, provider.createQuery().user("userId").event("event2").getResultList().size());
+        Assert.assertEquals(1, provider.createQuery().user("userId").event(EventType.REGISTER).getResultList().size());
 
         Assert.assertEquals(2, provider.createQuery().maxResults(2).getResultList().size());
         Assert.assertEquals(1, provider.createQuery().firstResult(5).getResultList().size());
@@ -82,11 +83,11 @@ public abstract class AbstractAuditProviderTest {
 
     @Test
     public void clear() {
-        provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 20000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis(), EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis(), EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId2", "clientId", "userId", "127.0.0.1", "error"));
 
         provider.close();
         provider = factory.create(null);
@@ -98,11 +99,11 @@ public abstract class AbstractAuditProviderTest {
 
     @Test
     public void clearOld() {
-        provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis() - 20000, "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis(), "event", "realmId", "clientId", "userId", "127.0.0.1", "error"));
-        provider.onEvent(create(System.currentTimeMillis() - 30000, "event", "realmId2", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 20000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis(), EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis(), EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
+        provider.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId2", "clientId", "userId", "127.0.0.1", "error"));
 
         provider.close();
         provider = factory.create(null);
@@ -112,11 +113,11 @@ public abstract class AbstractAuditProviderTest {
         Assert.assertEquals(3, provider.createQuery().getResultList().size());
     }
 
-    private Event create(String event, String realmId, String clientId, String userId, String ipAddress, String error) {
+    private Event create(EventType event, String realmId, String clientId, String userId, String ipAddress, String error) {
         return create(System.currentTimeMillis(), event, realmId, clientId, userId, ipAddress, error);
     }
 
-    private Event create(long time, String event, String realmId, String clientId, String userId, String ipAddress, String error) {
+    private Event create(long time, EventType event, String realmId, String clientId, String userId, String ipAddress, String error) {
         Event e = new Event();
         e.setTime(time);
         e.setEvent(event);
diff --git a/core/src/main/java/org/keycloak/Config.java b/core/src/main/java/org/keycloak/Config.java
index 9af395b..46c6fc9 100755
--- a/core/src/main/java/org/keycloak/Config.java
+++ b/core/src/main/java/org/keycloak/Config.java
@@ -70,6 +70,20 @@ public class Config {
         }
 
         @Override
+        public String[] getArray(String key) {
+            String value = get(key);
+            if (value != null) {
+                String[] a = value.split(",");
+                for (int i = 0; i < a.length; i++) {
+                    a[i] = a[i].trim();
+                }
+                return a;
+            } else {
+                return null;
+            }
+        }
+
+        @Override
         public Integer getInt(String key) {
             return getInt(key, null);
         }
@@ -113,6 +127,8 @@ public class Config {
 
         String get(String key, String defaultValue);
 
+        String[] getArray(String key);
+
         Integer getInt(String key);
 
         Integer getInt(String key, Integer defaultValue);
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java
index 06a07e7..31c13a3 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/LogBean.java
@@ -38,7 +38,7 @@ public class LogBean {
         }
 
         public String getEvent() {
-            return event.getEvent();
+            return event.getEvent().toString().toLowerCase().replace("_", " ");
         }
 
         public String getClient() {
@@ -51,8 +51,10 @@ public class LogBean {
 
         public List<DetailBean> getDetails() {
             List<DetailBean> details = new LinkedList<DetailBean>();
-            for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
-                details.add(new DetailBean(e));
+            if (event.getDetails() != null) {
+                for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
+                    details.add(new DetailBean(e));
+                }
             }
             return details;
         }
@@ -72,7 +74,7 @@ public class LogBean {
         }
 
         public String getValue() {
-            return entry.getValue();
+            return entry.getValue().replace("_", " ");
         }
 
     }
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-audit.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-audit.html
index 0e03ff0..28d60f4 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-audit.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-audit.html
@@ -73,12 +73,13 @@
             <tbody>
                 <tr ng-repeat="event in events">
                     <td>{{event.time|date:'shortDate'}}<br>{{event.time|date:'mediumTime'}}</td>
-                    <td data-ng-class="event.error && 'audit-error' || 'audit-success'">{{event.event}}<br/>{{event.error}}</td>
+                    <td data-ng-class="event.error && 'audit-error' || 'audit-success'">{{event.event}}</td>
                     <td>
-                        <table>
+                        <table class="table table-striped table-bordered">
                             <tr><td width="100px">Client</td><td>{{event.clientId}}</td></tr>
                             <tr><td>User</td><td>{{event.userId}}</td></tr>
                             <tr><td>IP Address</td><td>{{event.ipAddress}}</td></tr>
+                            <tr data-ng-show="event.error"><td>Error</td><td>{{event.error}}</td></tr>
                             <tr>
                                 <td>Details</td>
                                 <td>
@@ -86,7 +87,7 @@
                                         <span class="glyphicon glyphicon-plus" data-ng-show="!event.collapse"></span>
                                         <span class="glyphicon glyphicon-minus" data-ng-show="event.collapse"></span>
                                     </button>
-                                    <table data-ng-show="event.collapse">
+                                    <table data-ng-show="event.collapse" class="table table-striped table-bordered">
                                         <tr ng-repeat="(key, value) in event.details">
                                             <td>{{key}}</td>
                                             <td>{{value}}</td>
diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css
index df7e5e7..d567eed 100644
--- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css
+++ b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/admin-console.css
@@ -828,4 +828,8 @@ legend .kc-icon-collapse {
 
 .action-div > i {
     cursor: pointer;
+}
+
+table table {
+    margin-bottom: 0 !important;
 }
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/tables.css b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/tables.css
index 4ef3c53..b0a1f83 100644
--- a/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/tables.css
+++ b/forms/common-themes/src/main/resources/theme/admin/keycloak/resources/css/tables.css
@@ -1,3 +1,78 @@
+
+table tfoot tr .table-nav {
+    float: right;
+}
+table tfoot tr .table-nav a,
+table tfoot tr .table-nav button {
+    display: inline-block;
+    line-height: 22px;
+    border-left: 1px solid #d9d9d9;
+    border-right: none;
+    border-top: none;
+    border-bottom: none;
+    width: 3.5em;
+    background-color: #f3f3f3;
+    background-image: linear-gradient(top, #fafafa 0%, #ededed 100%);
+    background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%);
+    background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%);
+    background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%);
+    background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%);
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed));
+    text-indent: -99999em;
+    background-image: url(img/sprite-table-nav.png);
+    background-repeat: no-repeat;
+    background-position: left top;
+    vertical-align: top;
+}
+table tfoot tr .table-nav a.last,
+table tfoot tr .table-nav button.last {
+    background-position: top right;
+}
+table tfoot tr .table-nav a.prev,
+table tfoot tr .table-nav button.prev {
+    background-position: bottom left;
+}
+table tfoot tr .table-nav a.next,
+table tfoot tr .table-nav button.next {
+    background-position: bottom right;
+}
+table tfoot tr .table-nav a:hover,
+table tfoot tr .table-nav button:hover {
+    background-image: url(img/sprite-table-nav.png);
+    background-color: #eeeeee;
+}
+table tfoot tr .table-nav a:active,
+table tfoot tr .table-nav button:active {
+    box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.25) inset;
+}
+table tfoot tr .table-nav a.disabled,
+table tfoot tr .table-nav button:disabled {
+    opacity: 0.5;
+    filter: alpha(opacity=50);
+    cursor: default;
+}
+table tfoot tr .table-nav a.disabled:active,
+table tfoot tr .table-nav button:disabled:active {
+    box-shadow: none;
+}
+table tfoot tr .table-nav span {
+    font-size: 1.1em;
+    border-left: 1px solid #d9d9d9;
+    line-height: 2.18181818181818em;
+    display: inline-block;
+    padding: 0 1.36363636363636em;
+}
+
+
+td.audit-success {
+    background-color: #E4F1E1 !important;
+}
+
+td.audit-error {
+    background-color: #F8E7E7 !important;
+}
+
+
 /*
 table {
   width: 100%;
@@ -111,69 +186,6 @@ table tbody.selectable-rows tr.selected:hover td:first-child {
 table tfoot tr {
   border-top: 1px solid #cecece;
 }
-table tfoot tr .table-nav {
-  float: right;
-}
-table tfoot tr .table-nav a,
-table tfoot tr .table-nav button {
-  display: inline-block;
-  line-height: 22px;
-  border-left: 1px solid #d9d9d9;
-  border-right: none;
-  border-top: none;
-  border-bottom: none;
-  width: 3.5em;
-  background-color: #f3f3f3;
-  background-image: linear-gradient(top, #fafafa 0%, #ededed 100%);
-  background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%);
-  background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%);
-  background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%);
-  background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%);
-  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed));
-  text-indent: -99999em;
-  background-image: url(img/sprite-table-nav.png);
-  background-repeat: no-repeat;
-  background-position: left top;
-  vertical-align: top;
-}
-table tfoot tr .table-nav a.last,
-table tfoot tr .table-nav button.last {
-  background-position: top right;
-}
-table tfoot tr .table-nav a.prev,
-table tfoot tr .table-nav button.prev {
-  background-position: bottom left;
-}
-table tfoot tr .table-nav a.next,
-table tfoot tr .table-nav button.next {
-  background-position: bottom right;
-}
-table tfoot tr .table-nav a:hover,
-table tfoot tr .table-nav button:hover {
-  background-image: url(img/sprite-table-nav.png);
-  background-color: #eeeeee;
-}
-table tfoot tr .table-nav a:active,
-table tfoot tr .table-nav button:active {
-  box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.25) inset;
-}
-table tfoot tr .table-nav a.disabled,
-table tfoot tr .table-nav button:disabled {
-  opacity: 0.5;
-  filter: alpha(opacity=50);
-  cursor: default;
-}
-table tfoot tr .table-nav a.disabled:active,
-table tfoot tr .table-nav button:disabled:active {
-  box-shadow: none;
-}
-table tfoot tr .table-nav span {
-  font-size: 1.1em;
-  border-left: 1px solid #d9d9d9;
-  line-height: 2.18181818181818em;
-  display: inline-block;
-  padding: 0 1.36363636363636em;
-}
 
 .kc-table-actions > * {
     vertical-align: middle;
@@ -183,14 +195,6 @@ td .form-group {
     margin-bottom: 0;
 }
 
-td.audit-success {
-    background-color: #E4F1E1;
-}
-
-td.audit-error {
-    background-color: #F8E7E7;
-}
-
 .kc-table-actions .form-group {
     margin-top: 5px;
     margin-bottom: 5px;
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl
new file mode 100644
index 0000000..93e02be
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl
@@ -0,0 +1 @@
+Your password was changed on ${event.date} from ${event.ipAddress}. If this was not you, please contact an admin.
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl
new file mode 100644
index 0000000..0c6a142
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl
@@ -0,0 +1 @@
+TOTP was removed from your account on ${event.date} from ${event.ipAddress}. If this was not you, please contact an admin.
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl
new file mode 100644
index 0000000..8c3a94f
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl
@@ -0,0 +1 @@
+A failed login attempt was dettected to your account on ${event.date} from ${event.ipAddress}. If this was not you, please contact an admin.
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl
new file mode 100644
index 0000000..a1f153c
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl
@@ -0,0 +1 @@
+TOTP was updated for your account on ${event.date} from ${event.ipAddress}. If this was not you, please contact an admin.
\ No newline at end of file
diff --git a/forms/email-api/pom.xml b/forms/email-api/pom.xml
index 19ef08a..62df27f 100755
--- a/forms/email-api/pom.xml
+++ b/forms/email-api/pom.xml
@@ -26,6 +26,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
 	</dependencies>
 
 	<build>
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
index 0827b96..eb8f3e7 100644
--- a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.email;
 
+import org.keycloak.audit.Event;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.provider.Provider;
@@ -13,6 +14,8 @@ public interface EmailProvider extends Provider {
 
     public EmailProvider setUser(UserModel user);
 
+    public void sendEvent(Event event) throws EmailException;
+
     public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
 
     public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml
index b5fb4de..f83f098 100755
--- a/forms/email-freemarker/pom.xml
+++ b/forms/email-freemarker/pom.xml
@@ -34,6 +34,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-forms-common-freemarker</artifactId>
             <version>${project.version}</version>
             <scope>provided</scope>
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java
new file mode 100644
index 0000000..efee48d
--- /dev/null
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/EventBean.java
@@ -0,0 +1,61 @@
+package org.keycloak.email.freemarker.beans;
+
+import org.keycloak.audit.Event;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class EventBean {
+    private Event event;
+
+    public EventBean(Event event) {
+        this.event = event;
+    }
+
+    public Date getDate() {
+        return new Date(event.getTime());
+    }
+
+    public String getEvent() {
+        return event.getEvent().toString().toLowerCase().replace("_", " ");
+    }
+
+    public String getClient() {
+        return event.getClientId();
+    }
+
+    public String getIpAddress() {
+        return event.getIpAddress();
+    }
+
+    public List<DetailBean> getDetails() {
+        List<DetailBean> details = new LinkedList<DetailBean>();
+        for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
+            details.add(new DetailBean(e));
+        }
+        return details;
+    }
+
+    public static class DetailBean {
+
+        private Map.Entry<String, String> entry;
+
+        public DetailBean(Map.Entry<String, String> entry) {
+            this.entry = entry;
+        }
+
+        public String getKey() {
+            return entry.getKey();
+        }
+
+        public String getValue() {
+            return entry.getValue().replace("_", " ");
+        }
+
+    }
+}
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
index e4e8c52..0ec1fcb 100644
--- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
@@ -1,8 +1,10 @@
 package org.keycloak.email.freemarker;
 
 import org.jboss.logging.Logger;
+import org.keycloak.audit.Event;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailProvider;
+import org.keycloak.email.freemarker.beans.EventBean;
 import org.keycloak.freemarker.ExtendingThemeManager;
 import org.keycloak.freemarker.FreeMarkerUtil;
 import org.keycloak.freemarker.Theme;
@@ -48,6 +50,14 @@ public class FreeMarkerEmailProvider implements EmailProvider {
     }
 
     @Override
+    public void sendEvent(Event event) throws EmailException {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+        attributes.put("event", new EventBean(event));
+
+        send("passwordResetSubject", "event-" + event.getEvent().toString().toLowerCase() + ".ftl", attributes);
+    }
+
+    @Override
     public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
         Map<String, Object> attributes = new HashMap<String, Object>();
         attributes.put("link", link);
diff --git a/project-integrations/aerogear-ups/auth-server/src/main/webapp/WEB-INF/keycloak-server.json b/project-integrations/aerogear-ups/auth-server/src/main/webapp/WEB-INF/keycloak-server.json
index 73fc4b0..3e88c8a 100644
--- a/project-integrations/aerogear-ups/auth-server/src/main/webapp/WEB-INF/keycloak-server.json
+++ b/project-integrations/aerogear-ups/auth-server/src/main/webapp/WEB-INF/keycloak-server.json
@@ -3,10 +3,6 @@
         "realm": "keycloak-admin"
     },
 
-    "audit": {
-        "provider": "jpa"
-    },
-
     "model": {
         "provider": "jpa"
     },

server/pom.xml 5(+5 -0)

diff --git a/server/pom.xml b/server/pom.xml
index e353dd5..a557067 100755
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -62,6 +62,11 @@
             <artifactId>keycloak-audit-jboss-logging</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-email</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <!-- social -->
         <dependency>
             <groupId>org.keycloak</groupId>
diff --git a/server/src/main/resources/META-INF/keycloak-server.json b/server/src/main/resources/META-INF/keycloak-server.json
index 544ad81..aa44676 100644
--- a/server/src/main/resources/META-INF/keycloak-server.json
+++ b/server/src/main/resources/META-INF/keycloak-server.json
@@ -4,7 +4,10 @@
     },
 
     "audit": {
-        "provider": "jpa"
+        "provider": "jpa",
+        "jpa": {
+            "exclude-events": [ "REFRESH_TOKEN" ]
+        }
     },
 
     "model": {
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 5f6206f..5ff6ca3 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -31,7 +31,7 @@ import org.keycloak.audit.Audit;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
-import org.keycloak.audit.Events;
+import org.keycloak.audit.EventType;
 import org.keycloak.authentication.AuthProviderStatus;
 import org.keycloak.authentication.AuthenticationProviderException;
 import org.keycloak.authentication.AuthenticationProviderManager;
@@ -95,8 +95,8 @@ public class AccountService {
 
     private static final Logger logger = Logger.getLogger(AccountService.class);
 
-    private static final String[] AUDIT_EVENTS = {Events.LOGIN, Events.LOGOUT, Events.REGISTER, Events.REMOVE_SOCIAL_LINK, Events.REMOVE_TOTP, Events.SEND_RESET_PASSWORD,
-            Events.SEND_VERIFY_EMAIL, Events.SOCIAL_LINK, Events.UPDATE_EMAIL, Events.UPDATE_PASSWORD, Events.UPDATE_PROFILE, Events.UPDATE_TOTP, Events.VERIFY_EMAIL};
+    private static final EventType[] AUDIT_EVENTS = {EventType.LOGIN, EventType.LOGOUT, EventType.REGISTER, EventType.REMOVE_SOCIAL_LINK, EventType.REMOVE_TOTP, EventType.SEND_RESET_PASSWORD,
+            EventType.SEND_VERIFY_EMAIL, EventType.SOCIAL_LINK, EventType.UPDATE_EMAIL, EventType.UPDATE_PASSWORD, EventType.UPDATE_PROFILE, EventType.UPDATE_TOTP, EventType.VERIFY_EMAIL};
 
     private static final Set<String> AUDIT_DETAILS = new HashSet<String>();
     static {
@@ -246,20 +246,6 @@ public class AccountService {
     public Response logPage() {
         if (auth != null) {
             List<Event> events = auditProvider.createQuery().event(AUDIT_EVENTS).user(auth.getUser().getId()).maxResults(30).getResultList();
-            for (Event e : events) {
-                e.setEvent(e.getEvent().replace('_', ' '));
-
-                Map<String, String> details = new HashMap<String, String>();
-                if (e.getDetails() != null) {
-                    Iterator<String> itr = e.getDetails().keySet().iterator();
-                    for (Map.Entry<String, String> d : e.getDetails().entrySet()) {
-                        if (AUDIT_DETAILS.contains(d.getKey())) {
-                            details.put(d.getKey().replace('_', ' '), d.getValue());
-                        }
-                    }
-                }
-                e.setDetails(details);
-            }
             account.setEvents(events);
         }
         return forwardToPage("log", AccountPages.LOG);
@@ -300,11 +286,11 @@ public class AccountService {
 
         user.setEmail(formData.getFirst("email"));
 
-        audit.event(Events.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).success();
+        audit.event(EventType.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();
+            audit.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
         }
 
         return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT);
@@ -322,7 +308,7 @@ public class AccountService {
         UserModel user = auth.getUser();
         user.setTotp(false);
 
-        audit.event(Events.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
+        audit.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
 
         return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
     }
@@ -371,7 +357,7 @@ public class AccountService {
 
         user.setTotp(true);
 
-        audit.event(Events.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
+        audit.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success();
 
         return account.setSuccess("successTotp").createResponse(AccountPages.TOTP);
     }
@@ -414,7 +400,7 @@ public class AccountService {
             return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD);
         }
 
-        audit.event(Events.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success();
+        audit.event(EventType.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success();
 
         return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD);
     }
@@ -471,7 +457,7 @@ public class AccountService {
 
                         logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName());
 
-                        audit.event(Events.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser())
+                        audit.event(EventType.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser())
                                 .detail(Details.USERNAME, link.getSocialUserId() + "@" + link.getSocialProvider())
                                 .success();
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 014fe52..1107a3d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -7,6 +7,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.audit.AuditProvider;
 import org.keycloak.audit.Event;
 import org.keycloak.audit.EventQuery;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
@@ -236,7 +237,7 @@ public class RealmAdminResource {
             query.client(client);
         }
         if (event != null) {
-            query.event(event);
+            query.event(EventType.valueOf(event));
         }
         if (user != null) {
             query.user(user);
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 2ed76b0..7bf80fa 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
@@ -26,7 +26,7 @@ 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.audit.EventType;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
@@ -140,7 +140,7 @@ public class OAuthFlows {
 
             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();
+                audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
             }
 
             return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
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 4c1e062..335b10b 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -27,7 +27,7 @@ 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.audit.EventType;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailProvider;
 import org.keycloak.login.LoginFormsProvider;
@@ -136,10 +136,10 @@ public class RequiredActionsService {
         user.removeRequiredAction(RequiredAction.UPDATE_PROFILE);
         accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PROFILE);
 
-        audit.clone().event(Events.UPDATE_PROFILE).success();
+        audit.clone().event(EventType.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();
+            audit.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success();
         }
 
         return redirectOauth(user, accessCode);
@@ -178,7 +178,7 @@ public class RequiredActionsService {
         user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP);
         accessCode.getRequiredActions().remove(RequiredAction.CONFIGURE_TOTP);
 
-        audit.clone().event(Events.UPDATE_TOTP).success();
+        audit.clone().event(EventType.UPDATE_TOTP).success();
 
         return redirectOauth(user, accessCode);
     }
@@ -225,7 +225,7 @@ public class RequiredActionsService {
             accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
         }
 
-        audit.clone().event(Events.UPDATE_PASSWORD).success();
+        audit.clone().event(EventType.UPDATE_PASSWORD).success();
 
         return redirectOauth(user, accessCode);
     }
@@ -250,7 +250,7 @@ public class RequiredActionsService {
             user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
             accessCode.getRequiredActions().remove(RequiredAction.VERIFY_EMAIL);
 
-            audit.clone().event(Events.VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
+            audit.clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
 
             return redirectOauth(user, accessCode);
         } else {
@@ -260,7 +260,7 @@ public class RequiredActionsService {
             }
 
             initAudit(accessCode);
-            //audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
+            //audit.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success();
 
             return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser())
                     .createResponse(RequiredAction.VERIFY_EMAIL);
@@ -307,7 +307,7 @@ public class RequiredActionsService {
                     "Login requester not enabled.");
         }
 
-        audit.event(Events.SEND_RESET_PASSWORD).client(clientId)
+        audit.event(EventType.SEND_RESET_PASSWORD).client(clientId)
                 .detail(Details.REDIRECT_URI, redirect)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.AUTH_METHOD, "form")
@@ -430,7 +430,7 @@ public class RequiredActionsService {
     }
 
     private void initAudit(AccessCodeEntry accessCode) {
-        audit.event(Events.LOGIN).client(accessCode.getClient())
+        audit.event(EventType.LOGIN).client(accessCode.getClient())
                 .user(accessCode.getUser())
                 .session(accessCode.getSessionState())
                 .detail(Details.CODE_ID, accessCode.getId())
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 76c8985..c7c5d23 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -28,7 +28,7 @@ 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.audit.EventType;
 import org.keycloak.models.AccountRoles;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
@@ -65,7 +65,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.net.URISyntaxException;
@@ -131,7 +130,7 @@ public class SocialResource {
         RealmModel realm = realmManager.getRealmByName(realmName);
 
         Audit audit = new AuditManager(realm, providers, clientConnection).createAudit()
-                .event(Events.LOGIN)
+                .event(EventType.LOGIN)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.AUTH_METHOD, "social@" + provider.getId());
 
@@ -196,7 +195,7 @@ public class SocialResource {
         if (userId != null) {
             UserModel authenticatedUser = realm.getUserById(userId);
 
-            audit.event(Events.SOCIAL_LINK).user(userId);
+            audit.event(EventType.SOCIAL_LINK).user(userId);
 
             if (user != null) {
                 audit.error(Errors.SOCIAL_ID_IN_USE);
@@ -244,7 +243,7 @@ public class SocialResource {
 
             realm.addSocialLink(user, socialLink);
 
-            audit.clone().user(user).event(Events.REGISTER)
+            audit.clone().user(user).event(EventType.REGISTER)
                     .detail(Details.REGISTER_METHOD, "social@" + provider.getId())
                     .detail(Details.EMAIL, socialUser.getEmail())
                     .removeDetail("auth_method")
@@ -274,7 +273,7 @@ public class SocialResource {
         RealmModel realm = realmManager.getRealmByName(realmName);
 
         Audit audit = new AuditManager(realm, providers, clientConnection).createAudit()
-                .event(Events.LOGIN).client(clientId)
+                .event(EventType.LOGIN).client(clientId)
                 .detail(Details.REDIRECT_URI, redirectUri)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.AUTH_METHOD, "social@" + providerId);
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 5c337b7..53083ba 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -13,7 +13,7 @@ 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.audit.EventType;
 import org.keycloak.authentication.AuthenticationProviderException;
 import org.keycloak.authentication.AuthenticationProviderManager;
 import org.keycloak.jose.jws.JWSInput;
@@ -34,7 +34,6 @@ import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.ClientConnection;
-import org.keycloak.services.ForbiddenException;
 import org.keycloak.services.managers.AccessCodeEntry;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus;
@@ -212,7 +211,7 @@ public class TokenService {
             return createError("not_enabled", "Resource Owner Password Credentials Grant not enabled", Response.Status.FORBIDDEN);
         }
 
-        audit.event(Events.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
+        audit.event(EventType.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
 
         String username = form.getFirst(AuthenticationManager.FORM_USERNAME);
         if (username == null) {
@@ -294,7 +293,7 @@ public class TokenService {
             throw new NotAcceptableException("HTTPS required");
         }
 
-        audit.event(Events.REFRESH_TOKEN);
+        audit.event(EventType.REFRESH_TOKEN);
 
         ClientModel client = authorizeClient(authorizationHeader, form, audit);
         String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
@@ -334,7 +333,7 @@ public class TokenService {
         boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
         logger.debug("*** Remember me: " + remember);
 
-        audit.event(Events.LOGIN).client(clientId)
+        audit.event(EventType.LOGIN).client(clientId)
                 .detail(Details.REDIRECT_URI, redirect)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.AUTH_METHOD, "form")
@@ -383,12 +382,14 @@ public class TokenService {
             authManager.expireRememberMeCookie(realm, uriInfo);
         }
 
+        UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
+        if (user != null) {
+            audit.user(user);
+        }
+
         switch (status) {
             case SUCCESS:
             case ACTIONS_REQUIRED:
-                UserModel user = KeycloakModelUtils.findUserByNameOrEmail(realm, username);
-                audit.user(user);
-
                 UserSessionModel session = realm.createUserSession(user, clientConnection.getRemoteAddr());
 		        audit.session(session);
 
@@ -429,7 +430,7 @@ public class TokenService {
         String username = formData.getFirst("username");
         String email = formData.getFirst("email");
 
-        audit.event(Events.REGISTER).client(clientId)
+        audit.event(EventType.REGISTER).client(clientId)
                 .detail(Details.REDIRECT_URI, redirect)
                 .detail(Details.RESPONSE_TYPE, "code")
                 .detail(Details.USERNAME, username)
@@ -545,7 +546,7 @@ public class TokenService {
             throw new NotAcceptableException("HTTPS required");
         }
 
-        audit.event(Events.CODE_TO_TOKEN);
+        audit.event(EventType.CODE_TO_TOKEN);
 
         if (!realm.isEnabled()) {
             audit.error(Errors.REALM_DISABLED);
@@ -724,7 +725,7 @@ public class TokenService {
                               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");
+        audit.event(EventType.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
 
         OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
@@ -784,7 +785,7 @@ public class TokenService {
                                  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");
+        audit.event(EventType.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code");
 
         OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
@@ -833,7 +834,7 @@ public class TokenService {
     public Response logout(final @QueryParam("session_state") String sessionState, final @QueryParam("redirect_uri") String redirectUri) {
         // todo do we care if anybody can trigger this?
 
-        audit.event(Events.LOGOUT);
+        audit.event(EventType.LOGOUT);
         if (redirectUri != null) {
             audit.detail(Details.REDIRECT_URI, redirectUri);
         }
@@ -874,7 +875,7 @@ 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");
+        audit.event(EventType.LOGIN).detail(Details.RESPONSE_TYPE, "code");
 
         OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager);
 
diff --git a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java
index 181de11..714e4db 100644
--- a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java
+++ b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java
@@ -4,6 +4,8 @@ import org.codehaus.jackson.JsonNode;
 import org.keycloak.Config;
 import org.keycloak.util.StringPropertyReplacer;
 
+import java.util.ArrayList;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -63,6 +65,26 @@ public class JsonConfigProvider implements Config.ConfigProvider {
         }
 
         @Override
+        public String[] getArray(String key) {
+            if (config == null) {
+                return null;
+            }
+
+            JsonNode n = config.get(key);
+            if (n == null) {
+                return null;
+            } else if (n.isArray()) {
+                ArrayList<String> l = new ArrayList<String>();
+                for (JsonNode e : n) {
+                    l.add(StringPropertyReplacer.replaceProperties(e.getTextValue()));
+                }
+                return (String[]) l.toArray();
+            } else {
+               return new String[] { StringPropertyReplacer.replaceProperties(n.getTextValue()) };
+            }
+        }
+
+        @Override
         public Integer getInt(String key) {
             return getInt(key, null);
         }
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 7924ae9..74bac64 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -102,6 +102,11 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-email</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-core</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 393c3f7..7bec7a7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -29,6 +29,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
@@ -215,7 +216,7 @@ public class AccountTest {
 
         Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
 
-        events.expectAccount("update_password").assertEvent();
+        events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
 
         changePasswordPage.logout();
 
@@ -226,7 +227,7 @@ public class AccountTest {
 
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent();
 
         loginPage.open();
         loginPage.login("test-user@localhost", "new-password");
@@ -260,7 +261,7 @@ public class AccountTest {
 
             Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
 
-            events.expectAccount("update_password").assertEvent();
+            events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent();
         } finally {
             keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
                 @Override
@@ -317,8 +318,8 @@ public class AccountTest {
         Assert.assertEquals("New last", profilePage.getLastName());
         Assert.assertEquals("new@email.com", profilePage.getEmail());
 
-        events.expectAccount("update_profile").assertEvent();
-        events.expectAccount("update_email").detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        events.expectAccount(EventType.UPDATE_PROFILE).assertEvent();
+        events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
     }
 
     @Test
@@ -341,13 +342,13 @@ public class AccountTest {
 
         Assert.assertEquals("Google authenticator configured.", profilePage.getSuccess());
 
-        events.expectAccount("update_totp").assertEvent();
+        events.expectAccount(EventType.UPDATE_TOTP).assertEvent();
 
         Assert.assertTrue(driver.getPageSource().contains("pficon-delete"));
 
         totpPage.removeTotp();
 
-        events.expectAccount("remove_totp").assertEvent();
+        events.expectAccount(EventType.REMOVE_TOTP).assertEvent();
     }
 
     @Test
@@ -405,7 +406,7 @@ public class AccountTest {
             Iterator<List<String>> itr = logPage.getEvents().iterator();
             for (Event event : e) {
                 List<String> a = itr.next();
-                Assert.assertEquals(event.getEvent().replace('_', ' '), a.get(1));
+                Assert.assertEquals(event.getEvent().toString().replace('_', ' ').toLowerCase(), a.get(1));
                 Assert.assertEquals(event.getIpAddress(), a.get(2));
                 Assert.assertEquals(event.getClientId(), a.get(3));
             }
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 9746666..73f185f 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
@@ -28,6 +28,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.managers.RealmManager;
@@ -116,7 +117,7 @@ public class RequiredActionEmailVerificationTest {
         String body = (String) message.getContent();
         String verificationUrl = MailUtil.getLink(body);
 
-        Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+        Event sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent();
         String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -125,7 +126,7 @@ public class RequiredActionEmailVerificationTest {
 
         driver.navigate().to(verificationUrl.trim());
 
-        events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
@@ -148,7 +149,7 @@ public class RequiredActionEmailVerificationTest {
 
         String body = (String) message.getContent();
 
-        Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
+        Event sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
         String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -159,7 +160,7 @@ public class RequiredActionEmailVerificationTest {
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectRequiredAction("verify_email").user(userId).session(sessionId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction(EventType.VERIFY_EMAIL).user(userId).session(sessionId).detail("username", "verifyEmail").detail("email", "email").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
         events.expectLogin().user(userId).session(sessionId).detail("username", "verifyEmail").detail(Details.CODE_ID, mailCodeId).assertEvent();
     }
@@ -173,7 +174,7 @@ public class RequiredActionEmailVerificationTest {
 
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
-        Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
+        Event sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent();
         String sessionId = sendEvent.getSessionId();
 
         String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
@@ -186,7 +187,7 @@ public class RequiredActionEmailVerificationTest {
 
         String body = (String) message.getContent();
 
-        events.expectRequiredAction("send_verify_email").session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
+        events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
 
         String verificationUrl = MailUtil.getLink(body);
 
@@ -194,7 +195,7 @@ public class RequiredActionEmailVerificationTest {
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        events.expectRequiredAction("verify_email").session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+        events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
 
         events.expectLogin().session(sessionId).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 12a58b2..49f97ed 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
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
@@ -112,7 +113,7 @@ public class RequiredActionMultipleActionsTest {
     public String updatePassword(String sessionId) {
         changePasswordPage.changePassword("new-password", "new-password");
 
-        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_password");
+        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PASSWORD);
         if (sessionId != null) {
             expectedEvent.session(sessionId);
         }
@@ -122,12 +123,12 @@ public class RequiredActionMultipleActionsTest {
     public String updateProfile(String sessionId) {
         updateProfilePage.update("New first", "New last", "new@email.com");
 
-        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction("update_profile");
+        AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PROFILE);
         if (sessionId != null) {
             expectedEvent.session(sessionId);
         }
         sessionId = expectedEvent.assertEvent().getSessionId();
-        events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        events.expectRequiredAction(EventType.UPDATE_EMAIL).session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
         return sessionId;
     }
 
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 ad5f74b..d4cc32e 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
@@ -26,6 +26,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
@@ -93,7 +94,7 @@ public class RequiredActionResetPasswordTest {
         changePasswordPage.assertCurrent();
         changePasswordPage.changePassword("new-password", "new-password");
 
-        String sessionId = events.expectRequiredAction("update_password").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
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 f69aef9..a38ced3 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
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.representations.idm.CredentialRepresentation;
@@ -109,7 +110,7 @@ public class RequiredActionTotpSetupTest {
 
         totpPage.configure(totp.generate(totpPage.getTotpSecret()));
 
-        String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
@@ -127,7 +128,7 @@ public class RequiredActionTotpSetupTest {
 
         totpPage.configure(totp.generate(totpSecret));
 
-        String sessionId = events.expectRequiredAction("update_totp").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
@@ -164,7 +165,7 @@ 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.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
 
         Event loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
 
@@ -192,7 +193,7 @@ public class RequiredActionTotpSetupTest {
         // Remove google authentificator
         accountTotpPage.removeTotp();
 
-        events.expectAccount("remove_totp").user(userId).assertEvent();
+        events.expectAccount(EventType.REMOVE_TOTP).user(userId).assertEvent();
 
         // Logout
         oauth.openLogout();
@@ -206,7 +207,7 @@ public class RequiredActionTotpSetupTest {
         totpPage.assertCurrent();
         totpPage.configure(totp.generate(totpPage.getTotpSecret()));
 
-        String sessionId = events.expectRequiredAction("update_totp").user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
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 373b487..056e6fd 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
@@ -27,6 +27,7 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.managers.RealmManager;
@@ -87,8 +88,8 @@ public class RequiredActionUpdateProfileTest {
 
         updateProfilePage.update("New first", "New last", "new@email.com");
 
-        String sessionId = events.expectRequiredAction("update_profile").assertEvent().getSessionId();
-        events.expectRequiredAction("update_email").session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent().getSessionId();
+        events.expectRequiredAction(EventType.UPDATE_EMAIL).session(sessionId).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 4a8543e..1ca02fe 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -12,6 +12,7 @@ import org.keycloak.audit.AuditListener;
 import org.keycloak.audit.AuditListenerFactory;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
@@ -110,12 +111,12 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
         events.clear();
     }
 
-    public ExpectedEvent expectRequiredAction(String event) {
+    public ExpectedEvent expectRequiredAction(EventType event) {
         return expectLogin().event(event).session(isUUID());
     }
 
     public ExpectedEvent expectLogin() {
-        return expect("login")
+        return expect(EventType.LOGIN)
                 .detail(Details.CODE_ID, isCodeId())
                 .detail(Details.USERNAME, DEFAULT_USERNAME)
                 .detail(Details.RESPONSE_TYPE, "code")
@@ -125,7 +126,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     }
 
     public ExpectedEvent expectCodeToToken(String codeId, String sessionId) {
-        return expect("code_to_token")
+        return expect(EventType.CODE_TO_TOKEN)
                 .detail(Details.CODE_ID, codeId)
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, isUUID())
@@ -133,7 +134,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     }
 
     public ExpectedEvent expectRefresh(String refreshTokenId, String sessionId) {
-        return expect("refresh_token")
+        return expect(EventType.REFRESH_TOKEN)
                 .detail(Details.TOKEN_ID, isUUID())
                 .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
                 .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
@@ -141,14 +142,14 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
     }
 
     public ExpectedEvent expectLogout(String sessionId) {
-        return expect("logout").client((String) null)
+        return expect(EventType.LOGOUT).client((String) null)
                 .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
                 .session(sessionId);
     }
 
     public ExpectedEvent expectRegister(String username, String email) {
         UserRepresentation user = keycloak.getUser("test", username);
-        return expect("register")
+        return expect(EventType.REGISTER)
                 .user(user != null ? user.getId() : null)
                 .detail(Details.USERNAME, username)
                 .detail(Details.EMAIL, email)
@@ -157,11 +158,11 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
                 .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI);
     }
 
-    public ExpectedEvent expectAccount(String event) {
+    public ExpectedEvent expectAccount(EventType event) {
         return expect(event).client("account");
     }
 
-    public ExpectedEvent expect(String event) {
+    public ExpectedEvent expect(EventType event) {
         return new ExpectedEvent()
                 .realm(DEFAULT_REALM)
                 .client(DEFAULT_CLIENT_ID)
@@ -253,7 +254,7 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
             return this;
         }
 
-        public ExpectedEvent event(String e) {
+        public ExpectedEvent event(EventType e) {
             expected.setEvent(e);
             return this;
         }
@@ -291,6 +292,9 @@ public class AssertEvents implements TestRule, AuditListenerFactory {
         }
 
         public Event assertEvent(Event actual) {
+            if (expected.getError() != null && !expected.getEvent().toString().endsWith("_ERROR")) {
+                expected.setEvent(EventType.valueOf(expected.getEvent().toString() + "_ERROR"));
+            }
             Assert.assertEquals(expected.getEvent(), actual.getEvent());
             Assert.assertEquals(expected.getRealmId(), actual.getRealmId());
             Assert.assertEquals(expected.getClientId(), actual.getClientId());
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 aa51050..af561d1 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
@@ -94,7 +94,7 @@ public class LoginTest {
 
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
 
-        events.expectLogin().user((String) null).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
+        events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials").detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).assertEvent();
     }
 
     @Test
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 9502251..15710b1 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
@@ -111,7 +111,7 @@ 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).session((String) null).assertEvent();
+        events.expectLogin().error("invalid_user_credentials").removeDetail(Details.CODE_ID).session((String) null).assertEvent();
     }
 
     @Test
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 c1066bb..719b98c 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
@@ -27,6 +27,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
@@ -125,7 +126,7 @@ public class ResetPasswordTest {
 
         resetPasswordPage.assertCurrent();
 
-        String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
 
         Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
 
@@ -142,7 +143,7 @@ public class ResetPasswordTest {
 
         updatePasswordPage.changePassword("resetPassword", "resetPassword");
 
-        events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
+        events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
@@ -178,7 +179,7 @@ public class ResetPasswordTest {
 
         Assert.assertEquals(0, greenMail.getReceivedMessages().length);
 
-        events.expectRequiredAction("send_reset_password").user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
+        events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user((String) null).session((String) null).detail(Details.USERNAME, "invalid").removeDetail(Details.EMAIL).removeDetail(Details.CODE_ID).error("user_not_found").assertEvent();
     }
 
     @Test
@@ -208,7 +209,7 @@ public class ResetPasswordTest {
         String body = (String) message.getContent();
         String changePasswordUrl = MailUtil.getLink(body);
 
-        String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
+        String sessionId = events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
 
         driver.navigate().to(changePasswordUrl.trim());
 
@@ -220,7 +221,7 @@ public class ResetPasswordTest {
 
         updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy");
 
-        events.expectRequiredAction("update_password").user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
+        events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, "login-test").assertEvent();
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
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 f7693bf..8b5df03 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
@@ -29,6 +29,7 @@ import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.audit.Details;
 import org.keycloak.audit.Event;
+import org.keycloak.audit.EventType;
 import org.keycloak.models.RealmModel;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.idm.UserRepresentation;
@@ -110,7 +111,7 @@ public class SocialLoginTest {
 
         Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
 
-        String userId = events.expect("register")
+        String userId = events.expect(EventType.REGISTER)
                 .user(AssertEvents.isUUID())
                 .detail(Details.EMAIL, "bob@builder.com")
                 .detail(Details.RESPONSE_TYPE, "code")
@@ -202,7 +203,7 @@ public class SocialLoginTest {
             Assert.assertEquals("Builder", profilePage.getLastName());
             Assert.assertEquals("bob@builder.com", profilePage.getEmail());
 
-            String userId = events.expect("register")
+            String userId = events.expect(EventType.REGISTER)
                     .user(AssertEvents.isUUID())
                     .detail(Details.EMAIL, "bob@builder.com")
                     .detail(Details.RESPONSE_TYPE, "code")
@@ -213,8 +214,8 @@ public class SocialLoginTest {
 
             profilePage.update("Dummy", "User", "dummy-user-reg@dummy-social");
 
-            events.expectRequiredAction("update_profile").user(userId).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent();
-            events.expectRequiredAction("update_email").user(userId).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").detail(Details.PREVIOUS_EMAIL, "bob@builder.com").detail(Details.UPDATED_EMAIL, "dummy-user-reg@dummy-social").assertEvent();
+            events.expectRequiredAction(EventType.UPDATE_PROFILE).user(userId).detail(Details.AUTH_METHOD, "social@dummy").detail(Details.USERNAME, "2@dummy").assertEvent();
+            events.expectRequiredAction(EventType.UPDATE_EMAIL).user(userId).detail(Details.AUTH_METHOD, "social@dummy").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());