keycloak-developers

Merge pull request #921 from patriot1burke/master logout

1/20/2015 1:57:04 PM

Changes

Details

diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index ddf4396..bb40325 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -23,6 +23,7 @@
         <class>org.keycloak.models.sessions.jpa.entities.ClientSessionEntity</class>
         <class>org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity</class>
         <class>org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity</class>
+        <class>org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity</class>
         <class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
         <class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>
 
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
old mode 100644
new mode 100755
index e8acd71..c203b4e
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
@@ -32,6 +32,9 @@
                 <constraints nullable="false"/>
             </column>
         </createTable>
+        <addColumn tableName="CLIENT">
+            <column name="FRONTCHANNEL_LOGOUT" type="BOOLEAN" defaultValueBoolean="false"/>
+        </addColumn>
         <addPrimaryKey columnNames="INTERNAL_ID" constraintName="CONSTRAINT_2B" tableName="IDENTITY_PROVIDER"/>
         <addPrimaryKey columnNames="IDENTITY_PROVIDER, USER_ID" constraintName="CONSTRAINT_40" tableName="FEDERATED_IDENTITY"/>
         <addPrimaryKey columnNames="IDENTITY_PROVIDER_ID, NAME" constraintName="CONSTRAINT_D" tableName="IDENTITY_PROVIDER_CONFIG"/>
diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java
index cba47d9..ecbb489 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -69,6 +69,9 @@ public interface ClientModel {
     String getAttribute(String name);
     Map<String, String> getAttributes();
 
+    boolean isFrontchannelLogout();
+    void setFrontchannelLogout(boolean flag);
+
 
     boolean isPublicClient();
     void setPublicClient(boolean flag);
diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
index bdbc5c4..059afc9 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
@@ -49,7 +49,8 @@ public interface ClientSessionModel {
         UPDATE_PASSWORD,
         RECOVER_PASSWORD,
         AUTHENTICATE,
-        SOCIAL_CALLBACK
+        SOCIAL_CALLBACK,
+        LOGGED_OUT
     }
 
 }
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index 3ca5761..5b37c34 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -18,6 +18,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     private int notBefore;
     private boolean publicClient;
     private boolean fullScopeAllowed;
+    private boolean frontchannelLogout;
 
     private String realmId;
     private Map<String, String> attributes = new HashMap<String, String>();
@@ -130,4 +131,12 @@ public class ClientEntity extends AbstractIdentifiableEntity {
     public void setAttributes(Map<String, String> attributes) {
         this.attributes = attributes;
     }
+
+    public boolean isFrontchannelLogout() {
+        return frontchannelLogout;
+    }
+
+    public void setFrontchannelLogout(boolean frontchannelLogout) {
+        this.frontchannelLogout = frontchannelLogout;
+    }
 }
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
index 03a93d6..61767b5 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -27,4 +27,18 @@ public interface UserSessionModel {
 
     List<ClientSessionModel> getClientSessions();
 
+    public String getNote(String name);
+    public void setNote(String name, String value);
+    public void removeNote(String name);
+
+    State getState();
+    void setState(State state);
+
+    public static enum State {
+        LOGGING_IN,
+        LOGGED_IN,
+        LOGGING_OUT,
+        LOGGED_OUT
+    }
+
 }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
index 854fa62..dff08a6 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java
@@ -125,6 +125,16 @@ public abstract class ClientAdapter implements ClientModel {
         updatedClient.setPublicClient(flag);
     }
 
+    public boolean isFrontchannelLogout() {
+        if (updatedClient != null) return updatedClient.isPublicClient();
+        return cachedClient.isFrontchannelLogout();
+    }
+
+    public void setFrontchannelLogout(boolean flag) {
+        getDelegateForUpdate();
+        updatedClient.setFrontchannelLogout(flag);
+    }
+
     @Override
     public boolean isFullScopeAllowed() {
         if (updatedClient != null) return updatedClient.isFullScopeAllowed();
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index 484619f..b4b605e 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -28,6 +28,7 @@ public class CachedClient {
     protected boolean publicClient;
     protected boolean fullScopeAllowed;
     protected boolean directGrantsOnly;
+    protected boolean frontchannelLogout;
     protected int notBefore;
     protected Set<String> scope = new HashSet<String>();
     protected Set<String> webOrigins = new HashSet<String>();
@@ -42,6 +43,7 @@ public class CachedClient {
         attributes.putAll(model.getAttributes());
         notBefore = model.getNotBefore();
         directGrantsOnly = model.isDirectGrantsOnly();
+        frontchannelLogout = model.isFrontchannelLogout();
         publicClient = model.isPublicClient();
         allowedClaimsMask = model.getAllowedClaimsMask();
         fullScopeAllowed = model.isFullScopeAllowed();
@@ -112,4 +114,12 @@ public class CachedClient {
     public Map<String, String> getAttributes() {
         return attributes;
     }
+
+    public boolean isFrontchannelLogout() {
+        return frontchannelLogout;
+    }
+
+    public void setFrontchannelLogout(boolean frontchannelLogout) {
+        this.frontchannelLogout = frontchannelLogout;
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index 51257da..e71ba14 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -81,6 +81,16 @@ public abstract class ClientAdapter implements ClientModel {
     }
 
     @Override
+    public boolean isFrontchannelLogout() {
+        return entity.isFrontchannelLogout();
+    }
+
+    @Override
+    public void setFrontchannelLogout(boolean flag) {
+        entity.setFrontchannelLogout(flag);
+    }
+
+    @Override
     public boolean isFullScopeAllowed() {
         return entity.isFullScopeAllowed();
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index c5652a8..8b32096 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -43,6 +43,8 @@ public abstract class ClientEntity {
     private boolean publicClient;
     @Column(name="PROTOCOL")
     private String protocol;
+    @Column(name="FRONTCHANNEL_LOGOUT")
+    private boolean frontchannelLogout;
     @Column(name="FULL_SCOPE_ALLOWED")
     private boolean fullScopeAllowed;
 
@@ -169,4 +171,12 @@ public abstract class ClientEntity {
     public void setProtocol(String protocol) {
         this.protocol = protocol;
     }
+
+    public boolean isFrontchannelLogout() {
+        return frontchannelLogout;
+    }
+
+    public void setFrontchannelLogout(boolean frontchannelLogout) {
+        this.frontchannelLogout = frontchannelLogout;
+    }
 }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index b549f36..ad56f84 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -159,6 +159,18 @@ public abstract class ClientAdapter<T extends MongoIdentifiableEntity> extends A
         updateMongoEntity();
     }
 
+
+    @Override
+    public boolean isFrontchannelLogout() {
+        return getMongoEntityAsClient().isFrontchannelLogout();
+    }
+
+    @Override
+    public void setFrontchannelLogout(boolean flag) {
+        getMongoEntityAsClient().setFrontchannelLogout(flag);
+        updateMongoEntity();
+    }
+
     @Override
     public boolean isFullScopeAllowed() {
         return getMongoEntityAsClient().isFullScopeAllowed();
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
old mode 100644
new mode 100755
index 3300ae6..99c23a9
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -1,5 +1,8 @@
 package org.keycloak.models.sessions.infinispan.entities;
 
+import org.keycloak.models.UserSessionModel;
+
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -23,6 +26,10 @@ public class UserSessionEntity extends SessionEntity {
 
     private Set<String> clientSessions;
 
+    private UserSessionModel.State state;
+
+    private Map<String, String> notes;
+
     public String getUser() {
         return user;
     }
@@ -86,4 +93,20 @@ public class UserSessionEntity extends SessionEntity {
     public void setClientSessions(Set<String> clientSessions) {
         this.clientSessions = clientSessions;
     }
+
+    public Map<String, String> getNotes() {
+        return notes;
+    }
+
+    public void setNotes(Map<String, String> notes) {
+        this.notes = notes;
+    }
+
+    public UserSessionModel.State getState() {
+        return state;
+    }
+
+    public void setState(UserSessionModel.State state) {
+        this.state = state;
+    }
 }
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index 1c6ffb6..dfd7914 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -11,6 +11,7 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -78,6 +79,39 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public String getNote(String name) {
+        return entity.getNotes() != null ? entity.getNotes().get(name) : null;
+    }
+
+    @Override
+    public void setNote(String name, String value) {
+        if (entity.getNotes() == null) {
+            entity.setNotes(new HashMap<String, String>());
+        }
+        entity.getNotes().put(name, value);
+        update();
+    }
+
+    @Override
+    public void removeNote(String name) {
+        if (entity.getNotes() != null) {
+            entity.getNotes().remove(name);
+            update();
+        }
+    }
+
+    @Override
+    public State getState() {
+        return entity.getState();
+    }
+
+    @Override
+    public void setState(State state) {
+        entity.setState(state);
+        update();
+    }
+
+    @Override
     public List<ClientSessionModel> getClientSessions() {
         if (entity.getClientSessions() != null) {
             List<ClientSessionEntity> clientSessions = new LinkedList<ClientSessionEntity>();
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
index e807861..73f9b60 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java
@@ -1,5 +1,7 @@
 package org.keycloak.models.sessions.jpa.entities;
 
+import org.keycloak.models.UserSessionModel;
+
 import javax.persistence.CascadeType;
 import javax.persistence.Column;
 import javax.persistence.Entity;
@@ -54,9 +56,15 @@ public class UserSessionEntity {
     @Column(name="LAST_SESSION_REFRESH")
     protected int lastSessionRefresh;
 
+    @Column(name="USER_SESSION_STATE")
+    protected UserSessionModel.State state;
+
     @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="session")
     protected Collection<ClientSessionEntity> clientSessions = new ArrayList<ClientSessionEntity>();
 
+    @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="userSession")
+    protected Collection<UserSessionNoteEntity> notes = new ArrayList<UserSessionNoteEntity>();
+
     public String getId() {
         return id;
     }
@@ -133,4 +141,19 @@ public class UserSessionEntity {
         return clientSessions;
     }
 
+     public UserSessionModel.State getState() {
+        return state;
+    }
+
+    public void setState(UserSessionModel.State state) {
+        this.state = state;
+    }
+
+    public Collection<UserSessionNoteEntity> getNotes() {
+        return notes;
+    }
+
+    public void setNotes(Collection<UserSessionNoteEntity> notes) {
+        this.notes = notes;
+    }
 }
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionNoteEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionNoteEntity.java
new file mode 100755
index 0000000..762ce6a
--- /dev/null
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionNoteEntity.java
@@ -0,0 +1,107 @@
+package org.keycloak.models.sessions.jpa.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@NamedQueries({
+        @NamedQuery(name = "removeUserSessionNoteByUser", query="delete from UserSessionNoteEntity r where r.userSession IN (select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId)"),
+        @NamedQuery(name = "removeUserSessionNoteByRealm", query="delete from UserSessionNoteEntity r where r.userSession IN (select c from UserSessionEntity c where c.realmId = :realmId)"),
+        @NamedQuery(name = "removeUserSessionNoteByExpired", query = "delete from UserSessionNoteEntity r where r.userSession IN (select s from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime))")
+})
+@Table(name="USER_SESSION_NOTE")
+@Entity
+@IdClass(UserSessionNoteEntity.Key.class)
+public class UserSessionNoteEntity {
+
+    @Id
+    @ManyToOne(fetch= FetchType.LAZY)
+    @JoinColumn(name = "USER_SESSION")
+    protected UserSessionEntity userSession;
+
+    @Id
+    @Column(name = "NAME")
+    protected String name;
+    @Column(name = "VALUE")
+    protected String value;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public UserSessionEntity getUserSession() {
+        return userSession;
+    }
+
+    public void setUserSession(UserSessionEntity userSession) {
+        this.userSession = userSession;
+    }
+
+    public static class Key implements Serializable {
+
+        protected UserSessionEntity userSession;
+
+        protected String name;
+
+        public Key() {
+        }
+
+        public Key(UserSessionEntity clientSession, String name) {
+            this.userSession = clientSession;
+            this.name = name;
+        }
+
+        public UserSessionEntity getUserSession() {
+            return userSession;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Key key = (Key) o;
+
+            if (name != null ? !name.equals(key.name) : key.name != null) return false;
+            if (userSession != null ? !userSession.getId().equals(key.userSession != null ? key.userSession.getId() : null) : key.userSession != null) return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = userSession != null ? userSession.getId().hashCode() : 0;
+            result = 31 * result + (name != null ? name.hashCode() : 0);
+            return result;
+        }
+    }
+
+}
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
index 6708a2d..094d6ad 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java
@@ -173,6 +173,10 @@ public class JpaUserSessionProvider implements UserSessionProvider {
                 .setParameter("realmId", realm.getId())
                 .setParameter("userId", user.getId())
                 .executeUpdate();
+        em.createNamedQuery("removeUserSessionNoteByUser")
+                .setParameter("realmId", realm.getId())
+                .setParameter("userId", user.getId())
+                .executeUpdate();
         em.createNamedQuery("removeUserSessionByUser")
                 .setParameter("realmId", realm.getId())
                 .setParameter("userId", user.getId())
@@ -211,6 +215,11 @@ public class JpaUserSessionProvider implements UserSessionProvider {
                 .setParameter("maxTime", maxTime)
                 .setParameter("idleTime", idleTime)
                 .executeUpdate();
+        em.createNamedQuery("removeUserSessionNoteByExpired")
+                .setParameter("realmId", realm.getId())
+                .setParameter("maxTime", maxTime)
+                .setParameter("idleTime", idleTime)
+                .executeUpdate();
         em.createNamedQuery("removeUserSessionByExpired")
                 .setParameter("realmId", realm.getId())
                 .setParameter("maxTime", maxTime)
@@ -223,6 +232,7 @@ public class JpaUserSessionProvider implements UserSessionProvider {
         em.createNamedQuery("removeClientSessionNoteByRealm").setParameter("realmId", realm.getId()).executeUpdate();
         em.createNamedQuery("removeClientSessionRoleByRealm").setParameter("realmId", realm.getId()).executeUpdate();
         em.createNamedQuery("removeClientSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+        em.createNamedQuery("removeUserSessionNoteByRealm").setParameter("realmId", realm.getId()).executeUpdate();
         em.createNamedQuery("removeUserSessionByRealm").setParameter("realmId", realm.getId()).executeUpdate();
     }
 
diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java
index 2110453..ca16427 100755
--- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java
+++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java
@@ -7,8 +7,10 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity;
 import org.keycloak.models.sessions.jpa.entities.UserSessionEntity;
+import org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity;
 
 import javax.persistence.EntityManager;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -79,6 +81,55 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public void setNote(String name, String value) {
+        for (UserSessionNoteEntity attr : entity.getNotes()) {
+            if (attr.getName().equals(name)) {
+                attr.setValue(value);
+                return;
+            }
+        }
+        UserSessionNoteEntity attr = new UserSessionNoteEntity();
+        attr.setName(name);
+        attr.setValue(value);
+        attr.setUserSession(entity);
+        em.persist(attr);
+        entity.getNotes().add(attr);
+    }
+
+    @Override
+    public void removeNote(String name) {
+        Iterator<UserSessionNoteEntity> it = entity.getNotes().iterator();
+        while (it.hasNext()) {
+            UserSessionNoteEntity attr = it.next();
+            if (attr.getName().equals(name)) {
+                it.remove();
+                em.remove(attr);
+            }
+        }
+    }
+
+    @Override
+    public String getNote(String name) {
+        for (UserSessionNoteEntity attr : entity.getNotes()) {
+            if (attr.getName().equals(name)) {
+                return attr.getValue();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public State getState() {
+        return entity.getState();
+    }
+
+    @Override
+    public void setState(State state) {
+        entity.setState(state);
+
+    }
+
+    @Override
     public List<ClientSessionModel> getClientSessions() {
         List<ClientSessionModel> clientSessions = new LinkedList<ClientSessionModel>();
         for (ClientSessionEntity e : entity.getClientSessions()) {
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java
old mode 100644
new mode 100755
index 30cb920..16b74db
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java
@@ -1,8 +1,12 @@
 package org.keycloak.models.sessions.mem.entities;
 
+import org.keycloak.models.UserSessionModel;
+
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -18,6 +22,8 @@ public class UserSessionEntity {
     private boolean rememberMe;
     private int started;
     private int lastSessionRefresh;
+    private UserSessionModel.State state;
+    private Map<String, String> notes = new HashMap<String, String>();
     private List<ClientSessionEntity> clientSessions = Collections.synchronizedList(new LinkedList<ClientSessionEntity>());
 
     public String getId() {
@@ -109,4 +115,15 @@ public class UserSessionEntity {
         return clientSessions;
     }
 
+    public Map<String, String> getNotes() {
+        return notes;
+    }
+
+    public UserSessionModel.State getState() {
+        return state;
+    }
+
+    public void setState(UserSessionModel.State state) {
+        this.state = state;
+    }
 }
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java
index e2da268..5b215a0 100755
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java
@@ -82,6 +82,17 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public State getState() {
+        return entity.getState();
+    }
+
+    @Override
+    public void setState(State state) {
+        entity.setState(state);
+
+    }
+
+    @Override
     public List<ClientSessionModel> getClientSessions() {
         List<ClientSessionModel> clientSessionModels = new LinkedList<ClientSessionModel>();
         if (entity.getClientSessions() != null) {
@@ -106,4 +117,22 @@ public class UserSessionAdapter implements UserSessionModel {
         return getId().hashCode();
     }
 
+    @Override
+    public String getNote(String name) {
+        return entity.getNotes().get(name);
+    }
+
+    @Override
+    public void setNote(String name, String value) {
+        entity.getNotes().put(name, value);
+
+    }
+
+    @Override
+    public void removeNote(String name) {
+        entity.getNotes().remove(name);
+
+    }
+
+
 }
diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java
index 141297b..ccf0f49 100755
--- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java
+++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java
@@ -5,10 +5,13 @@ import com.mongodb.QueryBuilder;
 import org.keycloak.connections.mongo.api.MongoCollection;
 import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.entities.AbstractIdentifiableEntity;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -34,6 +37,10 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
 
     private List<String> clientSessions = new ArrayList<String>();
 
+    private Map<String, String> notes = new HashMap<String, String>();
+
+    private UserSessionModel.State state;
+
     public String getRealmId() {
         return realmId;
     }
@@ -114,4 +121,19 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
         context.getMongoStore().removeEntities(MongoClientSessionEntity.class, query, context);
     }
 
+    public Map<String, String> getNotes() {
+        return notes;
+    }
+
+    public void setNotes(Map<String, String> notes) {
+        this.notes = notes;
+    }
+
+    public UserSessionModel.State getState() {
+        return state;
+    }
+
+    public void setState(UserSessionModel.State state) {
+        this.state = state;
+    }
 }
diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java
index 0f30e5a..c12f377 100755
--- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java
+++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java
@@ -83,6 +83,18 @@ public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEnt
     }
 
     @Override
+    public State getState() {
+        return entity.getState();
+    }
+
+    @Override
+    public void setState(State state) {
+        entity.setState(state);
+        updateMongoEntity();
+
+    }
+
+    @Override
     public List<ClientSessionModel> getClientSessions() {
         List<ClientSessionModel> sessions = new LinkedList<ClientSessionModel>();
         if (entity.getClientSessions() == null) {
@@ -98,6 +110,23 @@ public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEnt
     }
 
     @Override
+    public String getNote(String name) {
+        return entity.getNotes().get(name);
+    }
+
+    @Override
+    public void setNote(String name, String value) {
+        entity.getNotes().put(name, value);
+        updateMongoEntity();
+    }
+
+    @Override
+    public void removeNote(String name) {
+        entity.getNotes().remove(name);
+        updateMongoEntity();
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || !(o instanceof UserSessionModel)) return false;
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java
index ed1b955..294f210 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java
@@ -140,11 +140,14 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
         }
 
         public String htmlResponse() throws ProcessingException, ConfigurationException, IOException {
-            return buildHtml(encoded());
+            return buildHtml(encoded(), destination);
 
         }
         public Response response() throws ConfigurationException, ProcessingException, IOException {
-            return buildResponse(document);
+            return buildResponse(document, destination);
+        }
+        public Response response(String actionUrl) throws ConfigurationException, ProcessingException, IOException {
+            return buildResponse(document, actionUrl);
         }
     }
 
@@ -162,11 +165,15 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
         public Document getDocument() {
             return document;
         }
-        public URI responseUri() throws ConfigurationException, ProcessingException, IOException {
-            return generateRedirectUri("SAMLResponse", document);
+        public URI responseUri(String redirectUri) throws ConfigurationException, ProcessingException, IOException {
+            return generateRedirectUri("SAMLResponse", redirectUri, document);
         }
         public Response response() throws ProcessingException, ConfigurationException, IOException {
-            URI uri = responseUri();
+            return response(destination);
+        }
+
+        public Response response(String redirectUri) throws ProcessingException, ConfigurationException, IOException {
+            URI uri = responseUri(redirectUri);
 
             CacheControl cacheControl = new CacheControl();
             cacheControl.setNoCache(true);
@@ -259,8 +266,8 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
     }
 
 
-    protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
-        String str = buildHtmlPostResponse(responseDoc);
+    protected Response buildResponse(Document responseDoc, String actionUrl) throws ProcessingException, ConfigurationException, IOException {
+        String str = buildHtmlPostResponse(responseDoc, actionUrl);
 
         CacheControl cacheControl = new CacheControl();
         cacheControl.setNoCache(true);
@@ -269,14 +276,14 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
                        .header("Cache-Control", "no-cache, no-store").build();
     }
 
-    protected String buildHtmlPostResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
+    protected String buildHtmlPostResponse(Document responseDoc, String actionUrl) throws ProcessingException, ConfigurationException, IOException {
         byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
         String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
 
-        return buildHtml(samlResponse);
+        return buildHtml(samlResponse, actionUrl);
     }
 
-    protected String buildHtml(String samlResponse) {
+    protected String buildHtml(String samlResponse, String actionUrl) {
         if (destination == null) {
             throw SALM2LoginResponseBuilder.logger.nullValueError("Destination is null");
         }
@@ -291,7 +298,7 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
         builder.append("</HEAD>");
         builder.append("<BODY Onload=\"document.forms[0].submit()\">");
 
-        builder.append("<FORM METHOD=\"POST\" ACTION=\"" + destination + "\">");
+        builder.append("<FORM METHOD=\"POST\" ACTION=\"" + actionUrl + "\">");
         builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
 
         if (isNotNull(relayState)) {
@@ -315,8 +322,8 @@ public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
     }
 
 
-    protected URI generateRedirectUri(String samlParameterName, Document document) throws ConfigurationException, ProcessingException, IOException {
-        UriBuilder builder = UriBuilder.fromUri(destination)
+    protected URI generateRedirectUri(String samlParameterName, String redirectUri, Document document) throws ConfigurationException, ProcessingException, IOException {
+        UriBuilder builder = UriBuilder.fromUri(redirectUri)
                 .replaceQuery(null)
                 .queryParam(samlParameterName, base64Encoded(document));
         if (relayState != null) {
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutResponseBuilder.java
new file mode 100755
index 0000000..9815e61
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutResponseBuilder.java
@@ -0,0 +1,81 @@
+package org.keycloak.protocol.saml;
+
+import org.picketlink.common.constants.JBossSAMLURIConstants;
+import org.picketlink.common.exceptions.ConfigurationException;
+import org.picketlink.common.exceptions.ParsingException;
+import org.picketlink.common.exceptions.ProcessingException;
+import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
+import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
+import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
+import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
+import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
+import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
+import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
+import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
+import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
+import org.picketlink.identity.federation.saml.v2.protocol.StatusCodeType;
+import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType;
+import org.picketlink.identity.federation.saml.v2.protocol.StatusType;
+import org.w3c.dom.Document;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SAML2LogoutResponseBuilder extends SAML2BindingBuilder<SAML2LogoutResponseBuilder> {
+
+    protected String logoutRequestID;
+
+    public SAML2LogoutResponseBuilder logoutRequestID(String logoutRequestID) {
+        this.logoutRequestID = logoutRequestID;
+        return this;
+    }
+
+    public RedirectBindingBuilder redirectBinding()  throws ConfigurationException, ProcessingException {
+        Document samlResponseDocument = buildDocument();
+        return new RedirectBindingBuilder(samlResponseDocument);
+
+    }
+
+    public PostBindingBuilder postBinding()  throws ConfigurationException, ProcessingException {
+        Document samlResponseDocument = buildDocument();
+        return new PostBindingBuilder(samlResponseDocument);
+
+    }
+
+
+    public Document buildDocument() throws ProcessingException {
+        Document samlResponse = null;
+        try {
+            StatusResponseType statusResponse = new StatusResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
+
+            // Status
+            StatusType statusType = new StatusType();
+            StatusCodeType statusCodeType = new StatusCodeType();
+            statusCodeType.setValue(URI.create(JBossSAMLURIConstants.STATUS_SUCCESS.get()));
+            statusType.setStatusCode(statusCodeType);
+
+            statusResponse.setStatus(statusType);
+            statusResponse.setInResponseTo(logoutRequestID);
+            NameIDType issuer = new NameIDType();
+            issuer.setValue(responseIssuer);
+
+            statusResponse.setIssuer(issuer);
+            statusResponse.setDestination(destination);
+
+            SAML2Response saml2Response = new SAML2Response();
+            samlResponse = saml2Response.convert(statusResponse);
+        } catch (ConfigurationException e) {
+            throw new ProcessingException(e);
+        } catch (ParsingException e) {
+            throw new ProcessingException(e);
+        }
+        if (encrypt) encryptDocument(samlResponse);
+        return samlResponse;
+
+    }
+
+
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 68e5d35..0e68eb3 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -21,12 +21,16 @@ import org.keycloak.services.resources.admin.ClientAttributeCertificateResource;
 import org.keycloak.services.resources.flows.Flows;
 import org.picketlink.common.constants.GeneralConstants;
 import org.picketlink.common.constants.JBossSAMLURIConstants;
+import org.picketlink.common.exceptions.ConfigurationException;
+import org.picketlink.common.exceptions.ParsingException;
+import org.picketlink.common.exceptions.ProcessingException;
 import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
 import org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler;
 
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
 import java.security.PublicKey;
 import java.util.UUID;
 
@@ -55,6 +59,12 @@ public class SamlProtocol implements LoginProtocol {
     public static final String SAML_ENCRYPT = "saml.encrypt";
     public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
     public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
+    public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
+    public static final String SAML_LOGOUT_ISSUER = "saml.logout.issuer";
+    public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
+    public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
+    public static final String SAML_LOGOUT_BINDING_URI = "SAML_LOGOUT_BINDING_URI";
+    public static final String SAML_LOGOUT_SIGNATURE_ALGORITHM = "saml.logout.signature.algorithm";
     public static final String SAML_NAME_ID = "SAML_NAME_ID";
     public static final String SAML_NAME_ID_FORMAT = "SAML_NAME_ID_FORMAT";
     public static final String SAML_DEFAULT_NAMEID_FORMAT = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
@@ -122,6 +132,15 @@ public class SamlProtocol implements LoginProtocol {
         return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || "true".equals(client.getAttribute(SAML_FORCE_POST_BINDING));
     }
 
+    protected boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
+        String note = session.getNote(SamlProtocol.SAML_LOGOUT_BINDING);
+        return SamlProtocol.SAML_POST_BINDING.equals(note);
+    }
+
+    protected boolean isLogoutPostBindingForClient(ClientModel client) {
+        return SamlProtocol.SAML_POST_BINDING.equals(client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING));
+    }
+
     protected String getNameIdFormat(ClientSessionModel clientSession) {
         String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT);
         if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT;
@@ -222,19 +241,19 @@ public class SamlProtocol implements LoginProtocol {
         }
     }
 
-    private boolean requiresRealmSignature(ClientModel client) {
+    public static boolean requiresRealmSignature(ClientModel client) {
         return "true".equals(client.getAttribute(SAML_SERVER_SIGNATURE));
     }
 
-    private boolean requiresAssertionSignature(ClientModel client) {
+    public static boolean requiresAssertionSignature(ClientModel client) {
         return "true".equals(client.getAttribute(SAML_ASSERTION_SIGNATURE));
     }
 
-    private boolean includeAuthnStatement(ClientModel client) {
+    public static boolean includeAuthnStatement(ClientModel client) {
         return "true".equals(client.getAttribute(SAML_AUTHNSTATEMENT));
     }
 
-    private boolean multivaluedRoles(ClientModel client) {
+    public static boolean multivaluedRoles(ClientModel client) {
         return "true".equals(client.getAttribute(SAML_MULTIVALUED_ROLES));
     }
 
@@ -271,34 +290,77 @@ public class SamlProtocol implements LoginProtocol {
         return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
     }
 
+    protected String getBindingUri(ClientModel client) {
+        String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
+        if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
+        return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
+
+    }
+
     @Override
-    public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+    public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
         ClientModel client = clientSession.getClient();
-        if (!(client instanceof ApplicationModel)) return;
+        if (!(client instanceof ApplicationModel)) return null;
         ApplicationModel app = (ApplicationModel)client;
-        if (app.getManagementUrl() == null) return;
+        String bindingUri = getBindingUri(client);
+        if (bindingUri == null) return null;
+        SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
+        try {
+            if (isLogoutPostBindingForClient(app)) {
+                return logoutBuilder.postBinding().response(bindingUri);
+            } else {
+                return logoutBuilder.redirectBinding().response(bindingUri);
+            }
+        } catch (ConfigurationException e) {
+            throw new RuntimeException(e);
+        } catch (ProcessingException e) {
+            throw new RuntimeException(e);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } catch (ParsingException e) {
+            throw new RuntimeException(e);
+        }
 
-        // build userPrincipal with subject used at login
-        SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
-                                         .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
-                                         .destination(client.getClientId());
-        if (requiresRealmSignature(client)) {
-            logoutBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
-                         .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
-                         .signDocument();
+    }
+
+    @Override
+    public Response finishLogout(UserSessionModel userSession) {
+        SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
+        builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID));
+        builder.destination(userSession.getNote(SAML_LOGOUT_ISSUER));
+        String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
+        if (signingAlgorithm != null) {
+            SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
+            builder.signatureAlgorithm(algorithm)
+                    .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
+                    .signDocument();
         }
-        /*
-        if (requiresEncryption(client)) {
-            PublicKey publicKey = null;
-            try {
-                publicKey = PemUtils.decodePublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
-            } catch (Exception e) {
-                logger.error("failed", e);
-                return;
+
+        try {
+            if (isLogoutPostBindingForInitiator(userSession)) {
+                return builder.postBinding().response(userSession.getNote(SAML_LOGOUT_BINDING_URI));
+            } else {
+                return builder.redirectBinding().response(userSession.getNote(SAML_LOGOUT_BINDING_URI));
             }
-            logoutBuilder.encrypt(publicKey);
+        } catch (ConfigurationException e) {
+            throw new RuntimeException(e);
+        } catch (ProcessingException e) {
+            throw new RuntimeException(e);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
         }
-        */
+    }
+
+
+
+    @Override
+    public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+        ClientModel client = clientSession.getClient();
+        if (!(client instanceof ApplicationModel)) return;
+        ApplicationModel app = (ApplicationModel)client;
+        if (app.getManagementUrl() == null) return;
+        SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
+
 
         String logoutRequestString = null;
         try {
@@ -344,6 +406,31 @@ public class SamlProtocol implements LoginProtocol {
 
     }
 
+    protected SAML2LogoutRequestBuilder createLogoutRequest(ClientSessionModel clientSession, ClientModel client) {
+        // build userPrincipal with subject used at login
+        SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
+                                         .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
+                                         .destination(client.getClientId());
+        if (requiresRealmSignature(client)) {
+            logoutBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
+                         .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
+                         .signDocument();
+        }
+        /*
+        if (requiresEncryption(client)) {
+            PublicKey publicKey = null;
+            try {
+                publicKey = PemUtils.decodePublicKey(client.getAttribute(ClientModel.PUBLIC_KEY));
+            } catch (Exception e) {
+                logger.error("failed", e);
+                return;
+            }
+            logoutBuilder.encrypt(publicKey);
+        }
+        */
+        return logoutBuilder;
+    }
+
     @Override
     public void close() {
 
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 2ad6584..a9797e5 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -20,6 +20,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.OpenIDConnectService;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.ResourceAdminManager;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.services.resources.flows.Flows;
 import org.keycloak.util.StreamUtil;
@@ -118,10 +119,24 @@ public class SamlService {
             return null;
         }
 
-        protected Response handleSamlResponse(String samleResponse, String relayState) {
-            event.event(EventType.LOGIN);
-            event.error(Errors.INVALID_TOKEN);
-            return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
+        protected Response handleSamlResponse(String samlResponse, String relayState) {
+            AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
+            if (authResult == null) {
+                logger.warn("Unknown saml response.");
+                event.event(EventType.LOGIN);
+                event.error(Errors.INVALID_TOKEN);
+                return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
+            }
+            // assume this is a logout response
+            UserSessionModel userSession = authResult.getSession();
+            if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
+                logger.warn("Unknown saml response.");
+                logger.warn("UserSession is not tagged as logging out.");
+                event.event(EventType.LOGIN);
+                event.error(Errors.INVALID_TOKEN);
+                return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
+            }
+            return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
         }
 
         protected Response handleSamlRequest(String samlRequest, String relayState) {
@@ -176,7 +191,7 @@ public class SamlService {
             } else if (samlObject instanceof LogoutRequestType) {
                 event.event(EventType.LOGOUT);
                 LogoutRequestType logout = (LogoutRequestType) samlObject;
-                return logoutRequest(logout, client);
+                return logoutRequest(logout, client, relayState);
 
             } else {
                 event.event(EventType.LOGIN);
@@ -255,13 +270,32 @@ public class SamlService {
 
         protected abstract String getBindingType();
 
-        protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) {
+        protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
             // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
+
+
             AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
             if (authResult != null) {
-                logout(authResult.getSession());
+                String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
+                if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
+                bindingUri = ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
+                UserSessionModel userSession = authResult.getSession();
+                userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
+                if (SamlProtocol.requiresRealmSignature(client)) {
+                    userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString());
+
+                }
+                if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
+                userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
+                String logoutBinding = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING);
+                if (logoutBinding == null) logoutBinding = getBindingType();
+                userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
+                userSession.setNote(SamlProtocol.SAML_LOGOUT_ISSUER, logoutRequest.getIssuer().getValue());
+                userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
+                return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
             }
 
+
             String redirectUri = null;
 
             if (client instanceof ApplicationModel) {
@@ -269,20 +303,23 @@ public class SamlService {
             }
 
             if (redirectUri != null) {
-                String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);;
-                if (validatedRedirect == null) {
+                redirectUri = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);
+                if (redirectUri == null) {
                     return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri.");
                 }
-                return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build();
+            }
+            if (redirectUri != null) {
+                return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
             } else {
                 return Response.ok().build();
             }
 
         }
 
-        private void logout(UserSessionModel userSession) {
-            authManager.logout(session, realm, userSession, uriInfo, clientConnection);
-            event.user(userSession.getUser()).session(userSession).success();
+        private Response logout(UserSessionModel userSession) {
+            Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection);
+            if (response == null) event.user(userSession.getUser()).session(userSession).success();
+            return response;
         }
 
         private boolean checkSsl() {
diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
index 47e672a..1d6a2fa 100755
--- a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
@@ -30,4 +30,6 @@ public interface LoginProtocol extends Provider {
     Response consentDenied(ClientSessionModel clientSession);
 
     void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
+    Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
+    Response finishLogout(UserSessionModel userSession);
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
index 73ac4b5..d38d5b0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java
@@ -147,6 +147,17 @@ public class OpenIDConnect implements LoginProtocol {
     }
 
     @Override
+    public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+        // todo oidc redirect support
+        throw new RuntimeException("NOT IMPLEMENTED");
+    }
+
+    @Override
+    public Response finishLogout(UserSessionModel userSession) {
+        throw new RuntimeException("NOT IMPLEMENTED");
+    }
+
+    @Override
     public void close() {
 
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 36f2d0c..51df254 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -42,7 +42,6 @@ import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 
 /**
  * Stateless object that manages authentication
@@ -58,6 +57,7 @@ public class AuthenticationManager {
     // used solely to determine is user is logged in
     public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
     public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME";
+    public static final String KEYCLOAK_LOGOUT_PROTOCOL = "KEYCLOAK_LOGOUT_PROTOCOL";
 
     protected BruteForceProtector protector;
 
@@ -81,6 +81,7 @@ public class AuthenticationManager {
     public static void logout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) {
         if (userSession == null) return;
         UserModel user = userSession.getUser();
+        userSession.setState(UserSessionModel.State.LOGGING_OUT);
 
         logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
         expireIdentityCookie(realm, uriInfo, connection);
@@ -88,17 +89,85 @@ public class AuthenticationManager {
 
         for (ClientSessionModel clientSession : userSession.getClientSessions()) {
             ClientModel client = clientSession.getClient();
-            if (client instanceof ApplicationModel) {
+            if (client instanceof ApplicationModel && !client.isFrontchannelLogout() && clientSession.getAction() != ClientSessionModel.Action.LOGGED_OUT) {
                 String authMethod = clientSession.getAuthMethod();
                 if (authMethod == null) continue; // must be a keycloak service like account
                 LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
                 protocol.setRealm(realm)
                         .setUriInfo(uriInfo);
                 protocol.backchannelLogout(userSession, clientSession);
+                clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT);
             }
         }
+        userSession.setState(UserSessionModel.State.LOGGED_OUT);
+        session.sessions().removeUserSession(realm, userSession);
+    }
+
+
+    public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) {
+        if (userSession == null) return null;
+        UserModel user = userSession.getUser();
 
+        logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
+        if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
+            userSession.setState(UserSessionModel.State.LOGGING_OUT);
+        }
+        List<ClientSessionModel> redirectClients = new LinkedList<ClientSessionModel>();
+        for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+            ClientModel client = clientSession.getClient();
+            if (client.isFrontchannelLogout()) {
+                String authMethod = clientSession.getAuthMethod();
+                if (authMethod == null) continue; // must be a keycloak service like account
+                redirectClients.add(clientSession);
+                continue;
+            }
+            if (client instanceof ApplicationModel && !client.isFrontchannelLogout() && clientSession.getAction() != ClientSessionModel.Action.LOGGED_OUT) {
+                String authMethod = clientSession.getAuthMethod();
+                if (authMethod == null) continue; // must be a keycloak service like account
+                LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
+                protocol.setRealm(realm)
+                        .setUriInfo(uriInfo);
+                try {
+                    protocol.backchannelLogout(userSession, clientSession);
+                    clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT);
+                } catch (Exception e) {
+                    logger.warn("Failed to logout client, continuing", e);
+                }
+            }
+        }
+
+        if (redirectClients.size() == 0) {
+            return finishBrowserLogout(session, realm, userSession, uriInfo, connection);
+        }
+        for (ClientSessionModel nextRedirectClient : redirectClients) {
+            String authMethod = nextRedirectClient.getAuthMethod();
+            LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
+            protocol.setRealm(realm)
+                    .setUriInfo(uriInfo);
+            // setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not
+            nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT);
+            try {
+                Response response = protocol.frontchannelLogout(userSession, nextRedirectClient);
+                if (response != null) return response;
+            } catch (Exception e) {
+                logger.warn("Failed to logout client, continuing", e);
+            }
+
+        }
+        return finishBrowserLogout(session, realm, userSession, uriInfo, connection);
+    }
+
+    protected static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) {
+        expireIdentityCookie(realm, uriInfo, connection);
+        expireRememberMeCookie(realm, uriInfo, connection);
+        userSession.setState(UserSessionModel.State.LOGGED_OUT);
+        String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL);
+        LoginProtocol protocol = session.getProvider(LoginProtocol.class, method);
+        protocol.setRealm(realm)
+                .setUriInfo(uriInfo);
+        Response response = protocol.finishLogout(userSession);
         session.sessions().removeUserSession(realm, userSession);
+        return response;
     }
 
 
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index ff2069a..55f4726 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -52,6 +52,12 @@ public class ResourceAdminManager {
         return new ApacheHttpClient4Executor(client);
     }
 
+    public static String resolveUri(URI requestUri, String uri) {
+        String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, uri);
+        return StringPropertyReplacer.replaceProperties(absoluteURI);
+
+   }
+
     public static String getManagementUrl(URI requestUri, ApplicationModel application) {
         String mgmtUrl = application.getManagementUrl();
         if (mgmtUrl == null || mgmtUrl.equals("")) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
index b796452..d633120 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
@@ -79,6 +79,11 @@ public class SamlBindingTest {
         Thread.sleep(10000000);
     }
 
+    protected void checkLoggedOut() {
+        Assert.assertTrue(driver.getPageSource().contains("request-path: /logout.jsp"));
+        Assert.assertTrue(driver.getPageSource().contains("principal=null"));
+    }
+
 
     @Test
     public void testPostSimpleLoginLogout() {
@@ -89,8 +94,7 @@ public class SamlBindingTest {
         System.out.println(driver.getPageSource());
         Assert.assertTrue(driver.getPageSource().contains("bburke"));
         driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
-
+        checkLoggedOut();
     }
     @Test
     public void testPostSignedLoginLogout() {
@@ -100,7 +104,7 @@ public class SamlBindingTest {
         Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-post-sig/");
         Assert.assertTrue(driver.getPageSource().contains("bburke"));
         driver.navigate().to("http://localhost:8081/sales-post-sig?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
     @Test
@@ -113,7 +117,7 @@ public class SamlBindingTest {
         Assert.assertFalse(driver.getPageSource().contains("bburke"));
         Assert.assertTrue(driver.getPageSource().contains("principal=G-"));
         driver.navigate().to("http://localhost:8081/sales-post-sig-transient?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
     @Test
@@ -126,7 +130,7 @@ public class SamlBindingTest {
         Assert.assertFalse(driver.getPageSource().contains("bburke"));
         Assert.assertTrue(driver.getPageSource().contains("principal=G-"));
         driver.navigate().to("http://localhost:8081/sales-post-sig-persistent?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
     @Test
@@ -138,7 +142,7 @@ public class SamlBindingTest {
         System.out.println(driver.getPageSource());
         Assert.assertTrue(driver.getPageSource().contains("principal=bburke@redhat.com"));
         driver.navigate().to("http://localhost:8081/sales-post-sig-email?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
     @Test
@@ -149,7 +153,7 @@ public class SamlBindingTest {
         Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/employee-sig/");
         Assert.assertTrue(driver.getPageSource().contains("bburke"));
         driver.navigate().to("http://localhost:8081/employee-sig?GLO=true");
-        Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/demo/protocol/saml"));
+        checkLoggedOut();
 
     }
 
@@ -161,7 +165,7 @@ public class SamlBindingTest {
         Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-post-enc/");
         Assert.assertTrue(driver.getPageSource().contains("bburke"));
         driver.navigate().to("http://localhost:8081/sales-post-enc?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
     @Test
@@ -209,7 +213,7 @@ public class SamlBindingTest {
         String pageSource = driver.getPageSource();
         Assert.assertTrue(pageSource.contains("bburke"));
         driver.navigate().to("http://localhost:8081/sales-metadata?GLO=true");
-        Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/auth/realms/demo/protocol/saml");
+        checkLoggedOut();
 
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlKeycloakRule.java
index f3b543f..90f6d44 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlKeycloakRule.java
@@ -36,6 +36,9 @@ public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
             resp.setContentType("text/plain");
             OutputStream stream = resp.getOutputStream();
             Principal principal = req.getUserPrincipal();
+            stream.write("request-path: ".getBytes());
+            stream.write(req.getPathInfo().getBytes());
+            stream.write("\n".getBytes());
             stream.write("principal=".getBytes());
             if (principal == null) {
                 stream.write("null".getBytes());
@@ -49,6 +52,9 @@ public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
             resp.setContentType("text/plain");
             OutputStream stream = resp.getOutputStream();
             Principal principal = req.getUserPrincipal();
+            stream.write("request-path: ".getBytes());
+            stream.write(req.getPathInfo().getBytes());
+            stream.write("\n".getBytes());
             stream.write("principal=".getBytes());
             if (principal == null) {
                 stream.write("null".getBytes());