Merge pull request #921 from patriot1burke/master
logout via redirect - step 1
This commit is contained in:
commit
f454e5ae12
31 changed files with 801 additions and 63 deletions
3
connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
Normal file → Executable file
3
connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
Normal file → Executable file
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -49,7 +49,8 @@ public interface ClientSessionModel {
|
|||
UPDATE_PASSWORD,
|
||||
RECOVER_PASSWORD,
|
||||
AUTHENTICATE,
|
||||
SOCIAL_CALLBACK
|
||||
SOCIAL_CALLBACK,
|
||||
LOGGED_OUT
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<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) {
|
||||
|
|
23
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
Normal file → Executable file
23
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
Normal file → Executable file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ClientSessionModel> getClientSessions() {
|
||||
List<ClientSessionModel> clientSessionModels = new LinkedList<ClientSessionModel>();
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
17
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java
Normal file → Executable file
17
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/UserSessionEntity.java
Normal file → Executable file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,18 @@ public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEnt
|
|||
updateMongoEntity();
|
||||
}
|
||||
|
||||
@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>();
|
||||
|
@ -97,6 +109,23 @@ public class UserSessionAdapter extends AbstractMongoAdapter<MongoUserSessionEnt
|
|||
return sessions;
|
||||
}
|
||||
|
||||
@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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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("")) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue