X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fhandler%2Fimpl%2FSAML2SessionInitiator.cpp;h=6556f0a48422b2f7c00567053e5699a94fbdbfa0;hb=db3dd0f8110b3daa89123ef9994dc86121a92b31;hp=600f45eda5dea6bea8a09bda6a27130f9e447a51;hpb=d0d35cedf14334057f56b2c2293c4cdabe2fefa5;p=shibboleth%2Fsp.git diff --git a/shibsp/handler/impl/SAML2SessionInitiator.cpp b/shibsp/handler/impl/SAML2SessionInitiator.cpp index 600f45e..6556f0a 100644 --- a/shibsp/handler/impl/SAML2SessionInitiator.cpp +++ b/shibsp/handler/impl/SAML2SessionInitiator.cpp @@ -30,20 +30,22 @@ #include "handler/SessionInitiator.h" #include "util/SPConstants.h" -#include -#include -#include -#include -#include -#include - -using namespace shibsp; +#ifndef SHIBSP_LITE +# include +# include +# include +# include +# include using namespace opensaml::saml2; using namespace opensaml::saml2p; using namespace opensaml::saml2md; +#else +#include +#endif + +using namespace shibsp; using namespace opensaml; using namespace xmltooling; -using namespace log4cpp; using namespace std; namespace shibsp { @@ -58,10 +60,13 @@ namespace shibsp { public: SAML2SessionInitiator(const DOMElement* e, const char* appId); virtual ~SAML2SessionInitiator() { +#ifndef SHIBSP_LITE if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { XMLString::release(&m_outgoing); for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair()); + delete m_requestTemplate; } +#endif } void setParent(const PropertySet* parent); @@ -74,15 +79,27 @@ namespace shibsp { HTTPResponse& httpResponse, const char* entityID, const XMLCh* acsIndex, - const XMLCh* acsLocation, + const char* acsLocation, const XMLCh* acsBinding, + bool isPassive, + bool forceAuthn, + const char* authnContextClassRef, + const char* authnContextComparison, string& relayState ) const; string m_appId; + auto_ptr_char m_paosNS,m_ecpNS; + auto_ptr_XMLCh m_paosBinding; +#ifndef SHIBSP_LITE XMLCh* m_outgoing; vector m_bindings; map m_encoders; + MessageEncoder* m_ecp; + AuthnRequest* m_requestTemplate; +#else + bool m_ecp; +#endif }; #if defined (_MSC_VER) @@ -97,26 +114,47 @@ namespace shibsp { }; SAML2SessionInitiator::SAML2SessionInitiator(const DOMElement* e, const char* appId) - : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".SessionInitiator")), m_appId(appId), m_outgoing(NULL) + : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".SessionInitiator.SAML2")), m_appId(appId), + m_paosNS(samlconstants::PAOS_NS), m_ecpNS(samlconstants::SAML20ECP_NS), m_paosBinding(samlconstants::SAML20_BINDING_PAOS) { - // If Location isn't set, defer address registration until the setParent call. - pair loc = getString("Location"); - if (loc.first) { - string address = m_appId + loc.second + "::run::SAML2SI"; - setAddress(address.c_str()); - } + static const XMLCh ECP[] = UNICODE_LITERAL_3(E,C,P); + const XMLCh* flag = e ? e->getAttributeNS(NULL,ECP) : NULL; +#ifdef SHIBSP_LITE + m_ecp = (flag && (*flag == chLatin_t || *flag == chDigit_1)); +#else + m_outgoing=NULL; + m_ecp = NULL; + m_requestTemplate=NULL; if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) { + // Check for a template AuthnRequest to build from. + DOMElement* child = XMLHelper::getFirstChildElement(e, samlconstants::SAML20P_NS, AuthnRequest::LOCAL_NAME); + if (child) + m_requestTemplate = dynamic_cast(AuthnRequestBuilder::buildOneFromElement(child)); + + // If directed, build an ECP encoder. + if (flag && (*flag == chLatin_t || *flag == chDigit_1)) { + try { + m_ecp = SAMLConfig::getConfig().MessageEncoderManager.newPlugin( + samlconstants::SAML20_BINDING_PAOS, pair(e,NULL) + ); + } + catch (exception& ex) { + m_log.error("error building PAOS/ECP MessageEncoder: %s", ex.what()); + } + } + + // Handle outgoing binding setup. pair outgoing = getXMLString("outgoingBindings"); if (outgoing.first) { m_outgoing = XMLString::replicate(outgoing.second); + XMLString::trim(m_outgoing); } else { // No override, so we'll install a default binding precedence. string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' + samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT; m_outgoing = XMLString::transcode(prec.c_str()); - XMLString::trim(m_outgoing); } int pos; @@ -128,9 +166,11 @@ SAML2SessionInitiator::SAML2SessionInitiator(const DOMElement* e, const char* ap m_bindings.push_back(start); try { auto_ptr_char b(start); - MessageEncoder * encoder = SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),e); + MessageEncoder * encoder = SAMLConfig::getConfig().MessageEncoderManager.newPlugin( + b.get(),pair(e,NULL) + ); m_encoders[start] = encoder; - m_log.info("supporting outgoing binding (%s)", b.get()); + m_log.debug("supporting outgoing binding (%s)", b.get()); } catch (exception& ex) { m_log.error("error building MessageEncoder: %s", ex.what()); @@ -141,6 +181,14 @@ SAML2SessionInitiator::SAML2SessionInitiator(const DOMElement* e, const char* ap break; } } +#endif + + // If Location isn't set, defer address registration until the setParent call. + pair loc = getString("Location"); + if (loc.first) { + string address = m_appId + loc.second + "::run::SAML2SI"; + setAddress(address.c_str()); + } } void SAML2SessionInitiator::setParent(const PropertySet* parent) @@ -158,45 +206,136 @@ void SAML2SessionInitiator::setParent(const PropertySet* parent) pair SAML2SessionInitiator::run(SPRequest& request, const char* entityID, bool isHandler) const { - // We have to know the IdP to function. - if (!entityID || !*entityID) - return make_pair(false,0); + // First check for ECP support, since that doesn't require an IdP to be known. + bool ECP = false; + if (m_ecp && request.getHeader("Accept").find("application/vnd.paos+xml") != string::npos) { + string PAOS = request.getHeader("PAOS"); + if (PAOS.find(m_paosNS.get()) != string::npos && PAOS.find(m_ecpNS.get()) != string::npos) + ECP = true; + } + + // We have to know the IdP to function unless this is ECP. + if (!ECP && (!entityID || !*entityID)) + return make_pair(false,0L); string target; const Handler* ACS=NULL; const char* option; + pair acClass; + pair acComp; + bool isPassive=false,forceAuthn=false; const Application& app=request.getApplication(); - pair acsByIndex = getBool("acsByIndex"); + + // ECP means the ACS will be by value no matter what. + pair acsByIndex = ECP ? make_pair(true,false) : getBool("acsByIndex"); if (isHandler) { option=request.getParameter("acsIndex"); - if (option) + if (option) { ACS = app.getAssertionConsumerServiceByIndex(atoi(option)); + if (!ACS) + request.log(SPRequest::SPWarn, "invalid acsIndex specified in request, using default ACS location"); + else if (ECP && !XMLString::equals(ACS->getString("Binding").second, samlconstants::SAML20_BINDING_PAOS)) { + request.log(SPRequest::SPWarn, "acsIndex in request referenced a non-PAOS ACS, using default ACS location"); + ACS = NULL; + } + } option = request.getParameter("target"); if (option) target = option; - if (!acsByIndex.first || !acsByIndex.second) { + if (acsByIndex.first && !acsByIndex.second) { // Since we're passing the ACS by value, we need to compute the return URL, // so we'll need the target resource for real. - recoverRelayState(request.getApplication(), request, target, false); + recoverRelayState(request.getApplication(), request, request, target, false); + } + + pair flag; + option = request.getParameter("isPassive"); + if (option) { + isPassive = (*option=='1' || *option=='t'); + } + else { + flag = getBool("isPassive"); + isPassive = (flag.first && flag.second); } + if (!isPassive) { + option = request.getParameter("forceAuthn"); + if (option) { + forceAuthn = (*option=='1' || *option=='t'); + } + else { + flag = getBool("forceAuthn"); + forceAuthn = (flag.first && flag.second); + } + } + + if (acClass.second = request.getParameter("authnContextClassRef")) + acClass.first = true; + else + acClass = getString("authnContextClassRef"); + + if (acComp.second = request.getParameter("authnContextComparison")) + acComp.first = true; + else + acComp = getString("authnContextComparison"); } else { // We're running as a "virtual handler" from within the filter. // The target resource is the current one and everything else is defaulted. target=request.getRequestURL(); + const PropertySet* settings = request.getRequestSettings().first; + + pair flag = settings->getBool("isPassive"); + if (!flag.first) + flag = getBool("isPassive"); + isPassive = flag.first && flag.second; + if (!isPassive) { + flag = settings->getBool("forceAuthn"); + if (!flag.first) + flag = getBool("forceAuthn"); + forceAuthn = flag.first && flag.second; + } + + acClass = settings->getString("authnContextClassRef"); + if (!acClass.first) + acClass = getString("authnContextClassRef"); + acComp = settings->getString("authnContextComparison"); + if (!acComp.first) + acComp = getString("authnContextComparison"); } - m_log.debug("attempting to initiate session using SAML 2.0 with provider (%s)", entityID); + if (ECP) + m_log.debug("attempting to initiate session using SAML 2.0 Enhanced Client Profile"); + else + m_log.debug("attempting to initiate session using SAML 2.0 with provider (%s)", entityID); + + if (!ACS) { + if (ECP) { + const vector& handlers = app.getAssertionConsumerServicesByBinding(m_paosBinding.get()); + if (handlers.empty()) + throw ConfigurationException("Unable to locate PAOS response endpoint."); + ACS = handlers.front(); + } + else { + pair index = getUnsignedInt("defaultACSIndex"); + if (index.first) { + ACS = app.getAssertionConsumerServiceByIndex(index.second); + if (!ACS) + request.log(SPRequest::SPWarn, "invalid defaultACSIndex, using default ACS location"); + } + if (!ACS) + ACS = app.getDefaultAssertionConsumerService(); + } + } - // To invoke the request builder, the key requirement is to figure out how and whether + // To invoke the request builder, the key requirement is to figure out how // to express the ACS, by index or value, and if by value, where. SPConfig& conf = SPConfig::getConfig(); if (conf.isEnabled(SPConfig::OutOfProcess)) { - if (acsByIndex.first && acsByIndex.second) { - // Pass by Index. This also allows for defaulting it entirely and sending nothing. + if (!acsByIndex.first || acsByIndex.second) { + // Pass by Index. if (isHandler) { // We may already have RelayState set if we looped back here, // but just in case target is a resource, we reset it back. @@ -205,13 +344,17 @@ pair SAML2SessionInitiator::run(SPRequest& request, const char* entit if (option) target = option; } - return doRequest(app, request, entityID, ACS ? ACS->getXMLString("index").second : NULL, NULL, NULL, target); + return doRequest( + app, request, entityID, + ACS ? ACS->getXMLString("index").second : NULL, NULL, NULL, + isPassive, forceAuthn, + acClass.first ? acClass.second : NULL, + acComp.first ? acComp.second : NULL, + target + ); } // Since we're not passing by index, we need to fully compute the return URL and binding. - if (!ACS) - ACS = app.getDefaultAssertionConsumerService(); - // Compute the ACS URL. We add the ACS location to the base handlerURL. string ACSloc=request.getHandlerURL(target.c_str()); pair loc=ACS ? ACS->getString("Location") : pair(false,NULL); @@ -226,24 +369,36 @@ pair SAML2SessionInitiator::run(SPRequest& request, const char* entit target = option; } - auto_ptr_XMLCh wideloc(ACSloc.c_str()); - return doRequest(app, request, entityID, NULL, wideloc.get(), ACS ? ACS->getXMLString("Binding").second : NULL, target); + return doRequest( + app, request, entityID, + NULL, ACSloc.c_str(), ACS ? ACS->getXMLString("Binding").second : NULL, + isPassive, forceAuthn, + acClass.first ? acClass.second : NULL, + acComp.first ? acComp.second : NULL, + target + ); } // Remote the call. DDF out,in = DDF(m_address.c_str()).structure(); DDFJanitor jin(in), jout(out); in.addmember("application_id").string(app.getId()); - in.addmember("entity_id").string(entityID); - if (acsByIndex.first && acsByIndex.second) { + if (entityID) + in.addmember("entity_id").string(entityID); + if (isPassive) + in.addmember("isPassive").integer(1); + else if (forceAuthn) + in.addmember("forceAuthn").integer(1); + if (acClass.first) + in.addmember("authnContextClassRef").string(acClass.second); + if (acComp.first) + in.addmember("authnContextComparison").string(acComp.second); + if (!acsByIndex.first || acsByIndex.second) { if (ACS) in.addmember("acsIndex").string(ACS->getString("index").second); } else { // Since we're not passing by index, we need to fully compute the return URL and binding. - if (!ACS) - ACS = app.getDefaultAssertionConsumerService(); - // Compute the ACS URL. We add the ACS location to the base handlerURL. string ACSloc=request.getHandlerURL(target.c_str()); pair loc=ACS ? ACS->getString("Location") : pair(false,NULL); @@ -280,10 +435,6 @@ void SAML2SessionInitiator::receive(DDF& in, ostream& out) throw ConfigurationException("Unable to locate application for new session, deleted?"); } - const char* entityID = in["entity_id"].string(); - if (!entityID) - throw ConfigurationException("No entityID parameter supplied to remoted SessionInitiator."); - DDF ret(NULL); DDFJanitor jout(ret); @@ -291,7 +442,6 @@ void SAML2SessionInitiator::receive(DDF& in, ostream& out) auto_ptr http(getResponse(ret)); auto_ptr_XMLCh index(in["acsIndex"].string()); - auto_ptr_XMLCh loc(in["acsLocation"].string()); auto_ptr_XMLCh bind(in["acsBinding"].string()); string relayState(in["RelayState"].string() ? in["RelayState"].string() : ""); @@ -299,105 +449,176 @@ void SAML2SessionInitiator::receive(DDF& in, ostream& out) // Since we're remoted, the result should either be a throw, which we pass on, // a false/0 return, which we just return as an empty structure, or a response/redirect, // which we capture in the facade and send back. - doRequest(*app, *http.get(), entityID, index.get(), loc.get(), bind.get(), relayState); + doRequest( + *app, *http.get(), in["entity_id"].string(), + index.get(), in["acsLocation"].string(), bind.get(), + in["isPassive"].integer()==1, in["forceAuthn"].integer()==1, + in["authnContextClassRef"].string(), in["authnContextComparison"].string(), + relayState + ); out << ret; } +#ifndef SHIBSP_LITE +namespace { + class _sameIdP : public binary_function + { + public: + bool operator()(const IDPEntry* entry, const XMLCh* entityID) const { + return entry ? XMLString::equals(entry->getProviderID(), entityID) : false; + } + }; +}; +#endif + pair SAML2SessionInitiator::doRequest( const Application& app, HTTPResponse& httpResponse, const char* entityID, const XMLCh* acsIndex, - const XMLCh* acsLocation, + const char* acsLocation, const XMLCh* acsBinding, + bool isPassive, + bool forceAuthn, + const char* authnContextClassRef, + const char* authnContextComparison, string& relayState ) const { - // Use metadata to locate the IdP's SSO service. +#ifndef SHIBSP_LITE + bool ECP = XMLString::equals(acsBinding, m_paosBinding.get()); + + pair entity = pair(NULL,NULL); + const IDPSSODescriptor* role = NULL; + const EndpointType* ep = NULL; + const MessageEncoder* encoder = NULL; + + // We won't need this for ECP, but safety dictates we get the lock here. MetadataProvider* m=app.getMetadataProvider(); Locker locker(m); - const EntityDescriptor* entity=m->getEntityDescriptor(entityID); - if (!entity) { - m_log.error("unable to locate metadata for provider (%s)", entityID); - return make_pair(false,0); - } - const IDPSSODescriptor* role=entity->getIDPSSODescriptor(samlconstants::SAML20P_NS); - if (!role) { - m_log.error("unable to locate SAML 2.0 identity provider role for provider (%s)", entityID); - return make_pair(false,0); + + if (ECP) { + encoder = m_ecp; + if (!encoder) { + m_log.error("MessageEncoder for PAOS binding not available"); + return make_pair(false,0L); + } } + else { + // Use metadata to locate the IdP's SSO service. + MetadataProvider::Criteria mc(entityID, &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS); + entity=m->getEntityDescriptor(mc); + if (!entity.first) { + m_log.warn("unable to locate metadata for provider (%s)", entityID); + throw MetadataException("Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", entityID)); + } + else if (!entity.second) { + m_log.warn("unable to locate SAML 2.0 identity provider role for provider (%s)", entityID); + if (getParent()) + return make_pair(false,0L); + throw MetadataException("Unable to locate SAML 2.0 identity provider role for provider ($entityID)", namedparams(1, "entityID", entityID)); + } - // Loop over the supportable outgoing bindings. - const EndpointType* ep=NULL; - const MessageEncoder* encoder=NULL; - vector::const_iterator b; - for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) { - if (ep=EndpointManager(role->getSingleSignOnServices()).getByBinding(*b)) { - map::const_iterator enc = m_encoders.find(*b); - if (enc!=m_encoders.end()) - encoder = enc->second; - break; + // Loop over the supportable outgoing bindings. + role = dynamic_cast(entity.second); + vector::const_iterator b; + for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) { + if (ep=EndpointManager(role->getSingleSignOnServices()).getByBinding(*b)) { + map::const_iterator enc = m_encoders.find(*b); + if (enc!=m_encoders.end()) + encoder = enc->second; + break; + } + } + if (!ep || !encoder) { + m_log.warn("unable to locate compatible SSO service for provider (%s)", entityID); + if (getParent()) + return make_pair(false,0L); + throw MetadataException("Unable to locate compatible SSO service for provider ($entityID)", namedparams(1, "entityID", entityID)); } - } - if (!ep || !encoder) { - m_log.error("unable to locate compatible SSO service for provider (%s)", entityID); - return make_pair(false,0); } preserveRelayState(app, httpResponse, relayState); - // For now just build a dummy AuthnRequest. - auto_ptr req(AuthnRequestBuilder::buildAuthnRequest()); - req->setDestination(ep->getLocation()); - if (acsIndex) + auto_ptr req(m_requestTemplate ? m_requestTemplate->cloneAuthnRequest() : AuthnRequestBuilder::buildAuthnRequest()); + if (m_requestTemplate) { + // Freshen TS and ID. + req->setID(NULL); + req->setIssueInstant(time(NULL)); + } + + if (ep) + req->setDestination(ep->getLocation()); + if (acsIndex && *acsIndex) req->setAssertionConsumerServiceIndex(acsIndex); - if (acsLocation) - req->setAssertionConsumerServiceURL(acsLocation); - if (acsBinding) + if (acsLocation) { + auto_ptr_XMLCh wideloc(acsLocation); + req->setAssertionConsumerServiceURL(wideloc.get()); + } + if (acsBinding && *acsBinding) req->setProtocolBinding(acsBinding); - Issuer* issuer = IssuerBuilder::buildIssuer(); - req->setIssuer(issuer); - issuer->setName(app.getXMLString("providerId").second); - - auto_ptr_char dest(ep->getLocation()); - - // Check for signing. - const PropertySet* relyingParty = app.getRelyingParty(entity); - pair flag = relyingParty->getBool("signRequests"); - if ((flag.first && flag.second) || role->WantAuthnRequestsSigned()) { - CredentialResolver* credResolver=app.getCredentialResolver(); - if (credResolver) { - Locker credLocker(credResolver); - // Fill in criteria to use. - MetadataCredentialCriteria mcc(*role); - mcc.setUsage(CredentialCriteria::SIGNING_CREDENTIAL); - pair sigalg = relyingParty->getXMLString("signatureAlg"); - if (sigalg.first) - mcc.setXMLAlgorithm(sigalg.second); - const Credential* cred = credResolver->resolve(&mcc); - if (cred) { - // Signed request. - long ret = encoder->encode( - httpResponse, - req.get(), - dest.get(), - entityID, - relayState.c_str(), - cred, - sigalg.second, - relyingParty->getXMLString("digestAlg").second - ); - req.release(); // freed by encoder - return make_pair(true,ret); - } - else { - m_log.warn("no signing credential resolved, leaving AuthnRequest unsigned"); - } + if (isPassive) + req->IsPassive(isPassive); + else if (forceAuthn) + req->ForceAuthn(forceAuthn); + if (!req->getIssuer()) { + Issuer* issuer = IssuerBuilder::buildIssuer(); + req->setIssuer(issuer); + issuer->setName(app.getRelyingParty(entity.first)->getXMLString("entityID").second); + } + if (!req->getNameIDPolicy()) { + NameIDPolicy* namepol = NameIDPolicyBuilder::buildNameIDPolicy(); + req->setNameIDPolicy(namepol); + namepol->AllowCreate(true); + } + if (authnContextClassRef || authnContextComparison) { + RequestedAuthnContext* reqContext = req->getRequestedAuthnContext(); + if (!reqContext) { + reqContext = RequestedAuthnContextBuilder::buildRequestedAuthnContext(); + req->setRequestedAuthnContext(reqContext); + } + if (authnContextClassRef) { + reqContext->getAuthnContextDeclRefs().clear(); + auto_ptr_XMLCh wideclass(authnContextClassRef); + AuthnContextClassRef* cref = AuthnContextClassRefBuilder::buildAuthnContextClassRef(); + cref->setReference(wideclass.get()); + reqContext->getAuthnContextClassRefs().push_back(cref); + } + if (authnContextComparison && + (!reqContext->getAuthnContextClassRefs().empty() || !reqContext->getAuthnContextDeclRefs().empty())) { + auto_ptr_XMLCh widecomp(authnContextComparison); + reqContext->setComparison(widecomp.get()); } } - // Unsigned request. - long ret = encoder->encode(httpResponse, req.get(), dest.get(), entityID, relayState.c_str()); + if (ECP && entityID) { + auto_ptr_XMLCh wideid(entityID); + Scoping* scoping = req->getScoping(); + if (!scoping) { + scoping = ScopingBuilder::buildScoping(); + req->setScoping(scoping); + } + IDPList* idplist = scoping->getIDPList(); + if (!idplist) { + idplist = IDPListBuilder::buildIDPList(); + scoping->setIDPList(idplist); + } + VectorOf(IDPEntry) entries = idplist->getIDPEntrys(); + if (find_if(entries, bind2nd(_sameIdP(), wideid.get())) == NULL) { + IDPEntry* entry = IDPEntryBuilder::buildIDPEntry(); + entry->setProviderID(wideid.get()); + entries.push_back(entry); + } + } + + auto_ptr_char dest(ep ? ep->getLocation() : NULL); + + long ret = sendMessage( + *encoder, req.get(), relayState.c_str(), dest.get(), role, app, httpResponse, role ? role->WantAuthnRequestsSigned() : false + ); req.release(); // freed by encoder return make_pair(true,ret); +#else + return make_pair(false,0L); +#endif }