X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fhandler%2Fimpl%2FSAML2Consumer.cpp;h=0a2ebd8abb276d3aaaad686bbe9382524f4c0a53;hb=3ccda9caa12c4e6e38b1f565f53e1057876fb2d6;hp=5d544ec955f6d1a3e5e557663ecb25c97d244540;hpb=d30673a03fe633dfb952bb6644657574f81a8ffa;p=shibboleth%2Fsp.git diff --git a/shibsp/handler/impl/SAML2Consumer.cpp b/shibsp/handler/impl/SAML2Consumer.cpp index 5d544ec..0a2ebd8 100644 --- a/shibsp/handler/impl/SAML2Consumer.cpp +++ b/shibsp/handler/impl/SAML2Consumer.cpp @@ -21,29 +21,29 @@ */ #include "internal.h" -#include "Application.h" -#include "exceptions.h" -#include "ServiceProvider.h" -#include "SessionCache.h" -#include "attribute/Attribute.h" -#include "attribute/filtering/AttributeFilter.h" -#include "attribute/filtering/BasicFilteringContext.h" -#include "attribute/resolver/AttributeExtractor.h" -#include "attribute/resolver/ResolutionContext.h" #include "handler/AssertionConsumerService.h" -#include -#include -#include -#include - -using namespace shibsp; +#ifndef SHIBSP_LITE +# include "exceptions.h" +# include "Application.h" +# include "ServiceProvider.h" +# include "SessionCache.h" +# include "attribute/resolver/ResolutionContext.h" +# include +# include +# include +# include using namespace opensaml::saml2; using namespace opensaml::saml2p; using namespace opensaml::saml2md; using namespace opensaml; +# ifndef min +# define min(a,b) (((a) < (b)) ? (a) : (b)) +# endif +#endif + +using namespace shibsp; using namespace xmltooling; -using namespace log4cpp; using namespace std; namespace shibsp { @@ -57,18 +57,26 @@ namespace shibsp { { public: SAML2Consumer(const DOMElement* e, const char* appId) - : AssertionConsumerService(e, appId, Category::getInstance(SHIBSP_LOGCAT".SAML2")) { + : AssertionConsumerService(e, appId, Category::getInstance(SHIBSP_LOGCAT".SSO.SAML2")) { } virtual ~SAML2Consumer() {} +#ifndef SHIBSP_LITE + void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const { + AssertionConsumerService::generateMetadata(role, handlerURL); + role.addSupport(samlconstants::SAML20P_NS); + } + private: - string implementProtocol( + void implementProtocol( const Application& application, const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, SecurityPolicy& policy, const PropertySet* settings, const XMLObject& xmlObject ) const; +#endif }; #if defined (_MSC_VER) @@ -82,9 +90,12 @@ namespace shibsp { }; -string SAML2Consumer::implementProtocol( +#ifndef SHIBSP_LITE + +void SAML2Consumer::implementProtocol( const Application& application, const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, SecurityPolicy& policy, const PropertySet* settings, const XMLObject& xmlObject @@ -95,7 +106,7 @@ string SAML2Consumer::implementProtocol( // Remember whether we already established trust. // None of the SAML 2 bindings require security at the protocol layer. - bool alreadySecured = policy.isSecure(); + bool alreadySecured = policy.isAuthenticated(); // Check for errors...this will throw if it's not a successful message. checkError(&xmlObject); @@ -121,54 +132,61 @@ string SAML2Consumer::implementProtocol( // And also track "owned" tokens that we decrypt here. vector ownedtokens; - // Profile validator. + // With this flag on, we ignore any unsigned assertions. + const EntityDescriptor* entity = NULL; + pair flag = make_pair(false,false); + if (alreadySecured && policy.getIssuerMetadata()) { + entity = dynamic_cast(policy.getIssuerMetadata()->getParent()); + flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions"); + } + time_t now = time(NULL); string dest = httpRequest.getRequestURL(); - BrowserSSOProfileValidator ssoValidator(application.getAudiences(), now, dest.substr(0,dest.find('?')).c_str()); - // With this flag on, we ignore any unsigned assertions. - pair flag = settings->getBool("signedAssertions"); + // authnskew allows rejection of SSO if AuthnInstant is too old. + const PropertySet* sessionProps = application.getPropertySet("Sessions"); + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair(false,0); - // Saves off IP-mismatch error message because it's potentially helpful for users. - string addressMismatch; + // Saves off error messages potentially helpful for users. + string contextualError; for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { - // Skip unsigned assertion? - if (!(*a)->getSignature() && flag.first && flag.second) { - m_log.warn("found unsigned assertion in SAML response, ignoring it per signedAssertions policy"); - badtokens.push_back(*a); - continue; - } - try { + // Skip unsigned assertion? + if (!(*a)->getSignature() && flag.first && flag.second) + throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy."); + // We clear the security flag, so we can tell whether the token was secured on its own. - policy.setSecure(false); - - // Run the policy over the assertion. Handles issuer consistency, replay, freshness, - // and signature verification, assuming the relevant rules are configured. + policy.setAuthenticated(false); + policy.reset(true); + + // Extract message bits and re-verify Issuer information. + extractMessageDetails(*(*a), samlconstants::SAML20P_NS, policy); + + // Run the policy over the assertion. Handles replay, freshness, and + // signature verification, assuming the relevant rules are configured. policy.evaluate(*(*a)); // If no security is in place now, we kick it. - if (!alreadySecured && !policy.isSecure()) { - m_log.warn("unable to establish security of assertion"); - badtokens.push_back(*a); - continue; + if (!alreadySecured && !policy.isAuthenticated()) + throw SecurityPolicyException("Unable to establish security of incoming assertion."); + + // If we hadn't established Issuer yet, redo the signedAssertions check. + if (!entity && policy.getIssuerMetadata()) { + entity = dynamic_cast(policy.getIssuerMetadata()->getParent()); + flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions"); + if (!(*a)->getSignature() && flag.first && flag.second) + throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy."); } // Now do profile and core semantic validation to ensure we can use it for SSO. + BrowserSSOProfileValidator ssoValidator( + application.getRelyingParty(entity)->getXMLString("entityID").second, application.getAudiences(), now, dest.substr(0,dest.find('?')).c_str() + ); ssoValidator.validateAssertion(*(*a)); // Address checking. - try { - if (ssoValidator.getAddress()) - checkAddress(application, httpRequest, ssoValidator.getAddress()); - } - catch (exception& ex) { - // We save off the message if there's no SSO statement yet. - if (!ssoStatement) - addressMismatch = ex.what(); - throw; - } + checkAddress(application, httpRequest, ssoValidator.getAddress()); // Track it as a valid token. tokens.push_back(*a); @@ -176,7 +194,9 @@ string SAML2Consumer::implementProtocol( // Save off the first valid SSO statement, but favor the "soonest" session expiration. const vector& statements = const_cast(*a)->getAuthnStatements(); for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { - if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) + if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second)) + contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit."; + else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) ssoStatement = *s; } @@ -186,6 +206,8 @@ string SAML2Consumer::implementProtocol( } catch (exception& ex) { m_log.warn("detected a problem with assertion: %s", ex.what()); + if (!ssoStatement) + contextualError = ex.what(); badtokens.push_back(*a); } } @@ -203,11 +225,13 @@ string SAML2Consumer::implementProtocol( auto_ptr mcc( policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL ); - auto_ptr wrapper((*ea)->decrypt(*cr, application.getXMLString("entityID").second, mcc.get())); + auto_ptr wrapper((*ea)->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get())); decrypted = dynamic_cast(wrapper.get()); if (decrypted) { wrapper.release(); ownedtokens.push_back(decrypted); + if (m_log.isDebugEnabled()) + m_log.debugStream() << "decrypted Assertion: " << *decrypted << logging::eol; } } catch (exception& ex) { @@ -216,43 +240,37 @@ string SAML2Consumer::implementProtocol( if (!decrypted) continue; - // Skip unsigned assertion? - if (!decrypted->getSignature() && flag.first && flag.second) { - m_log.warn("found unsigned assertion in SAML response, ignoring it per signedAssertions policy"); - badtokens.push_back(decrypted); - continue; - } - try { + // Skip unsigned assertion? + if (!decrypted->getSignature() && flag.first && flag.second) + throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy."); + // We clear the security flag, so we can tell whether the token was secured on its own. - policy.setSecure(false); - - // Run the policy over the assertion. Handles issuer consistency, replay, freshness, - // and signature verification, assuming the relevant rules are configured. + policy.setAuthenticated(false); + policy.reset(true); + + // Extract message bits and re-verify Issuer information. + extractMessageDetails(*decrypted, samlconstants::SAML20P_NS, policy); + + // Run the policy over the assertion. Handles replay, freshness, and + // signature verification, assuming the relevant rules are configured. // We have to marshall the object first to ensure signatures can be checked. + if (!decrypted->getDOM()) + decrypted->marshall(); policy.evaluate(*decrypted); // If no security is in place now, we kick it. - if (!alreadySecured && !policy.isSecure()) { - m_log.warn("unable to establish security of assertion"); - badtokens.push_back(decrypted); - continue; - } + if (!alreadySecured && !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. + BrowserSSOProfileValidator ssoValidator( + application.getRelyingParty(entity)->getXMLString("entityID").second, application.getAudiences(), now, dest.substr(0,dest.find('?')).c_str() + ); ssoValidator.validateAssertion(*decrypted); // Address checking. - try { - if (ssoValidator.getAddress()) - checkAddress(application, httpRequest, ssoValidator.getAddress()); - } - catch (exception& ex) { - // We save off the message if there's no SSO statement yet. - if (!ssoStatement) - addressMismatch = ex.what(); - throw; - } + checkAddress(application, httpRequest, ssoValidator.getAddress()); // Track it as a valid token. tokens.push_back(decrypted); @@ -260,7 +278,9 @@ string SAML2Consumer::implementProtocol( // Save off the first valid SSO statement, but favor the "soonest" session expiration. const vector& statements = const_cast(decrypted)->getAuthnStatements(); for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { - if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) + if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second)) + contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit."; + else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) ssoStatement = *s; } @@ -270,15 +290,17 @@ string SAML2Consumer::implementProtocol( } catch (exception& ex) { m_log.warn("detected a problem with assertion: %s", ex.what()); + if (!ssoStatement) + contextualError = ex.what(); badtokens.push_back(decrypted); } } if (!ssoStatement) { for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - if (addressMismatch.empty()) + if (contextualError.empty()) throw FatalProfileException("A valid authentication statement was not found in the incoming message."); - throw FatalProfileException(addressMismatch.c_str()); + throw FatalProfileException(contextualError.c_str()); } // May need to decrypt NameID. @@ -295,11 +317,13 @@ string SAML2Consumer::implementProtocol( policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL ); try { - auto_ptr decryptedID(encname->decrypt(*cr,application.getXMLString("entityID").second,mcc.get())); + auto_ptr decryptedID(encname->decrypt(*cr,application.getRelyingParty(entity)->getXMLString("entityID").second,mcc.get())); ssoName = dynamic_cast(decryptedID.get()); if (ssoName) { ownedName = true; decryptedID.release(); + if (m_log.isDebugEnabled()) + m_log.debugStream() << "decrypted NameID: " << *ssoName << logging::eol; } } catch (exception& ex) { @@ -318,105 +342,65 @@ string SAML2Consumer::implementProtocol( // Session expiration for SAML 2.0 is jointly IdP- and SP-driven. time_t sessionExp = ssoStatement->getSessionNotOnOrAfter() ? ssoStatement->getSessionNotOnOrAfterEpoch() : 0; - const PropertySet* sessionProps = application.getPropertySet("Sessions"); - pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : make_pair(true,28800); - if (!lifetime.first) + pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); + if (!lifetime.first || lifetime.second == 0) lifetime.second = 28800; - if (lifetime.second != 0) { - if (sessionExp == 0) - sessionExp = now + lifetime.second; // IdP says nothing, calulate based on SP. - else - sessionExp = min(sessionExp, now + lifetime.second); // Use the lowest. - } + if (sessionExp == 0) + sessionExp = now + lifetime.second; // IdP says nothing, calulate based on SP. + else + sessionExp = min(sessionExp, now + lifetime.second); // Use the lowest. - // Other details... const AuthnContext* authnContext = ssoStatement->getAuthnContext(); - auto_ptr_char authnClass((authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL); - auto_ptr_char authnDecl((authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL); - auto_ptr_char index(ssoStatement->getSessionIndex()); - auto_ptr_char authnInstant(ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : NULL); - - multimap resolvedAttributes; - AttributeExtractor* extractor = application.getAttributeExtractor(); - if (extractor) { - m_log.debug("extracting pushed attributes..."); - Locker extlocker(extractor); - try { - extractor->extractAttributes(application, policy.getIssuerMetadata(), *ssoName, resolvedAttributes); - } - catch (exception& ex) { - m_log.error("caught exception extracting attributes: %s", ex.what()); - } - for (vector::const_iterator t = tokens.begin(); t!=tokens.end(); ++t) { - try { - extractor->extractAttributes(application, policy.getIssuerMetadata(), *(*t), resolvedAttributes); - } - catch (exception& ex) { - m_log.error("caught exception extracting attributes: %s", ex.what()); - } - } - } - - AttributeFilter* filter = application.getAttributeFilter(); - if (filter && !resolvedAttributes.empty()) { - BasicFilteringContext fc(application, resolvedAttributes, policy.getIssuerMetadata(), authnClass.get(), authnDecl.get()); - Locker filtlocker(filter); - try { - filter->filterAttributes(fc, resolvedAttributes); - } - catch (exception& ex) { - m_log.error("caught exception filtering attributes: %s", ex.what()); - m_log.error("dumping extracted attributes due to filtering exception"); - for_each(resolvedAttributes.begin(), resolvedAttributes.end(), cleanup_pair()); - resolvedAttributes.clear(); - } - } try { - const EntityDescriptor* issuerMetadata = - policy.getIssuerMetadata() ? dynamic_cast(policy.getIssuerMetadata()->getParent()) : NULL; + // The context will handle deleting attributes and new tokens. auto_ptr ctx( - resolveAttributes(application, issuerMetadata, ssoName, authnClass.get(), authnDecl.get(), &tokens, &resolvedAttributes) + resolveAttributes( + application, + policy.getIssuerMetadata(), + samlconstants::SAML20P_NS, + NULL, + ssoName, + (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL, + (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL, + &tokens + ) ); if (ctx.get()) { // Copy over any new tokens, but leave them in the context for cleanup. tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end()); - - // Copy over new attributes, and transfer ownership. - resolvedAttributes.insert(ctx->getResolvedAttributes().begin(), ctx->getResolvedAttributes().end()); - ctx->getResolvedAttributes().clear(); } // Now merge in bad tokens for caching. tokens.insert(tokens.end(), badtokens.begin(), badtokens.end()); - string key = application.getServiceProvider().getSessionCache()->insert( - sessionExp, + application.getServiceProvider().getSessionCache()->insert( application, - httpRequest.getRemoteAddr().c_str(), - issuerMetadata, + httpRequest, + httpResponse, + sessionExp, + entity, + samlconstants::SAML20P_NS, ssoName, - authnInstant.get(), - index.get(), - authnClass.get(), - authnDecl.get(), + ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : NULL, + ssoStatement->getSessionIndex(), + (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL, + (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL, &tokens, - &resolvedAttributes + ctx.get() ? &ctx->getResolvedAttributes() : NULL ); - resolvedAttributes.clear(); // Attributes are owned by cache now. if (ownedName) delete ssoName; for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - - return key; } catch (exception&) { if (ownedName) delete ssoName; for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - for_each(resolvedAttributes.begin(), resolvedAttributes.end(), cleanup_pair()); throw; } } + +#endif