Merge pull request #1087 from patriot1burke/master

broker backchannel logout
This commit is contained in:
Bill Burke 2015-03-24 16:58:22 -04:00
commit 82dd3cf8a4
5 changed files with 201 additions and 15 deletions

View file

@ -13,17 +13,22 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.saml.SAML2LogoutResponseBuilder;
import org.keycloak.protocol.saml.SAMLRequestParser;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.SignatureAlgorithm;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.EventsManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.flows.Flows;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLConstants;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil;
import org.picketlink.common.util.StaxParserUtil;
@ -38,6 +43,8 @@ import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.EncryptedAssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.assertion.SubjectType;
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
import org.picketlink.identity.federation.saml.v2.protocol.RequestAbstractType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType;
import org.w3c.dom.Document;
@ -53,8 +60,10 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
@ -68,6 +77,7 @@ import java.util.Map;
*/
public class SAMLEndpoint {
protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class);
public static final String SAML_FEDERATED_SESSION_INDEX = "SAML_FEDERATED_SESSION_INDEX";
protected RealmModel realm;
protected EventBuilder event;
protected SAMLIdentityProviderConfig config;
@ -161,22 +171,112 @@ public class SAMLEndpoint {
event = new EventsManager(realm, session, clientConnection).createEventBuilder();
Response response = basicChecks(samlRequest, samlResponse);
if (response != null) return response;
if (samlRequest != null) throw new RuntimeException("NOT IMPLEMETED");//return handleSamlRequest(samlRequest, relayState);
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
else return handleSamlResponse(samlResponse, relayState);
}
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
protected Response handleSamlRequest(String samlRequest, String relayState) {
SAMLDocumentHolder holder = extractRequestDocument(samlRequest);
RequestAbstractType requestAbstractType = (RequestAbstractType) holder.getSamlObject();
// validate destination
if (!uriInfo.getAbsolutePath().toString().equals(requestAbstractType.getDestination())) {
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
event.detail(Details.REASON, "invalid_destination");
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST);
}
if (config.isValidateSignature()) {
try {
verifySignature(holder);
} catch (VerificationException e) {
logger.error("validation failed", e);
event.event(EventType.LOGIN);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SIGNATURE);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUESTER);
}
}
if (requestAbstractType instanceof LogoutRequestType) {
logger.debug("** logout request");
event.event(EventType.LOGOUT);
LogoutRequestType logout = (LogoutRequestType) requestAbstractType;
return logoutRequest(logout, relayState);
} else {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST);
}
}
protected Response logoutRequest(LogoutRequestType request, String relayState) {
UserModel user = session.users().getUserByUsername(request.getNameID().getValue(), realm);
if (user == null) {
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
if (sessions == null || sessions.size() == 0) {
event.event(EventType.LOGOUT);
event.error(Errors.USER_SESSION_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
for (UserSessionModel userSession : sessions) {
String brokerId = userSession.getNote(IdentityBrokerService.BROKER_PROVIDER_ID);
if (!config.getAlias().equals(brokerId)) continue;
boolean logout = false;
if (request.getSessionIndex() == null || request.getSessionIndex().size() == 0) {
logout = true;
} else {
for (String sessionIndex : request.getSessionIndex()) {
if (sessionIndex.equals(userSession.getNote(SAML_FEDERATED_SESSION_INDEX))) {
logout = true;
break;
}
}
}
if (logout) {
try {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers);
} catch (Exception e) {
logger.error("Failed to logout", e);
}
}
String issuerURL = getEntityId(uriInfo, realm);
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(request.getID());
builder.destination(config.getSingleLogoutServiceUrl());
builder.issuer(issuerURL);
builder.relayState(relayState);
if (config.isWantAuthnRequestsSigned()) {
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signDocument();
}
try {
if (config.isPostBindingResponse()) {
return builder.postBinding().response();
} else {
return builder.redirectBinding().response();
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
} catch (ProcessingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
throw new RuntimeException("Unreachable");
}
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
}
protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
try {
AssertionType assertion = getAssertion(responseType);
SubjectType subject = assertion.getSubject();
@ -205,7 +305,7 @@ public class SAMLEndpoint {
}
}
if (authn != null && authn.getSessionIndex() != null) {
notes.put("SAML_FEDERATED_SESSION_INDEX", authn.getSessionIndex());
notes.put(SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
}
return callback.authenticated(notes, config, identity, relayState);
@ -241,7 +341,17 @@ public class SAMLEndpoint {
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SAML_RESPONSE);
event.detail(Details.REASON, "invalid_destination");
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
}
if (config.isValidateSignature()) {
try {
verifySignature(holder);
} catch (VerificationException e) {
logger.error("validation failed", e);
event.event(EventType.IDENTITY_PROVIDER_RESPONSE);
event.error(Errors.INVALID_SIGNATURE);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_FEDERATED_IDENTITY_ACTION);
}
}
if (statusResponse instanceof ResponseType) {
return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
@ -255,16 +365,6 @@ public class SAMLEndpoint {
}
protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) {
if (config.isValidateSignature()) {
try {
verifySignature(holder);
} catch (VerificationException e) {
logger.error("logout response validation failed", e);
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_SIGNATURE);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
}
}
if (relayState == null) {
logger.error("no valid user session");
event.event(EventType.LOGOUT);

View file

@ -654,10 +654,12 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.initSamlProvider = function() {
$scope.nameIdFormats = [
/*
{
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
name: "Transient"
},
*/
{
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
name: "Persistent"

View file

@ -3,6 +3,7 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -19,6 +20,7 @@ public interface UserSessionProvider extends Provider {
List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults);
int getActiveUserSessions(RealmModel realm, ClientModel client);
void removeUserSession(RealmModel realm, UserSessionModel session);
void removeUserSessions(RealmModel realm, UserModel user);

View file

@ -21,6 +21,7 @@ import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.util.Time;
@ -172,6 +173,16 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return userSessions;
}
public List<UserSessionModel> getUserSessionsByNote(RealmModel realm, Map<String, String> notes) {
Map<String, UserSessionEntity> sessions = new MapReduceTask(sessionCache)
.mappedWith(UserSessionNoteMapper.create(realm.getId()).notes(notes))
.reducedWith(new FirstResultReducer())
.execute();
return wrapUserSessions(realm, sessions.values());
}
@Override
public int getActiveUserSessions(RealmModel realm, ClientModel client) {
Map map = new MapReduceTask(sessionCache)

View file

@ -0,0 +1,71 @@
package org.keycloak.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionNoteMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public UserSessionNoteMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
private Map<String, String> notes;
public static UserSessionNoteMapper create(String realm) {
return new UserSessionNoteMapper(realm);
}
public UserSessionNoteMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
public UserSessionNoteMapper notes(Map<String, String> notes) {
this.notes = notes;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!(e instanceof UserSessionEntity)) {
return;
}
UserSessionEntity entity = (UserSessionEntity) e;
if (!realm.equals(entity.getRealm())) {
return;
}
for (Map.Entry<String, String> entry : notes.entrySet()) {
String note = entity.getNotes().get(entry.getKey());
if (note == null) return;
if (!note.equals(entry.getValue())) return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, entity);
break;
}
}
}