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 e8acd713d0..c203b4e83a --- 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 @@ + + + diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index ddf4396d10..bb403256a1 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 @@ org.keycloak.models.sessions.jpa.entities.ClientSessionEntity org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity + org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity org.keycloak.models.sessions.jpa.entities.UserSessionEntity org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity 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 cba47d95fe..ecbb48950f 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 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 bdbc5c48f6..059afc941b 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/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index 03a93d6f44..61767b54ec 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 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/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 3ca5761481..5b37c340fb 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 attributes = new HashMap(); @@ -130,4 +131,12 @@ public class ClientEntity extends AbstractIdentifiableEntity { public void setAttributes(Map attributes) { this.attributes = attributes; } + + public boolean isFrontchannelLogout() { + return frontchannelLogout; + } + + public void setFrontchannelLogout(boolean frontchannelLogout) { + this.frontchannelLogout = frontchannelLogout; + } } 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 854fa62a97..dff08a685a 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 484619fbdc..b4b605eff0 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 scope = new HashSet(); protected Set webOrigins = new HashSet(); @@ -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 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 51257da7c0..e71ba141d1 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 @@ -80,6 +80,16 @@ public abstract class ClientAdapter implements ClientModel { entity.setPublicClient(flag); } + @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 c5652a82f5..8b32096780 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 b549f36db6..ad56f84ff1 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 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/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index 1c6ffb6ccc..dfd7914e58 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; @@ -77,6 +78,39 @@ public class UserSessionAdapter implements UserSessionModel { update(); } + @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()); + } + 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 getClientSessions() { if (entity.getClientSessions() != null) { 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 3300ae666f..99c23a934b --- 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 clientSessions; + private UserSessionModel.State state; + + private Map notes; + public String getUser() { return user; } @@ -86,4 +93,20 @@ public class UserSessionEntity extends SessionEntity { public void setClientSessions(Set clientSessions) { this.clientSessions = clientSessions; } + + public Map getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.notes = notes; + } + + public UserSessionModel.State getState() { + return state; + } + + public void setState(UserSessionModel.State state) { + this.state = state; + } } 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 6708a2dbb7..094d6adf1a 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 2110453051..ca164270c5 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; @@ -78,6 +80,55 @@ public class UserSessionAdapter implements UserSessionModel { entity.setLastSessionRefresh(seconds); } + @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 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 getClientSessions() { List clientSessions = new LinkedList(); 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 e807861e48..73f9b605ae 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 clientSessions = new ArrayList(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="userSession") + protected Collection notes = new ArrayList(); + 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 getNotes() { + return notes; + } + + public void setNotes(Collection 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 0000000000..762ce6a317 --- /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 Bill Burke + * @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-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 e2da268763..5b215a055f 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 @@ -81,6 +81,17 @@ public class UserSessionAdapter implements UserSessionModel { entity.setLastSessionRefresh(lastSessionRefresh); } + @Override + public State getState() { + return entity.getState(); + } + + @Override + public void setState(State state) { + entity.setState(state); + + } + @Override public List getClientSessions() { List clientSessionModels = new LinkedList(); @@ -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-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 30cb920145..16b74db626 --- 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 Stian Thorgersen @@ -18,6 +22,8 @@ public class UserSessionEntity { private boolean rememberMe; private int started; private int lastSessionRefresh; + private UserSessionModel.State state; + private Map notes = new HashMap(); private List clientSessions = Collections.synchronizedList(new LinkedList()); public String getId() { @@ -109,4 +115,15 @@ public class UserSessionEntity { return clientSessions; } + public Map getNotes() { + return 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 0f30e5aa72..c12f377861 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 @@ -82,6 +82,18 @@ public class UserSessionAdapter extends AbstractMongoAdapter getClientSessions() { List sessions = new LinkedList(); @@ -97,6 +109,23 @@ public class UserSessionAdapter extends AbstractMongoAdapterStian Thorgersen @@ -34,6 +37,10 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement private List clientSessions = new ArrayList(); + private Map notes = new HashMap(); + + 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 getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.notes = notes; + } + + public UserSessionModel.State getState() { + return state; + } + + public void setState(UserSessionModel.State state) { + this.state = state; + } } 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 ed1b9552ae..294f210006 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 { } 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 { 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 { } - 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 { .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 { builder.append(""); builder.append(""); - builder.append("
"); + builder.append(""); builder.append(""); if (isNotNull(relayState)) { @@ -315,8 +322,8 @@ public class 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 0000000000..9815e61ec5 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class SAML2LogoutResponseBuilder extends SAML2BindingBuilder { + + 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 68e5d35e49..0e68eb315f 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 Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { + ClientModel client = clientSession.getClient(); + if (!(client instanceof ApplicationModel)) return null; + ApplicationModel app = (ApplicationModel)client; + 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); + } + + } + + @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(); + } + + 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)); + } + } 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); - // 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); - } - */ 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 2ad6584266..a9797e5a2a 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 47e672a4f1..1d6a2fa8f6 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 73ac4b51b1..d38d5b0ff1 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnect.java @@ -146,6 +146,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 36f2d0c187..51df25400d 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 redirectClients = new LinkedList(); + 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 ff2069a616..55f4726d6d 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 b796452f93..d63312010f 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 f3b543ff34..90f6d44905 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());