keycloak-developers
Changes
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/ClientAdapter.java 10(+10 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java 10(+10 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 23(+23 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 34(+34 -0)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionEntity.java 23(+23 -0)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/UserSessionNoteEntity.java 107(+107 -0)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/JpaUserSessionProvider.java 10(+10 -0)
model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/UserSessionAdapter.java 51(+51 -0)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java 17(+17 -0)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/UserSessionAdapter.java 29(+29 -0)
model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoUserSessionEntity.java 22(+22 -0)
model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/UserSessionAdapter.java 29(+29 -0)
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());