X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fhandler%2Fimpl%2FSAML2Consumer.cpp;h=0a2ebd8abb276d3aaaad686bbe9382524f4c0a53;hb=3ccda9caa12c4e6e38b1f565f53e1057876fb2d6;hp=9b1c3ec714ad7f2979a17fdfe46daa9c83d3d722;hpb=86b94e5088c87e59c1a786c0de796c15653fcaea;p=shibboleth%2Fsp.git diff --git a/shibsp/handler/impl/SAML2Consumer.cpp b/shibsp/handler/impl/SAML2Consumer.cpp index 9b1c3ec..0a2ebd8 100644 --- a/shibsp/handler/impl/SAML2Consumer.cpp +++ b/shibsp/handler/impl/SAML2Consumer.cpp @@ -61,11 +61,17 @@ namespace shibsp { } virtual ~SAML2Consumer() {} - private: #ifndef SHIBSP_LITE - string implementProtocol( + void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const { + AssertionConsumerService::generateMetadata(role, handlerURL); + role.addSupport(samlconstants::SAML20P_NS); + } + + private: + void implementProtocol( const Application& application, const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, SecurityPolicy& policy, const PropertySet* settings, const XMLObject& xmlObject @@ -86,9 +92,10 @@ namespace shibsp { #ifndef SHIBSP_LITE -string SAML2Consumer::implementProtocol( +void SAML2Consumer::implementProtocol( const Application& application, const HTTPRequest& httpRequest, + HTTPResponse& httpResponse, SecurityPolicy& policy, const PropertySet* settings, const XMLObject& xmlObject @@ -125,26 +132,30 @@ 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.setAuthenticated(false); policy.reset(true); @@ -157,26 +168,25 @@ string SAML2Consumer::implementProtocol( policy.evaluate(*(*a)); // If no security is in place now, we kick it. - if (!alreadySecured && !policy.isAuthenticated()) { - 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); @@ -184,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; } @@ -194,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); } } @@ -211,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) { @@ -224,14 +240,11 @@ 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.setAuthenticated(false); policy.reset(true); @@ -247,26 +260,17 @@ string SAML2Consumer::implementProtocol( policy.evaluate(*decrypted); // If no security is in place now, we kick it. - if (!alreadySecured && !policy.isAuthenticated()) { - 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); @@ -274,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; } @@ -284,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. @@ -309,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) { @@ -332,7 +342,6 @@ 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") : pair(true,28800); if (!lifetime.first || lifetime.second == 0) lifetime.second = 28800; @@ -366,11 +375,12 @@ string SAML2Consumer::implementProtocol( // 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(), - policy.getIssuerMetadata() ? dynamic_cast(policy.getIssuerMetadata()->getParent()) : NULL, + httpRequest, + httpResponse, + sessionExp, + entity, samlconstants::SAML20P_NS, ssoName, ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : NULL, @@ -384,7 +394,6 @@ string SAML2Consumer::implementProtocol( if (ownedName) delete ssoName; for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - return key; } catch (exception&) { if (ownedName)