/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * https://opensso.dev.java.net/public/CDDLv1.0.html or * opensso/legal/CDDLv1.0.txt * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at opensso/legal/CDDLv1.0.txt. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * $Id: LogoutUtil.java,v 1.16 2009/11/20 21:41:16 exu Exp $ * * Portions Copyrighted 2012-2015 ForgeRock AS. */ package com.sun.identity.saml2.profile; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPMessage; import com.sun.identity.saml2.common.SOAPCommunicator; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.sun.identity.plugin.session.SessionException; import com.sun.identity.shared.encode.Base64; import com.sun.identity.shared.encode.URLEncDec; import com.sun.identity.shared.debug.Debug; import com.sun.identity.shared.xml.XMLUtils; import com.sun.identity.saml.common.SAMLUtils; import com.sun.identity.saml.xmlsig.KeyProvider; import com.sun.identity.saml2.assertion.EncryptedID; import com.sun.identity.saml2.assertion.Issuer; import com.sun.identity.saml2.assertion.NameID; import com.sun.identity.saml2.common.SAML2Constants; import com.sun.identity.saml2.common.SAML2Exception; import com.sun.identity.saml2.common.SAML2Utils; import com.sun.identity.saml2.jaxb.entityconfig.BaseConfigType; import com.sun.identity.saml2.jaxb.metadata.EndpointType; import com.sun.identity.saml2.jaxb.metadata.IDPSSODescriptorElement; import com.sun.identity.saml2.jaxb.metadata.KeyDescriptorType; import com.sun.identity.saml2.jaxb.metadata.SPSSODescriptorElement; import com.sun.identity.saml2.jaxb.metadata.SingleLogoutServiceElement; import com.sun.identity.saml2.key.EncInfo; import com.sun.identity.saml2.key.KeyUtil; import com.sun.identity.saml2.logging.LogUtil; import com.sun.identity.saml2.meta.SAML2MetaException; import com.sun.identity.saml2.meta.SAML2MetaManager; import com.sun.identity.saml2.meta.SAML2MetaUtils; import com.sun.identity.saml2.plugins.FedletAdapter; import com.sun.identity.saml2.protocol.Extensions; import com.sun.identity.saml2.protocol.LogoutRequest; import com.sun.identity.saml2.protocol.LogoutResponse; import com.sun.identity.saml2.protocol.ProtocolFactory; import com.sun.identity.saml2.protocol.SessionIndex; import com.sun.identity.saml2.protocol.Status; import com.sun.identity.saml2.protocol.StatusDetail; /** * This class constructs the LogoutRequest and executes * the required processing logic for sending LogoutRequest * from SP to IDP. */ public class LogoutUtil { static KeyProvider keyProvider = KeyUtil.getKeyProviderInstance(); static SAML2MetaManager metaManager = null; static Debug debug = SAML2Utils.debug; static { metaManager= SAML2Utils.getSAML2MetaManager(); } /** * Builds the LogoutRequest and executes * the required processing logic for sending LogoutRequest * from SP to IDP. * * @param metaAlias the requester's metaAlais. * @param recipientEntityID the recipient's entity ID. * @param recipientSLOList recipient's Single Logout Service location * URL list. * @param extensionsList Extension list for request. * @param binding binding used for this request. * @param relayState the target URL on successful Single Logout. * @param sessionIndex sessionIndex of the Assertion generated by the * Identity Provider or Service Provider. * @param nameID NameID of the Provider. * @param response the HttpServletResponse. * @param paramsMap Map of all other parameters. * Following parameters names with their respective * String values are allowed in this paramsMap. * "realm" - MetaAlias for Service Provider. The format of * this parameter is /realm_name/SP name. * "RelayState" - the target URL on successful Single Logout * "Destination" - A URI Reference indicating the address to * which the request has been sent. * "Consent" - Specifies a URI a SAML defined identifier * known as Consent Identifiers. * @param config entity base config for basic auth. * * @return Logout request ID * * @throws SAML2Exception if error initiating request to IDP. * @throws SessionException if error initiating request to IDP. */ public static StringBuffer doLogout( String metaAlias, String recipientEntityID, List recipientSLOList, List extensionsList, String binding, String relayState, String sessionIndex, NameID nameID, HttpServletRequest request, HttpServletResponse response, Map paramsMap, BaseConfigType config) throws SAML2Exception, SessionException { EndpointType logoutEndpoint = null; for (EndpointType endpoint : recipientSLOList) { if (binding.equals(endpoint.getBinding())) { logoutEndpoint = endpoint; break; } } return doLogout(metaAlias, recipientEntityID, extensionsList, logoutEndpoint, relayState, sessionIndex, nameID, request, response, paramsMap, config); } public static StringBuffer doLogout( String metaAlias, String recipientEntityID, List extensionsList, EndpointType logoutEndpoint, String relayState, String sessionIndex, NameID nameID, HttpServletRequest request, HttpServletResponse response, Map paramsMap, BaseConfigType config) throws SAML2Exception, SessionException { StringBuffer logoutRequestID = new StringBuffer(); String classMethod = "LogoutUtil.doLogout: "; String requesterEntityID = metaManager.getEntityByMetaAlias(metaAlias); String realm = SAML2MetaUtils.getRealmByMetaAlias(metaAlias); String hostEntityRole = SAML2Utils.getHostEntityRole(paramsMap); String location = null; String binding = null; if (logoutEndpoint != null) { location = logoutEndpoint.getLocation(); binding = logoutEndpoint.getBinding(); } else { debug.error(classMethod + "Unable to find the recipient's single logout service with the binding " + binding); throw new SAML2Exception(SAML2Utils.bundle.getString("sloServiceNotfound")); } if (debug.messageEnabled()) { debug.message(classMethod + "Entering ..." + "\nrequesterEntityID=" + requesterEntityID + "\nrecipientEntityID=" + recipientEntityID + "\nbinding=" + binding + "\nrelayState=" + relayState + "\nsessionIndex=" + sessionIndex); } // generate unique request ID String requestID = SAML2Utils.generateID(); if ((requestID == null) || (requestID.length() == 0)) { throw new SAML2Exception(SAML2Utils.bundle.getString("cannotGenerateID")); } // retrieve data from the params map // destinationURI required if message is signed. String destinationURI = SAML2Utils.getParameter(paramsMap, SAML2Constants.DESTINATION); String consent = SAML2Utils.getParameter(paramsMap, SAML2Constants.CONSENT); Extensions extensions = createExtensions(extensionsList); Issuer issuer = SAML2Utils.createIssuer(requesterEntityID); // construct LogoutRequest LogoutRequest logoutReq = null; try { logoutReq = ProtocolFactory.getInstance().createLogoutRequest(); } catch (Exception e) { debug.error(classMethod + "Unable to create LogoutRequest : ", e); throw new SAML2Exception(SAML2Utils.bundle.getString("errorCreatingLogoutRequest")); } // set required attributes / elements logoutReq.setID(requestID); logoutReq.setVersion(SAML2Constants.VERSION_2_0); logoutReq.setIssueInstant(new Date()); setNameIDForSLORequest(logoutReq, nameID, realm, requesterEntityID, hostEntityRole, recipientEntityID); // set optional attributes / elements logoutReq.setDestination(XMLUtils.escapeSpecialCharacters(destinationURI)); logoutReq.setConsent(consent); logoutReq.setIssuer(issuer); if (hostEntityRole.equals(SAML2Constants.IDP_ROLE)) { // use the assertion effective time (in seconds) int effectiveTime = SAML2Constants.ASSERTION_EFFECTIVE_TIME; String effectiveTimeStr = SAML2Utils.getAttributeValueFromSSOConfig(realm, requesterEntityID, SAML2Constants.IDP_ROLE, SAML2Constants.ASSERTION_EFFECTIVE_TIME_ATTRIBUTE); if (effectiveTimeStr != null) { try { effectiveTime = Integer.parseInt(effectiveTimeStr); if (SAML2Utils.debug.messageEnabled()) { SAML2Utils.debug.message(classMethod + "got effective time from config:" + effectiveTime); } } catch (NumberFormatException nfe) { SAML2Utils.debug.error(classMethod + "Failed to get assertion effective time from " + "IDP SSO config: ", nfe); effectiveTime = SAML2Constants.ASSERTION_EFFECTIVE_TIME; } } Date date = new Date(); date.setTime(date.getTime() + effectiveTime * 1000); logoutReq.setNotOnOrAfter(date); } if (extensions != null) { logoutReq.setExtensions(extensions); } if (sessionIndex != null) { List list = new ArrayList(); list.add(sessionIndex); logoutReq.setSessionIndex(list); } debug.message(classMethod + "Recipient's single logout service location = " + location); if (destinationURI == null || destinationURI.isEmpty()) { logoutReq.setDestination(XMLUtils.escapeSpecialCharacters(location)); } if (debug.messageEnabled()) { debug.message(classMethod + "SLO Request before signing : "); debug.message(logoutReq.toXMLString(true, true)); } if (binding.equals(SAML2Constants.HTTP_REDIRECT)) { try { doSLOByHttpRedirect(logoutReq.toXMLString(true, true), location, relayState, realm, requesterEntityID, hostEntityRole, recipientEntityID, response); logoutRequestID.append(requestID); String[] data = {location}; LogUtil.access(Level.INFO, LogUtil.REDIRECT_TO_IDP, data, null); } catch (Exception e) { debug.error("Exception :", e); throw new SAML2Exception(SAML2Utils.bundle.getString("errorRedirectingLogoutRequest")); } } else if (binding.equals(SAML2Constants.SOAP)) { logoutRequestID.append(requestID); signSLORequest(logoutReq, realm, requesterEntityID, hostEntityRole, recipientEntityID); if (debug.messageEnabled()) { debug.message(classMethod + "SLO Request after signing : "); debug.message(logoutReq.toXMLString(true, true)); } location = SAML2Utils.fillInBasicAuthInfo(config, location); doSLOBySOAP(requestID, logoutReq, location, realm, requesterEntityID, hostEntityRole, request, response); } else if (binding.equals(SAML2Constants.HTTP_POST)) { logoutRequestID.append(requestID); signSLORequest(logoutReq, realm, requesterEntityID, hostEntityRole, recipientEntityID); if (debug.messageEnabled()) { debug.message(classMethod + "SLO Request after signing : "); debug.message(logoutReq.toXMLString(true, true)); } doSLOByPOST(requestID, logoutReq.toXMLString(true, true), location, relayState, realm, requesterEntityID, hostEntityRole, response, request); } SPCache.logoutRequestIDHash.put(logoutRequestID.toString(), logoutReq); return logoutRequestID; } static private void doSLOByHttpRedirect( String sloRequestXMLString, String sloURL, String relayState, String realm, String hostEntity, String hostEntityRole, String remoteEntity, HttpServletResponse response) throws SAML2Exception, IOException { String method = "doSLOByHttpRedirect: "; // encode the xml string String encodedXML = SAML2Utils.encodeForRedirect(sloRequestXMLString); StringBuffer queryString = new StringBuffer().append(SAML2Constants.SAML_REQUEST) .append(SAML2Constants.EQUAL) .append(encodedXML); // spec states that the relay state MUST NOT exceed 80 // chanracters, need to have some means to pass it when it // exceeds 80 chars if ((relayState != null) && (relayState.length() > 0)) { String tmp = SAML2Utils.generateID(); if (hostEntityRole.equals(SAML2Constants.IDP_ROLE)) { IDPCache.relayStateCache.put(tmp, relayState); } else { SPCache.relayStateHash.put(tmp, new CacheObject(relayState)); } queryString.append("&").append(SAML2Constants.RELAY_STATE) .append("=").append(tmp); } boolean needToSign = false; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { needToSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, SAML2Constants.SP_ROLE); } else { needToSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, SAML2Constants.IDP_ROLE); } String signedQueryString = queryString.toString(); if (needToSign == true) { signedQueryString = SAML2Utils.signQueryString(signedQueryString, realm, hostEntity, hostEntityRole); } String redirectURL = sloURL + (sloURL.contains("?") ? "&" : "?") + signedQueryString; if (debug.messageEnabled()) { debug.message(method + "LogoutRequestXMLString : " + sloRequestXMLString); debug.message(method + "LogoutRedirectURL : " + sloURL); } response.sendRedirect(redirectURL); } /** * Performs SOAP logout, this method will send LogoutResuest to IDP using * SOAP binding, and process LogoutResponse. * @param requestID Request id. * @param sloRequest a string representation of LogoutRequest. * @param sloURL SOAP logout URL on IDP side. * @param realm a string representation of LogoutRequest. * @param hostEntity host entity is sending the request. * @param hostRole SOAP logout URL on IDP side. * @throws SAML2Exception if logout failed. * @throws SessionException if logout failed. */ private static void doSLOBySOAP( String requestID, LogoutRequest sloRequest, String sloURL, String realm, String hostEntity, String hostRole, HttpServletRequest request, HttpServletResponse response) throws SAML2Exception, SessionException { String sloRequestXMLString = sloRequest.toXMLString(true, true); if (debug.messageEnabled()) { debug.message("LogoutUtil.doSLOBySOAP : SLORequestXML: " + sloRequestXMLString + "\nSOAPURL : " + sloURL); } SOAPMessage resMsg = null; try { resMsg = SOAPCommunicator.getInstance().sendSOAPMessage(sloRequestXMLString, sloURL, true); } catch (SOAPException se) { debug.error("Unable to send SOAPMessage to IDP ", se); throw new SAML2Exception(se.getMessage()); } // get the LogoutResponse element from SOAP message Element respElem = SOAPCommunicator.getInstance().getSamlpElement(resMsg, "LogoutResponse"); LogoutResponse sloResponse = ProtocolFactory.getInstance() .createLogoutResponse(respElem); String userId = null; // invoke SPAdapter for preSingleLogoutProcess : SP initiated SOAP if ((hostRole != null) && hostRole.equals(SAML2Constants.SP_ROLE)) { userId = SPSingleLogout.preSingleLogoutProcess(hostEntity, realm, request, response, null, sloRequest, sloResponse, SAML2Constants.SOAP); } if (sloResponse == null) { debug.error("LogoutUtil.doSLOBySoap : null response"); throw new SAML2Exception(SAML2Utils.bundle.getString( "nullLogoutResponse")); } if (debug.messageEnabled()) { debug.message("LogoutUtil.doSLOBySOAP : " + "LogoutResponse without SOAP envelope:\n" + sloResponse.toXMLString()); } Issuer resIssuer = sloResponse.getIssuer(); String requestId = sloResponse.getInResponseTo(); SAML2Utils.verifyResponseIssuer( realm, hostEntity, resIssuer, requestId); String remoteEntityID = sloResponse.getIssuer().getValue(); verifySLOResponse(sloResponse, realm, remoteEntityID, hostEntity, hostRole); boolean success = checkSLOResponse(sloResponse, requestID); if (debug.messageEnabled()) { debug.message("Request success : " + success); } if (success == false) { if (SPCache.isFedlet) { FedletAdapter fedletAdapter = SAML2Utils.getFedletAdapterClass(hostEntity, realm); if (fedletAdapter != null) { fedletAdapter.onFedletSLOFailure( request, response, sloRequest, sloResponse, hostEntity, remoteEntityID, SAML2Constants.SOAP); } } throw new SAML2Exception(SAML2Utils.bundle.getString("sloFailed")); } else { // invoke SPAdapter for postSLOSuccess : SP inited SOAP if ((hostRole != null) && hostRole.equals(SAML2Constants.SP_ROLE)) { if (SPCache.isFedlet) { FedletAdapter fedletAdapter = SAML2Utils.getFedletAdapterClass(hostEntity, realm); if (fedletAdapter != null) { fedletAdapter.onFedletSLOSuccess( request, response, sloRequest, sloResponse, hostEntity, remoteEntityID, SAML2Constants.SOAP); } } else { SPSingleLogout.postSingleLogoutSuccess(hostEntity, realm, request, response, userId, sloRequest, sloResponse, SAML2Constants.SOAP); } } } } private static boolean checkSLOResponse(LogoutResponse sloResponse, String requestID) throws SAML2Exception { boolean success = false; String retCode = sloResponse.getStatus().getStatusCode().getValue(); if (retCode.equalsIgnoreCase(SAML2Constants.SUCCESS)) { String inResponseTo = sloResponse.getInResponseTo(); if (inResponseTo.equals(requestID)) { if (debug.messageEnabled()) { debug.message("LogoutUtil.doSLOBySOAP " + "LogoutResponse inResponseTo matches LogoutRequest ID"); } } else { debug.error("LogoutUtil.doSLOBySOAP " + "LogoutResponse inResponseTo does not match Request ID."); throw new SAML2Exception(SAML2Utils.bundle.getString( "inResponseToNoMatch")); } success = true; } else { if (debug.messageEnabled()) { debug.message("LogoutUtil.doSLOBySOAP : " + "return code : " + retCode); } success = false; } return success; } static LogoutResponse forwardToRemoteServer(LogoutRequest logoutReq, String remoteLogoutURL) { if (debug.messageEnabled()) { debug.message("LogoutUtil.forwardToRemoteServer: " + "remoteLogoutURL = " + remoteLogoutURL); } try { SOAPMessage resMsg = SOAPCommunicator.getInstance().sendSOAPMessage( logoutReq.toXMLString(true, true), remoteLogoutURL, true); // get the LogoutResponse element from SOAP message Element respElem = SOAPCommunicator.getInstance().getSamlpElement(resMsg, "LogoutResponse"); return ProtocolFactory.getInstance().createLogoutResponse(respElem); } catch (Exception ex) { if (debug.messageEnabled()) { debug.message("LogoutUtil.forwardToRemoteServer:", ex); } } return null; } static List getSessionIndex(LogoutResponse logoutRes) { StatusDetail statusDetail = logoutRes.getStatus().getStatusDetail(); if (statusDetail == null) { return null; } List details = statusDetail.getAny(); if (details == null || details.isEmpty()) { return null; } List sessionIndexList = new ArrayList(); for(Iterator iter = details.iterator(); iter.hasNext();) { String detail = (String)iter.next(); Document doc = XMLUtils.toDOMDocument(detail, debug); Element elem = doc.getDocumentElement(); String localName = elem.getLocalName(); if (SAML2Constants.SESSION_INDEX.equals(localName)) { sessionIndexList.add(XMLUtils.getElementString(elem)); } } return sessionIndexList; } static void setSessionIndex(Status status, List sessionIndex) { try { StatusDetail sd=ProtocolFactory.getInstance().createStatusDetail(); status.setStatusDetail(sd); if (sessionIndex != null && !sessionIndex.isEmpty()) { List details = new ArrayList(); for(Iterator iter = sessionIndex.iterator(); iter.hasNext();) { String si = (String)iter.next(); SessionIndex sIndex = ProtocolFactory.getInstance() .createSessionIndex(si); details.add(sIndex.toXMLString(true, true)); } sd.setAny(details); } } catch (SAML2Exception e) { debug.error("LogoutUtil.setSessionIndex: ", e); } } /** * Based on the preferred SAML binding this method tries to choose the most appropriate * {@link SingleLogoutServiceElement} that can be used to send the logout request to. The algorithm itself is * simple: * * * @param sloList The list of SLO endpoints for a given entity. * @param preferredBinding The binding that was used to initiate the logout request. * @return The most appropriate SLO service location that can be used for sending the logout request. If there is * no appropriate logout endpoint, null is returned. */ public static SingleLogoutServiceElement getMostAppropriateSLOServiceLocation( List sloList, String preferredBinding) { //shortcut for the case when SLO isn't supported at all if (sloList.isEmpty()) { return null; } Map sloBindings = new HashMap(sloList.size()); for (SingleLogoutServiceElement sloEndpoint : sloList) { sloBindings.put(sloEndpoint.getBinding(), sloEndpoint); } SingleLogoutServiceElement endpoint = sloBindings.get(preferredBinding); if (endpoint == null) { //if the requested binding isn't supported let's try to find the most appropriate SLO endpoint if (preferredBinding.equals(SAML2Constants.HTTP_POST)) { endpoint = sloBindings.get(SAML2Constants.HTTP_REDIRECT); } else if (preferredBinding.equals(SAML2Constants.HTTP_REDIRECT)) { endpoint = sloBindings.get(SAML2Constants.HTTP_POST); } if (endpoint == null) { //we ran out of asynchronous bindings, so our only chance is to try to use SOAP binding //in case the preferred binding was SOAP from the beginning, then this code will just return null again endpoint = sloBindings.get(SAML2Constants.SOAP); } } return endpoint; } /** * Gets Single Logout Service location URL. * * @param sloList list of configured SingleLogoutElement. * @param desiredBinding desired binding of SingleLogout. * @return url of desiredBinding. */ public static String getSLOServiceLocation( List sloList, String desiredBinding) { String classMethod = "LogoutUtil.getSLOserviceLocation: "; int n = sloList.size(); if (debug.messageEnabled()) { debug.message( classMethod + "Number of single logout services = " + n); } SingleLogoutServiceElement slos = null; String location = null; String binding = null; for (int i=0; iSingleLogoutElement. * @param desiredBinding desired binding of SingleLogout. * @return url of desiredBinding. */ public static String getSLOResponseServiceLocation( List sloList, String desiredBinding) { String classMethod = "LogoutUtil.getSLOResponseServiceLocation: "; int n = sloList.size(); if (debug.messageEnabled()) { debug.message( classMethod + "Number of single logout services = " + n); } SingleLogoutServiceElement slos = null; String resLocation = null; String binding = null; for (int i=0; iLogoutResponse to be sent to IDP. * * @param status status of the response. * @param inResponseTo inResponseTo. * @param issuer issuer of the response, which is SP. * @param realm inResponseTo. * @param hostRole issuer of the response, which is SP. * @param remoteEntity will get this response. * * @return LogoutResponse * */ public static LogoutResponse generateResponse( Status status, String inResponseTo, Issuer issuer, String realm, String hostRole, String remoteEntity) { if (status == null) { status = SAML2Utils.generateStatus(SAML2Constants.SUCCESS, SAML2Utils.bundle.getString("requestSuccess")); } LogoutResponse logoutResponse = ProtocolFactory.getInstance() .createLogoutResponse(); String responseID = SAMLUtils.generateID(); try { logoutResponse.setStatus(status); logoutResponse.setID(responseID); logoutResponse.setInResponseTo(inResponseTo); logoutResponse.setVersion(SAML2Constants.VERSION_2_0); logoutResponse.setIssueInstant(new Date()); logoutResponse.setIssuer(issuer); } catch (SAML2Exception e) { debug.error("Error in generating LogoutResponse.", e); } return logoutResponse; } /** * Sign LogoutRequest. * * @param sloRequest SLO request will be signed. * @param realm realm of host entity. * @param hostEntity entity ID of host entity. * @param hostEntityRole role of host entity. * @param remoteEntity entity ID of remote host entity. * @throws SAML2Exception if error in signing the request. */ public static void signSLORequest(LogoutRequest sloRequest, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception { signSLORequest(sloRequest, realm, hostEntity, hostEntityRole, remoteEntity, false); } static void signSLORequest(LogoutRequest sloRequest, String realm, String hostEntity, String hostEntityRole, String remoteEntity, boolean includeCert) throws SAML2Exception { String method = "signSLORequest : "; boolean needRequestSign = false; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { needRequestSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, SAML2Constants.SP_ROLE); } else { needRequestSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, SAML2Constants.IDP_ROLE); } if (needRequestSign == false) { if (debug.messageEnabled()) { debug.message(method + "SLORequest doesn't need to be signed."); } return; } String alias = SAML2Utils.getSigningCertAlias(realm, hostEntity, hostEntityRole); String encryptedKeyPass = SAML2Utils.getSigningCertEncryptedKeyPass(realm, hostEntity, hostEntityRole); if (debug.messageEnabled()) { debug.message(method + "realm is : "+ realm); debug.message(method + "hostEntity is : " + hostEntity); debug.message(method + "Host Entity role is : " + hostEntityRole); debug.message(method + "Cert Alias is : " + alias); debug.message(method + "SLO Request before sign : " + sloRequest.toXMLString(true, true)); if (encryptedKeyPass != null && !encryptedKeyPass.isEmpty()) { debug.message(method + "Using provided Cert KeyPass"); } } PrivateKey signingKey; if (encryptedKeyPass == null || encryptedKeyPass.isEmpty()) { signingKey = keyProvider.getPrivateKey(alias); } else { signingKey = keyProvider.getPrivateKey(alias, encryptedKeyPass); } X509Certificate signingCert = null; if (includeCert) { signingCert = keyProvider.getX509Certificate(alias); } if (signingKey != null) { sloRequest.sign(signingKey, signingCert); } else { debug.error("Incorrect configuration for Signing Certificate."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } if (debug.messageEnabled()) { debug.message(method + "SLO Request after sign : " + sloRequest.toXMLString(true, true)); } } /** * Verify the signature in LogoutRequest. * * @param sloRequest SLO request will be verified. * @param realm realm of host entity. * @param remoteEntity entity ID of remote host entity. * @param hostEntity entity ID of host entity. * @param hostEntityRole role of host entity. * @return returns true if signature is valid. * @throws SAML2Exception if error in verifying the signature. * @throws SessionException if error in verifying the signature. */ public static boolean verifySLORequest(LogoutRequest sloRequest, String realm, String remoteEntity, String hostEntity, String hostEntityRole) throws SAML2Exception, SessionException { String method = "verifySLORequest : "; boolean needVerifySignature = SAML2Utils.getWantLogoutRequestSigned(realm, hostEntity, hostEntityRole); if (needVerifySignature == false) { if (debug.messageEnabled()) { debug.message(method+"SLORequest doesn't need to be verified."); } return true; } if (debug.messageEnabled()) { debug.message(method + "realm is : "+ realm); debug.message(method + "remoteEntity is : " + remoteEntity); debug.message(method + "Host Entity role is : " + hostEntityRole); } boolean valid = false; Set signingCerts; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { SPSSODescriptorElement spSSODesc = metaManager.getSPSSODescriptor(realm, remoteEntity); signingCerts = KeyUtil.getVerificationCerts(spSSODesc, remoteEntity, SAML2Constants.SP_ROLE); } else { IDPSSODescriptorElement idpSSODesc = metaManager.getIDPSSODescriptor(realm, remoteEntity); signingCerts = KeyUtil.getVerificationCerts(idpSSODesc, remoteEntity, SAML2Constants.IDP_ROLE); } if (!signingCerts.isEmpty()) { valid = sloRequest.isSignatureValid(signingCerts); if (debug.messageEnabled()) { debug.message(method + "Signature is : " + valid); } } else { debug.error("Incorrect configuration for Signing Certificate."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } return valid; } /** * Sign LogoutResponse. * * @param sloResponse SLO response will be signed. * @param realm realm of host entity. * @param hostEntity entity ID of host entity. * @param hostEntityRole role of host entity. * @param remoteEntity entity ID of remote host entity. * @throws SAML2Exception if error in signing the request. */ public static void signSLOResponse(LogoutResponse sloResponse, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception { signSLOResponse(sloResponse, realm, hostEntity, hostEntityRole, remoteEntity, false); } static void signSLOResponse(LogoutResponse sloResponse, String realm, String hostEntity, String hostEntityRole, String remoteEntity, boolean includeCert) throws SAML2Exception { String method = "signSLOResponse : "; boolean needSignResponse = false; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { needSignResponse = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, SAML2Constants.SP_ROLE); } else { needSignResponse = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, SAML2Constants.IDP_ROLE); } if (needSignResponse == false) { if (debug.messageEnabled()) { debug.message(method + "SLOResponse doesn't need to be signed."); } return; } String alias = SAML2Utils.getSigningCertAlias(realm, hostEntity, hostEntityRole); String encryptedKeyPass = SAML2Utils.getSigningCertEncryptedKeyPass(realm, hostEntity, hostEntityRole); if (debug.messageEnabled()) { debug.message(method + "realm is : "+ realm); debug.message(method + "hostEntity is : " + hostEntity); debug.message(method + "Host Entity role is : " + hostEntityRole); debug.message(method + "Cert Alias is : " + alias); if (encryptedKeyPass != null && !encryptedKeyPass.isEmpty()) { debug.message(method + "Using provided Cert KeyPass"); } } PrivateKey signingKey; if (encryptedKeyPass == null || encryptedKeyPass.isEmpty()) { signingKey = keyProvider.getPrivateKey(alias); } else { signingKey = keyProvider.getPrivateKey(alias, encryptedKeyPass); } X509Certificate signingCert = null; if (includeCert) { signingCert = keyProvider.getX509Certificate(alias); } if (signingKey != null) { sloResponse.sign(signingKey, signingCert); } else { debug.error("Incorrect configuration for Signing Certificate."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } } /** * Verify the signature in LogoutResponse. * * @param sloResponse SLO response will be verified. * @param realm realm of host entity. * @param remoteEntity entity ID of remote host entity. * @param hostEntity entity ID of host entity. * @param hostEntityRole role of host entity. * @return returns true if signature is valid. * @throws SAML2Exception if error in verifying the signature. * @throws SessionException if error in verifying the signature. */ public static boolean verifySLOResponse(LogoutResponse sloResponse, String realm, String remoteEntity, String hostEntity, String hostEntityRole) throws SAML2Exception, SessionException { String method = "verifySLOResponse : "; boolean needVerifySignature = SAML2Utils.getWantLogoutResponseSigned(realm, hostEntity, hostEntityRole); if (needVerifySignature == false) { if (debug.messageEnabled()) { debug.message(method + "SLOResponse doesn't need to be verified."); } return true; } Set signingCerts; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { SPSSODescriptorElement spSSODesc = metaManager.getSPSSODescriptor(realm, remoteEntity); signingCerts = KeyUtil.getVerificationCerts(spSSODesc, remoteEntity, SAML2Constants.SP_ROLE); } else { IDPSSODescriptorElement idpSSODesc = metaManager.getIDPSSODescriptor(realm, remoteEntity); signingCerts = KeyUtil.getVerificationCerts(idpSSODesc, remoteEntity, SAML2Constants.IDP_ROLE); } if (!signingCerts.isEmpty()) { boolean valid = sloResponse.isSignatureValid(signingCerts); if (debug.messageEnabled()) { debug.message(method + "Signature is : " + valid); } return valid; } else { debug.error("Incorrect configuration for Signing Certificate."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } } public static void setNameIDForSLORequest(LogoutRequest request, NameID nameID, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception, SessionException { String method = "setNameIDForSLORequest: "; boolean needEncryptIt = false; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { needEncryptIt = SAML2Utils.getWantNameIDEncrypted(realm, remoteEntity, SAML2Constants.SP_ROLE); } else { needEncryptIt = SAML2Utils.getWantNameIDEncrypted(realm, remoteEntity, SAML2Constants.IDP_ROLE); } if (needEncryptIt == false) { if (debug.messageEnabled()) { debug.message(method + "NamID doesn't need to be encrypted."); } request.setNameID(nameID); return; } EncInfo encryptInfo = null; KeyDescriptorType keyDescriptor = null; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { SPSSODescriptorElement spSSODesc = metaManager.getSPSSODescriptor(realm, remoteEntity); keyDescriptor = KeyUtil.getKeyDescriptor(spSSODesc, "encryption"); encryptInfo = KeyUtil.getEncInfo(spSSODesc, remoteEntity, SAML2Constants.SP_ROLE); } else { IDPSSODescriptorElement idpSSODesc = metaManager.getIDPSSODescriptor(realm, remoteEntity); keyDescriptor = KeyUtil.getKeyDescriptor(idpSSODesc, "encryption"); encryptInfo = KeyUtil.getEncInfo(idpSSODesc, remoteEntity, SAML2Constants.IDP_ROLE); } if (debug.messageEnabled()) { debug.message(method + "realm is : "+ realm); debug.message(method + "hostEntity is : " + hostEntity); debug.message(method + "Host Entity role is : " + hostEntityRole); debug.message(method + "remoteEntity is : " + remoteEntity); } if (encryptInfo == null) { debug.error("NO meta data for encrypt Info."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } X509Certificate certificate = KeyUtil.getCert(keyDescriptor); PublicKey recipientPublicKey = certificate.getPublicKey(); EncryptedID encryptedID = nameID.encrypt(recipientPublicKey, encryptInfo.getDataEncAlgorithm(), encryptInfo.getDataEncStrength(), remoteEntity); request.setEncryptedID(encryptedID); } static NameID getNameIDFromSLORequest(LogoutRequest request, String realm, String hostEntity, String hostEntityRole) throws SAML2Exception { String method = "getNameIDFromSLORequest: "; boolean needDecryptIt = SAML2Utils.getWantNameIDEncrypted(realm,hostEntity,hostEntityRole); if (needDecryptIt == false) { if (debug.messageEnabled()) { debug.message(method + "NamID doesn't need to be decrypted."); } return request.getNameID(); } if (debug.messageEnabled()) { debug.message(method + "realm is : "+ realm); debug.message(method + "hostEntity is : " + hostEntity); debug.message(method + "Host Entity role is : " + hostEntityRole); } EncryptedID encryptedID = request.getEncryptedID(); return encryptedID.decrypt(KeyUtil.getDecryptionKeys(realm, hostEntity, hostEntityRole)); } public static void sendSLOResponse(HttpServletResponse response, LogoutResponse sloResponse, String sloURL, String relayState, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception { sendSLOResponseRedirect(response, sloResponse, sloURL, relayState, realm, hostEntity, hostEntityRole, remoteEntity); } public static void sendSLOResponse(HttpServletResponse response, HttpServletRequest request, LogoutResponse sloResponse, String sloURL, String relayState, String realm, String hostEntity, String hostEntityRole, String remoteEntity, String binding) throws SAML2Exception { if (SAML2Constants.HTTP_POST.equals(binding)) { sendSLOResponsePost(response, request, sloResponse, sloURL, relayState, realm, hostEntity, hostEntityRole, remoteEntity); } else { sendSLOResponseRedirect(response, sloResponse, sloURL, relayState, realm, hostEntity, hostEntityRole, remoteEntity); } } public static void sendSLOResponsePost(HttpServletResponse response, HttpServletRequest request, LogoutResponse sloResponse, String sloURL, String relayState, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception { signSLOResponse(sloResponse, realm, hostEntity, hostEntityRole, remoteEntity); String logoutResponseStr = sloResponse.toXMLString(true,true); String encMsg = SAML2Utils.encodeForPOST(logoutResponseStr); SAML2Utils.postToTarget(request, response, "SAMLResponse", encMsg, "RelayState", relayState, sloURL); } public static void sendSLOResponseRedirect(HttpServletResponse response, LogoutResponse sloResponse, String sloURL, String relayState, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception { try { String logoutResXMLString = sloResponse.toXMLString(true,true); // encode the xml string String encodedXML = SAML2Utils.encodeForRedirect(logoutResXMLString); StringBuffer queryString = new StringBuffer().append(SAML2Constants.SAML_RESPONSE) .append(SAML2Constants.EQUAL) .append(encodedXML); if (relayState != null && relayState.length() > 0 && relayState.getBytes("UTF-8").length <= 80) { queryString.append("&").append(SAML2Constants.RELAY_STATE) .append("=").append(URLEncDec.encode(relayState)); } boolean needToSign = false; if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)) { needToSign = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, SAML2Constants.SP_ROLE); } else { needToSign = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, SAML2Constants.IDP_ROLE); } String signedQueryString = queryString.toString(); if (needToSign == true) { signedQueryString = SAML2Utils.signQueryString(signedQueryString, realm, hostEntity, hostEntityRole); } String redirectURL = sloURL+ (sloURL.contains("?") ? "&" : "?") + signedQueryString; if (debug.messageEnabled()) { debug.message("redirectURL :" + redirectURL); } String[] data = {sloURL}; LogUtil.access(Level.INFO,LogUtil.REDIRECT_TO_SP,data, null); response.sendRedirect(redirectURL); } catch (Exception e) { debug.error("Exception :",e); throw new SAML2Exception(SAML2Utils.bundle.getString( "errorRedirectingLogoutResponse")); } } /** * Returns binding information of SLO Service for remote entity * from request or meta configuration. * * @param request the HttpServletRequest. * @param metaAlias entityID of hosted entity. * @param hostEntityRole Role of hosted entity. * @param remoteEntityID entityID of remote entity. * @return return true if the processing is successful. * @throws SAML2Exception if no binding information is configured. */ public static String getSLOBindingInfo(HttpServletRequest request, String metaAlias, String hostEntityRole, String remoteEntityID) throws SAML2Exception { String binding = request.getParameter(SAML2Constants.BINDING); try { if (binding == null) { String realm = SAML2MetaUtils.getRealmByMetaAlias(metaAlias); SingleLogoutServiceElement sloService = getSLOServiceElement(realm, remoteEntityID, hostEntityRole, null); if (sloService != null) { binding = sloService.getBinding(); } } } catch (SessionException e) { debug.error("Invalid SSOToken", e); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } if (binding == null) { debug.error("Incorrect configuration for SingleLogout Service."); throw new SAML2Exception( SAML2Utils.bundle.getString("metaDataError")); } return binding; } private static SingleLogoutServiceElement getSLOServiceElement( String realm, String entityID, String hostEntityRole, String binding) throws SAML2MetaException, SessionException, SAML2Exception { SingleLogoutServiceElement sloService = null; String method = "getSLOServiceElement: "; if (debug.messageEnabled()) { debug.message(method + "Realm : " + realm); debug.message(method + "Entity ID : " + entityID); debug.message(method + "Host Entity Role : " + hostEntityRole); } if (hostEntityRole.equalsIgnoreCase(SAML2Constants.SP_ROLE)) { sloService = getIDPSLOConfig(realm, entityID, binding); } else if (hostEntityRole.equalsIgnoreCase(SAML2Constants.IDP_ROLE)){ sloService = getSPSLOConfig(realm, entityID, binding); } else { debug.error("Hosted Entity role is missing ."); throw new SAML2Exception( SAML2Utils.bundle.getString("nullIDPEntityID")); } return sloService; } /** * Returns first SingleLogout configuration in an entity under * the realm. * @param realm The realm under which the entity resides. * @param entityId ID of the entity to be retrieved. * @param binding bind type need to has to be matched. * @return SingleLogoutServiceElement for the entity or null * @throws SAML2MetaException if unable to retrieve the first identity * provider's SSO configuration. * @throws SessionException invalid or expired single-sign-on session */ static public SingleLogoutServiceElement getIDPSLOConfig( String realm, String entityId, String binding) throws SAML2MetaException, SessionException { SingleLogoutServiceElement slo = null; IDPSSODescriptorElement idpSSODesc = metaManager.getIDPSSODescriptor(realm, entityId); if (idpSSODesc == null) { debug.error("Identity Provider SSO config is missing."); return null; } List list = idpSSODesc.getSingleLogoutService(); if ((list != null) && !list.isEmpty()) { if (binding == null) { return (SingleLogoutServiceElement)list.get(0); } Iterator it = list.iterator(); while (it.hasNext()) { slo = (SingleLogoutServiceElement)it.next(); if (binding.equalsIgnoreCase(slo.getBinding())) { break; } } } return slo; } /** * Returns first SingleLogout configuration in an entity under * the realm. * @param realm The realm under which the entity resides. * @param entityId ID of the entity to be retrieved. * @param binding bind type need to has to be matched. * @return SingleLogoutServiceElement for the entity or null * @throws SAML2MetaException if unable to retrieve the first identity * provider's SSO configuration. * @throws SessionException invalid or expired single-sign-on session */ static public SingleLogoutServiceElement getSPSLOConfig( String realm, String entityId, String binding) throws SAML2MetaException, SessionException { SingleLogoutServiceElement slo = null; SPSSODescriptorElement spSSODesc = metaManager.getSPSSODescriptor(realm, entityId); if (spSSODesc == null) { return null; } List list = spSSODesc.getSingleLogoutService(); if ((list != null) && !list.isEmpty()) { if (binding == null) { return (SingleLogoutServiceElement)list.get(0); } Iterator it = list.iterator(); while (it.hasNext()) { slo = (SingleLogoutServiceElement)it.next(); if (binding.equalsIgnoreCase(slo.getBinding())) { break; } } } return slo; } /** * Returns the extensions list * @param paramsMap request paramsMap has extensions * @return List for extensions params */ public static List getExtensionsList(Map paramsMap) { List extensionsList = null; // TODO Get the Extensions list from request parameter return extensionsList; } private static void doSLOByPOST(String requestID, String sloRequestXMLString, String sloURL, String relayState, String realm, String hostEntity, String hostRole, HttpServletResponse response, HttpServletRequest request) throws SAML2Exception, SessionException { if (debug.messageEnabled()) { debug.message("LogoutUtil.doSLOByPOST : SLORequestXML: " + sloRequestXMLString + "\nPOSTURL : " + sloURL); debug.message("LogoutUtil.doSLOByPOST : relayState : " + sloRequestXMLString + "\nPOSTURL : " + relayState); } String encMsg = SAML2Utils.encodeForPOST(sloRequestXMLString); SAML2Utils.postToTarget(request, response, "SAMLRequest", encMsg, "RelayState", relayState, sloURL); } static LogoutResponse getLogoutResponseFromPost(String samlResponse, HttpServletResponse response) throws SAML2Exception { if (samlResponse == null) { throw new SAML2Exception(SAML2Utils.bundle.getString( "missingLogoutResponse")); } LogoutResponse resp = null; ByteArrayInputStream bis = null; try { byte[] raw = Base64.decode(samlResponse); if (raw != null) { bis = new ByteArrayInputStream(raw); Document doc = XMLUtils.toDOMDocument(bis, debug); if (doc != null) { resp = ProtocolFactory.getInstance(). createLogoutResponse(doc.getDocumentElement()); } } } catch (Exception e) { debug.error("LogoutUtil.getLogoutResponseFromPost:", e); } finally { if (bis != null) { try { bis.close(); } catch (Exception ie) { if (debug.messageEnabled()) { debug.message("LogoutUtil.getLogoutResponseFromPost:", ie); } } } } if (resp == null) { throw new SAML2Exception(SAML2Utils.bundle.getString( "errorGettingLogoutResponse")); } return resp; } static LogoutRequest getLogoutRequestFromPost(String samlRequest, HttpServletResponse response) throws SAML2Exception { if (samlRequest == null) { throw new SAML2Exception(SAML2Utils.bundle.getString( "missingLogoutRequest")); } LogoutRequest req = null; ByteArrayInputStream bis = null; try { byte[] raw = Base64.decode(samlRequest); if (raw != null) { bis = new ByteArrayInputStream(raw); Document doc = XMLUtils.toDOMDocument(bis, debug); if (doc != null) { req = ProtocolFactory.getInstance(). createLogoutRequest(doc.getDocumentElement()); } } } catch (Exception e) { debug.error("LogoutUtil.getLogoutRequestFromPost:", e); } finally { if (bis != null) { try { bis.close(); } catch (Exception ie) { if (debug.messageEnabled()) { debug.message("LogoutUtil.getLogoutRequestFromPost:", ie); } } } } if (req == null) { throw new SAML2Exception(SAML2Utils.bundle.getString( "errorGettingLogoutRequest")); } return req; } }