X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=adfs%2Fadfs.cpp;h=c22ce2a98c38874cc88b2ffb72dff166a3457433;hb=a3eb31c426af84dcd1827d8763b652a474196d42;hp=a31006afa8e3ea17c9be8de582bc9f368bee713e;hpb=605cb3cf0ef675358e3996254a062e43c8c227f5;p=shibboleth%2Fsp.git diff --git a/adfs/adfs.cpp b/adfs/adfs.cpp index a31006a..c22ce2a 100644 --- a/adfs/adfs.cpp +++ b/adfs/adfs.cpp @@ -57,6 +57,7 @@ # include # include # include +# include # include # include using namespace opensaml::saml2md; @@ -128,7 +129,7 @@ namespace { } void receive(DDF& in, ostream& out); - pair run(SPRequest& request, const char* entityID=NULL, bool isHandler=true) const; + pair run(SPRequest& request, string& entityID, bool isHandler=true) const; private: pair doRequest( @@ -136,6 +137,7 @@ namespace { HTTPResponse& httpResponse, const char* entityID, const char* acsLocation, + const char* authnContextClassRef, string& relayState ) const; string m_appId; @@ -307,15 +309,16 @@ extern "C" void ADFS_EXPORTS xmltooling_extension_term() */ } -pair ADFSSessionInitiator::run(SPRequest& request, const char* entityID, bool isHandler) const +pair ADFSSessionInitiator::run(SPRequest& request, string& entityID, bool isHandler) const { // We have to know the IdP to function. - if (!entityID || !*entityID) + if (entityID.empty()) return make_pair(false,0L); string target; const Handler* ACS=NULL; const char* option; + pair acClass; const Application& app=request.getApplication(); if (isHandler) { @@ -326,11 +329,21 @@ pair ADFSSessionInitiator::run(SPRequest& request, const char* entity // 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, request, target, false); + + if (acClass.second = request.getParameter("authnContextClassRef")) + acClass.first = true; + else + acClass = getString("authnContextClassRef"); } 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; + acClass = settings->getString("authnContextClassRef"); + if (!acClass.first) + acClass = getString("authnContextClassRef"); } // Since we're not passing by index, we need to fully compute the return URL. @@ -372,19 +385,21 @@ pair ADFSSessionInitiator::run(SPRequest& request, const char* entity target = option; } - m_log.debug("attempting to initiate session using ADFS with provider (%s)", entityID); + m_log.debug("attempting to initiate session using ADFS with provider (%s)", entityID.c_str()); if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) - return doRequest(app, request, entityID, ACSloc.c_str(), target); + return doRequest(app, request, entityID.c_str(), ACSloc.c_str(), (acClass.first ? acClass.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); + in.addmember("entity_id").string(entityID.c_str()); in.addmember("acsLocation").string(ACSloc.c_str()); if (!target.empty()) in.addmember("RelayState").string(target.c_str()); + if (acClass.first) + in.addmember("authnContextClassRef").string(acClass.second); // Remote the processing. out = request.getServiceProvider().getListenerService()->send(in); @@ -418,7 +433,7 @@ void ADFSSessionInitiator::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, acsLocation, relayState); + doRequest(*app, *http.get(), entityID, acsLocation, in["authnContextClassRef"].string(), relayState); out << ret; } @@ -427,6 +442,7 @@ pair ADFSSessionInitiator::doRequest( HTTPResponse& httpResponse, const char* entityID, const char* acsLocation, + const char* authnContextClassRef, string& relayState ) const { @@ -474,6 +490,8 @@ pair ADFSSessionInitiator::doRequest( string req=string(dest.get()) + (strchr(dest.get(),'?') ? '&' : '?') + "wa=wsignin1.0&wreply=" + urlenc->encode(acsLocation) + "&wct=" + urlenc->encode(timebuf) + "&wtrealm=" + urlenc->encode(app.getString("entityID").second); + if (authnContextClassRef) + req += "&wauth=" + urlenc->encode(authnContextClassRef); if (!relayState.empty()) req += "&wctx=" + urlenc->encode(relayState.c_str()); @@ -551,9 +569,9 @@ void ADFSConsumer::implementProtocol( response = dynamic_cast(response->getUnknownXMLObjects().front()); if (!response || !response->hasChildren()) throw FatalProfileException("Token wrapper element did not contain a security token."); - const saml1::Assertion* token = dynamic_cast(response->getUnknownXMLObjects().front()); + const Assertion* token = dynamic_cast(response->getUnknownXMLObjects().front()); if (!token || !token->getSignature()) - throw FatalProfileException("Incoming message did not contain a signed SAML 1.1 assertion."); + throw FatalProfileException("Incoming message did not contain a signed SAML assertion."); // Extract message and issuer details from assertion. extractMessageDetails(*token, m_protocol.get(), policy); @@ -566,67 +584,121 @@ void ADFSConsumer::implementProtocol( if (!policy.isAuthenticated()) throw SecurityPolicyException("Unable to establish security of incoming assertion."); - // Now do profile and core semantic validation to ensure we can use it for SSO. - // Profile validator. time_t now = time(NULL); - saml1::AssertionValidator ssoValidator(application.getAudiences(), now); - ssoValidator.validateAssertion(*token); - if (!token->getConditions() || !token->getConditions()->getNotBefore() || !token->getConditions()->getNotOnOrAfter()) - throw FatalProfileException("Assertion did not contain time conditions."); - else if (token->getAuthenticationStatements().empty()) - throw FatalProfileException("Assertion did not contain an authentication statement."); - - // With ADFS, we only have one token, but we need to put it in a vector. - vector tokens(1,token); - const saml1::AuthenticationStatement* ssoStatement=token->getAuthenticationStatements().front(); - - // authnskew allows rejection of SSO if AuthnInstant is too old. const PropertySet* sessionProps = application.getPropertySet("Sessions"); - pair authnskew = sessionProps ? sessionProps->getUnsignedInt("authnskew") : pair(false,0); + const EntityDescriptor* entity = policy.getIssuerMetadata() ? dynamic_cast(policy.getIssuerMetadata()->getParent()) : NULL; + + saml1::NameIdentifier* saml1name=NULL; + saml2::NameID* saml2name=NULL; + const XMLCh* authMethod=NULL; + const XMLCh* authInstant=NULL; + time_t sessionExp = 0; + + const saml1::Assertion* saml1token = dynamic_cast(token); + if (saml1token) { + // Now do profile and core semantic validation to ensure we can use it for SSO. + saml1::AssertionValidator ssoValidator(application.getRelyingParty(entity)->getXMLString("entityID").second, application.getAudiences(), now); + ssoValidator.validateAssertion(*saml1token); + if (!saml1token->getConditions() || !saml1token->getConditions()->getNotBefore() || !saml1token->getConditions()->getNotOnOrAfter()) + throw FatalProfileException("Assertion did not contain time conditions."); + else if (saml1token->getAuthenticationStatements().empty()) + throw FatalProfileException("Assertion did not contain an authentication statement."); + + // authnskew allows rejection of SSO if AuthnInstant is too old. + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair(false,0); + + const saml1::AuthenticationStatement* ssoStatement=saml1token->getAuthenticationStatements().front(); + if (authnskew.first && authnskew.second && + ssoStatement->getAuthenticationInstant() && (now - ssoStatement->getAuthenticationInstantEpoch() > authnskew.second)) + throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the limit."); + + // Address checking. + saml1::SubjectLocality* locality = ssoStatement->getSubjectLocality(); + if (locality && locality->getIPAddress()) { + auto_ptr_char ip(locality->getIPAddress()); + checkAddress(application, httpRequest, ip.get()); + } - if (authnskew.first && authnskew.second && - ssoStatement->getAuthenticationInstant() && (now - ssoStatement->getAuthenticationInstantEpoch() > authnskew.second)) - throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the limit."); + saml1name = ssoStatement->getSubject()->getNameIdentifier(); + authMethod = ssoStatement->getAuthenticationMethod(); + if (ssoStatement->getAuthenticationInstant()) + authInstant = ssoStatement->getAuthenticationInstant()->getRawData(); - // Address checking. - saml1::SubjectLocality* locality = ssoStatement->getSubjectLocality(); - if (locality && locality->getIPAddress()) { - auto_ptr_char ip(locality->getIPAddress()); - checkAddress(application, httpRequest, ip.get()); + // Session expiration. + pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); + if (!lifetime.first || lifetime.second == 0) + lifetime.second = 28800; + sessionExp = now + lifetime.second; } + else { + const saml2::Assertion* saml2token = dynamic_cast(token); + if (!saml2token) + throw FatalProfileException("Incoming message did not contain a recognized type of SAML assertion."); + + // Now do profile and core semantic validation to ensure we can use it for SSO. + saml2::AssertionValidator ssoValidator(application.getRelyingParty(entity)->getXMLString("entityID").second, application.getAudiences(), now); + ssoValidator.validateAssertion(*saml2token); + if (!saml2token->getConditions() || !saml2token->getConditions()->getNotBefore() || !saml2token->getConditions()->getNotOnOrAfter()) + throw FatalProfileException("Assertion did not contain time conditions."); + else if (saml2token->getAuthnStatements().empty()) + throw FatalProfileException("Assertion did not contain an authentication statement."); + + // authnskew allows rejection of SSO if AuthnInstant is too old. + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair(false,0); + + const saml2::AuthnStatement* ssoStatement=saml2token->getAuthnStatements().front(); + if (authnskew.first && authnskew.second && + ssoStatement->getAuthnInstant() && (now - ssoStatement->getAuthnInstantEpoch() > authnskew.second)) + throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the limit."); + + // Address checking. + saml2::SubjectLocality* locality = ssoStatement->getSubjectLocality(); + if (locality && locality->getAddress()) { + auto_ptr_char ip(locality->getAddress()); + checkAddress(application, httpRequest, ip.get()); + } + saml2name = saml2token->getSubject() ? saml2token->getSubject()->getNameID() : NULL; + if (ssoStatement->getAuthnContext() && ssoStatement->getAuthnContext()->getAuthnContextClassRef()) + authMethod = ssoStatement->getAuthnContext()->getAuthnContextClassRef()->getReference(); + if (ssoStatement->getAuthnInstant()) + authInstant = ssoStatement->getAuthnInstant()->getRawData(); + + // Session expiration for SAML 2.0 is jointly IdP- and SP-driven. + sessionExp = ssoStatement->getSessionNotOnOrAfter() ? ssoStatement->getSessionNotOnOrAfterEpoch() : 0; + pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); + if (!lifetime.first || lifetime.second == 0) + lifetime.second = 28800; + if (sessionExp == 0) + sessionExp = now + lifetime.second; // IdP says nothing, calulate based on SP. + else + sessionExp = min(sessionExp, now + lifetime.second); // Use the lowest. + } + m_log.debug("ADFS profile processing completed successfully"); - saml1::NameIdentifier* n = ssoStatement->getSubject()->getNameIdentifier(); - - // Now we have to extract the authentication details for attribute and session setup. - - // Session expiration for ADFS is purely SP-driven, and the method is mapped to a ctx class. - pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); - if (!lifetime.first || lifetime.second == 0) - lifetime.second = 28800; - // We've successfully "accepted" the SSO token. // To complete processing, we need to extract and resolve attributes and then create the session. - // Normalize the SAML 1.x NameIdentifier... - auto_ptr nameid(n ? saml2::NameIDBuilder::buildNameID() : NULL); - if (n) { - nameid->setName(n->getName()); - nameid->setFormat(n->getFormat()); - nameid->setNameQualifier(n->getNameQualifier()); + // Normalize a SAML 1.x NameIdentifier... + auto_ptr nameid(saml1name ? saml2::NameIDBuilder::buildNameID() : NULL); + if (saml1name) { + nameid->setName(saml1name->getName()); + nameid->setFormat(saml1name->getFormat()); + nameid->setNameQualifier(saml1name->getNameQualifier()); } // The context will handle deleting attributes and new tokens. + vector tokens(1,token); auto_ptr ctx( resolveAttributes( application, policy.getIssuerMetadata(), m_protocol.get(), - n, - nameid.get(), - ssoStatement->getAuthenticationMethod(), + saml1name, + (saml1name ? nameid.get() : saml2name), + authMethod, NULL, &tokens ) @@ -641,13 +713,13 @@ void ADFSConsumer::implementProtocol( application, httpRequest, httpResponse, - now + lifetime.second, - policy.getIssuerMetadata() ? dynamic_cast(policy.getIssuerMetadata()->getParent()) : NULL, + sessionExp, + entity, m_protocol.get(), - nameid.get(), - ssoStatement->getAuthenticationInstant() ? ssoStatement->getAuthenticationInstant()->getRawData() : NULL, + (saml1name ? nameid.get() : saml2name), + authInstant, NULL, - ssoStatement->getAuthenticationMethod(), + authMethod, NULL, &tokens, ctx.get() ? &ctx->getResolvedAttributes() : NULL @@ -820,5 +892,5 @@ pair ADFSLogout::run(SPRequest& request, bool isHandler) const if (param) return make_pair(true, request.sendRedirect(param)); - return sendLogoutPage(app, request, false, "Logout complete."); + return sendLogoutPage(app, request, request, false, "Logout complete."); }