From 71de12b446816afd4d93525f31e694af4b712925 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 24 Mar 2015 16:03:46 -0400 Subject: [PATCH] broker backchannel logout --- .../keycloak/broker/saml/SAMLEndpoint.java | 130 ++++++++++++++++-- .../admin/resources/js/controllers/realm.js | 2 + .../keycloak/models/UserSessionProvider.java | 2 + .../InfinispanUserSessionProvider.java | 11 ++ .../mapreduce/UserSessionNoteMapper.java | 71 ++++++++++ 5 files changed, 201 insertions(+), 15 deletions(-) mode change 100644 => 100755 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java create mode 100755 model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index 849b2b54ac..23299bde23 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -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 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); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 7493e66f5c..e2d089abd6 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -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" diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java index a316f6fc48..56d433562a 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -3,6 +3,7 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import java.util.List; +import java.util.Map; /** * @author Bill Burke @@ -19,6 +20,7 @@ public interface UserSessionProvider extends Provider { List getUserSessions(RealmModel realm, UserModel user); List getUserSessions(RealmModel realm, ClientModel client); List 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); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java old mode 100644 new mode 100755 index 42dc3aa356..85d7a604ca --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -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 getUserSessionsByNote(RealmModel realm, Map notes) { + Map 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) diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java new file mode 100755 index 0000000000..4a235b550f --- /dev/null +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/UserSessionNoteMapper.java @@ -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 Stian Thorgersen + */ +public class UserSessionNoteMapper implements Mapper, Serializable { + + public UserSessionNoteMapper(String realm) { + this.realm = realm; + } + + private enum EmitValue { + KEY, ENTITY + } + + private String realm; + + private EmitValue emit = EmitValue.ENTITY; + private Map notes; + + public static UserSessionNoteMapper create(String realm) { + return new UserSessionNoteMapper(realm); + } + + public UserSessionNoteMapper emitKey() { + emit = EmitValue.KEY; + return this; + } + + public UserSessionNoteMapper notes(Map 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 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; + } + } + +}