X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fhandler%2Fimpl%2FSAML2Consumer.cpp;h=be2397c17d4789d5244a67c53a025750bfa352d2;hb=6304f238f421a26455e1278131e69b1b471a8d45;hp=98b5f400a6b126a7019546aed92748ea5ec01dc3;hpb=f29de21cba6480f04c3d8877039ae25e373233a9;p=shibboleth%2Fcpp-sp.git diff --git a/shibsp/handler/impl/SAML2Consumer.cpp b/shibsp/handler/impl/SAML2Consumer.cpp index 98b5f40..be2397c 100644 --- a/shibsp/handler/impl/SAML2Consumer.cpp +++ b/shibsp/handler/impl/SAML2Consumer.cpp @@ -1,45 +1,60 @@ -/* - * Copyright 2001-2007 Internet2 +/** + * Licensed to the University Corporation for Advanced Internet + * Development, Inc. (UCAID) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * UCAID licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. */ /** * SAML2Consumer.cpp * - * SAML 2.0 assertion consumer service + * SAML 2.0 assertion consumer service. */ #include "internal.h" #include "handler/AssertionConsumerService.h" #ifndef SHIBSP_LITE -# include "exceptions.h" # include "Application.h" # include "ServiceProvider.h" # include "SessionCache.h" +# include "TransactionLog.h" # include "attribute/resolver/ResolutionContext.h" +# include +# include +# include +# include # include -# include # include # include +# include +# include +# include +# include +# include using namespace opensaml::saml2; using namespace opensaml::saml2p; using namespace opensaml::saml2md; using namespace opensaml; +using namespace boost; # ifndef min # define min(a,b) (((a) < (b)) ? (a) : (b)) # endif +#else +# include "lite/SAMLConstants.h" #endif using namespace shibsp; @@ -58,6 +73,10 @@ namespace shibsp { public: SAML2Consumer(const DOMElement* e, const char* appId) : AssertionConsumerService(e, appId, Category::getInstance(SHIBSP_LOGCAT".SSO.SAML2")) { +#ifndef SHIBSP_LITE + if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) + m_ssoRule.reset(SAMLConfig::getConfig().SecurityPolicyRuleManager.newPlugin(BEARER_POLICY_RULE, e)); +#endif } virtual ~SAML2Consumer() {} @@ -73,9 +92,15 @@ namespace shibsp { const HTTPRequest& httpRequest, HTTPResponse& httpResponse, SecurityPolicy& policy, - const PropertySet* settings, + const PropertySet*, const XMLObject& xmlObject ) const; + + scoped_ptr m_ssoRule; +#else + const XMLCh* getProtocolFamily() const { + return samlconstants::SAML20P_NS; + } #endif }; @@ -88,6 +113,18 @@ namespace shibsp { return new SAML2Consumer(p.first, p.second); } +#ifndef SHIBSP_LITE + class SHIBSP_DLLLOCAL _rulenamed : std::unary_function + { + public: + _rulenamed(const char* name) : m_name(name) {} + bool operator()(const SecurityPolicyRule* rule) const { + return rule ? !strcmp(m_name, rule->getType()) : false; + } + private: + const char* m_name; + }; +#endif }; #ifndef SHIBSP_LITE @@ -97,7 +134,7 @@ void SAML2Consumer::implementProtocol( const HTTPRequest& httpRequest, HTTPResponse& httpResponse, SecurityPolicy& policy, - const PropertySet* settings, + const PropertySet*, const XMLObject& xmlObject ) const { @@ -121,8 +158,8 @@ void SAML2Consumer::implementProtocol( throw FatalProfileException("Incoming message contained no SAML assertions."); // Maintain list of "legit" tokens to feed to SP subsystems. - const Subject* ssoSubject=NULL; - const AuthnStatement* ssoStatement=NULL; + const Subject* ssoSubject=nullptr; + const AuthnStatement* ssoStatement=nullptr; vector tokens; // Also track "bad" tokens that we'll cache but not use. @@ -130,19 +167,23 @@ void SAML2Consumer::implementProtocol( vector badtokens; // And also track "owned" tokens that we decrypt here. - vector ownedtokens; + vector< boost::shared_ptr > ownedtokens; + + // With this flag on, we block unauthenticated ciphertext when decrypting, + // unless the protocol was authenticated. + pair requireAuthenticatedEncryption = application.getBool("requireAuthenticatedEncryption"); + if (alreadySecured) + requireAuthenticatedEncryption.second = false; // With this flag on, we ignore any unsigned assertions. - const EntityDescriptor* entity = NULL; - pair flag = make_pair(false,false); + const EntityDescriptor* entity = nullptr; + pair requireSignedAssertions = make_pair(false,false); if (alreadySecured && policy.getIssuerMetadata()) { entity = dynamic_cast(policy.getIssuerMetadata()->getParent()); - flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions"); + const PropertySet* rp = application.getRelyingParty(entity); + requireSignedAssertions = rp->getBool("requireSignedAssertions"); } - time_t now = time(NULL); - string dest = httpRequest.getRequestURL(); - // 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); @@ -150,10 +191,19 @@ void SAML2Consumer::implementProtocol( // Saves off error messages potentially helpful for users. string contextualError; - for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { + // Ensure the Bearer rule is in the policy set. + if (find_if(policy.getRules(), _rulenamed(BEARER_POLICY_RULE)) == nullptr) + policy.getRules().push_back(m_ssoRule.get()); + + // Populate recipient as audience. + policy.getAudiences().push_back(application.getRelyingParty(entity)->getXMLString("entityID").second); + + time_t now = time(nullptr); + for (indirect_iterator::const_iterator> a = make_indirect_iterator(assertions.begin()); + a != make_indirect_iterator(assertions.end()); ++a) { try { // Skip unsigned assertion? - if (!(*a)->getSignature() && flag.first && flag.second) + if (!a->getSignature() && requireSignedAssertions.first && requireSignedAssertions.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. @@ -161,11 +211,12 @@ void SAML2Consumer::implementProtocol( policy.reset(true); // Extract message bits and re-verify Issuer information. - extractMessageDetails(*(*a), samlconstants::SAML20P_NS, policy); + 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)); + // signature verification, assuming the relevant rules are configured, + // along with condition and profile enforcement. + policy.evaluate(*a, &httpRequest); // If no security is in place now, we kick it. if (!alreadySecured && !policy.isAuthenticated()) @@ -174,77 +225,97 @@ void SAML2Consumer::implementProtocol( // 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) + requireSignedAssertions = application.getRelyingParty(entity)->getBool("requireSignedAssertions"); + if (!a->getSignature() && requireSignedAssertions.first && requireSignedAssertions.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. - checkAddress(application, httpRequest, ssoValidator.getAddress()); + SubjectConfirmationData* subcondata = dynamic_cast( + dynamic_cast(policy).getSubjectConfirmation()->getSubjectConfirmationData() + ); + if (subcondata && subcondata->getAddress()) { + auto_ptr_char boundip(subcondata->getAddress()); + checkAddress(application, httpRequest, boundip.get()); + } // Track it as a valid token. - tokens.push_back(*a); + tokens.push_back(&(*a)); // 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 (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; + const vector& statements = const_cast(*a).getAuthnStatements(); + for (indirect_iterator::const_iterator> s = make_indirect_iterator(statements.begin()); + s != make_indirect_iterator(statements.end()); ++s) { + if (s->getAuthnInstant() && s->getAuthnInstantEpoch() - XMLToolingConfig::getConfig().clock_skew_secs > now) { + contextualError = "The login time at your identity provider was future-dated."; + } + else if (authnskew.first && authnskew.second && s->getAuthnInstant() && + s->getAuthnInstantEpoch() <= now && (now - s->getAuthnInstantEpoch() > authnskew.second)) { + contextualError = "The gap between now and the time you logged into your identity provider exceeds the allowed limit."; + } + else if (authnskew.first && authnskew.second && s->getAuthnInstant() == nullptr) { + contextualError = "Your identity provider did not supply a time of login, violating local policy."; + } + else if (!ssoStatement || s->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) { + ssoStatement = &(*s); + } } // Save off the first valid Subject, but favor an unencrypted NameID over anything else. - if (!ssoSubject || (!ssoSubject->getNameID() && (*a)->getSubject()->getNameID())) - ssoSubject = (*a)->getSubject(); + if (!ssoSubject || (!ssoSubject->getNameID() && a->getSubject()->getNameID())) + ssoSubject = a->getSubject(); } - catch (exception& ex) { + catch (std::exception& ex) { m_log.warn("detected a problem with assertion: %s", ex.what()); if (!ssoStatement) contextualError = ex.what(); - badtokens.push_back(*a); + badtokens.push_back(&(*a)); } } // In case we need decryption... - CredentialResolver* cr=application.getCredentialResolver(); + CredentialResolver* cr = application.getCredentialResolver(); if (!cr && !encassertions.empty()) m_log.warn("found encrypted assertions, but no CredentialResolver was available"); - for (vector::const_iterator ea = encassertions.begin(); cr && ea!=encassertions.end(); ++ea) { + for (indirect_iterator::const_iterator> ea = make_indirect_iterator(encassertions.begin()); + ea != make_indirect_iterator(encassertions.end()); ++ea) { // Attempt to decrypt it. - saml2::Assertion* decrypted=NULL; + boost::shared_ptr decrypted; try { Locker credlocker(cr); - auto_ptr mcc( - policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL + scoped_ptr mcc( + policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : nullptr ); - auto_ptr wrapper((*ea)->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get())); - decrypted = dynamic_cast(wrapper.get()); + boost::shared_ptr wrapper( + ea->decrypt( + *cr, + application.getRelyingParty(entity)->getXMLString("entityID").second, + mcc.get(), + requireAuthenticatedEncryption.first && requireAuthenticatedEncryption.second + ) + ); + decrypted = dynamic_pointer_cast(wrapper); if (decrypted) { - wrapper.release(); ownedtokens.push_back(decrypted); if (m_log.isDebugEnabled()) m_log.debugStream() << "decrypted Assertion: " << *decrypted << logging::eol; } } - catch (exception& ex) { - m_log.error(ex.what()); + catch (std::exception& ex) { + m_log.error("failed to decrypt assertion: %s", ex.what()); } if (!decrypted) continue; try { // Skip unsigned assertion? - if (!decrypted->getSignature() && flag.first && flag.second) + if (!decrypted->getSignature() && requireSignedAssertions.first && requireSignedAssertions.second) throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy."); + // Run the schema validators against the assertion, since it was hidden by encryption. + SchemaValidators.validate(decrypted.get()); + // We clear the security flag, so we can tell whether the token was secured on its own. policy.setAuthenticated(false); policy.reset(true); @@ -253,58 +324,67 @@ void SAML2Consumer::implementProtocol( extractMessageDetails(*decrypted, samlconstants::SAML20P_NS, policy); // Run the policy over the assertion. Handles replay, freshness, and - // signature verification, assuming the relevant rules are configured. + // signature verification, assuming the relevant rules are configured, + // along with condition and profile enforcement. // We have to marshall the object first to ensure signatures can be checked. if (!decrypted->getDOM()) decrypted->marshall(); - policy.evaluate(*decrypted); + policy.evaluate(*decrypted, &httpRequest); // If no security is in place now, we kick it. 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); + // If we hadn't established Issuer yet, redo the signedAssertions check. + if (!entity && policy.getIssuerMetadata()) { + entity = dynamic_cast(policy.getIssuerMetadata()->getParent()); + requireSignedAssertions = application.getRelyingParty(entity)->getBool("requireSignedAssertions"); + if (!decrypted->getSignature() && requireSignedAssertions.first && requireSignedAssertions.second) + throw SecurityPolicyException("The decrypted assertion was unsigned, violating local security policy."); + } // Address checking. - checkAddress(application, httpRequest, ssoValidator.getAddress()); + SubjectConfirmationData* subcondata = dynamic_cast( + dynamic_cast(policy).getSubjectConfirmation()->getSubjectConfirmationData() + ); + if (subcondata && subcondata->getAddress()) { + auto_ptr_char boundip(subcondata->getAddress()); + checkAddress(application, httpRequest, boundip.get()); + } // Track it as a valid token. - tokens.push_back(decrypted); + tokens.push_back(decrypted.get()); // 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 (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second)) + const vector& statements = const_cast(decrypted.get())->getAuthnStatements(); + for (indirect_iterator::const_iterator> s = make_indirect_iterator(statements.begin()); + s != make_indirect_iterator(statements.end()); ++s) { + 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; + else if (!ssoStatement || s->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) + ssoStatement = &(*s); } // Save off the first valid Subject, but favor an unencrypted NameID over anything else. if (!ssoSubject || (!ssoSubject->getNameID() && decrypted->getSubject()->getNameID())) ssoSubject = decrypted->getSubject(); } - catch (exception& ex) { + catch (std::exception& ex) { m_log.warn("detected a problem with assertion: %s", ex.what()); if (!ssoStatement) contextualError = ex.what(); - badtokens.push_back(decrypted); + badtokens.push_back(decrypted.get()); } } if (!ssoStatement) { - for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); if (contextualError.empty()) throw FatalProfileException("A valid authentication statement was not found in the incoming message."); throw FatalProfileException(contextualError.c_str()); } // May need to decrypt NameID. - bool ownedName = false; + scoped_ptr decryptedID; NameID* ssoName = ssoSubject->getNameID(); if (!ssoName) { EncryptedID* encname = ssoSubject->getEncryptedID(); @@ -313,21 +393,19 @@ void SAML2Consumer::implementProtocol( m_log.warn("found encrypted NameID, but no decryption credential was available"); else { Locker credlocker(cr); - auto_ptr mcc( - policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL + scoped_ptr mcc( + policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : nullptr ); try { - auto_ptr decryptedID(encname->decrypt(*cr,application.getRelyingParty(entity)->getXMLString("entityID").second,mcc.get())); + decryptedID.reset(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) { - m_log.error(ex.what()); + catch (std::exception& ex) { + m_log.error("failed to decrypt NameID: %s", ex.what()); } } } @@ -353,54 +431,71 @@ void SAML2Consumer::implementProtocol( const AuthnContext* authnContext = ssoStatement->getAuthnContext(); - try { - // The context will handle deleting attributes and new tokens. - auto_ptr ctx( - 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()); - } - - // Now merge in bad tokens for caching. - tokens.insert(tokens.end(), badtokens.begin(), badtokens.end()); - - application.getServiceProvider().getSessionCache()->insert( + // The context will handle deleting attributes and new tokens. + scoped_ptr ctx( + resolveAttributes( application, - httpRequest, - httpResponse, - sessionExp, - entity, + &httpRequest, + policy.getIssuerMetadata(), samlconstants::SAML20P_NS, + response, + nullptr, + nullptr, ssoName, - ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : NULL, - ssoStatement->getSessionIndex(), - (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL, - (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL, - &tokens, - ctx.get() ? &ctx->getResolvedAttributes() : NULL - ); - - if (ownedName) - delete ssoName; - for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); + ssoStatement, + (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : nullptr, + (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : nullptr, + &tokens + ) + ); + + if (ctx) { + // Copy over any new tokens, but leave them in the context for cleanup. + tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end()); + } + + // Now merge in bad tokens for caching. + tokens.insert(tokens.end(), badtokens.begin(), badtokens.end()); + + string session_id; + application.getServiceProvider().getSessionCache()->insert( + session_id, + application, + httpRequest, + httpResponse, + sessionExp, + entity, + samlconstants::SAML20P_NS, + ssoName, + ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : nullptr, + ssoStatement->getSessionIndex(), + (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : nullptr, + (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : nullptr, + &tokens, + ctx ? &ctx->getResolvedAttributes() : nullptr + ); + + try { + scoped_ptr event(newLoginEvent(application, httpRequest)); + LoginEvent* login_event = dynamic_cast(event.get()); + if (login_event) { + login_event->m_sessionID = session_id.c_str(); + login_event->m_peer = entity; + auto_ptr_char prot(getProtocolFamily()); + login_event->m_protocol = prot.get(); + login_event->m_nameID = ssoName; + login_event->m_saml2AuthnStatement = ssoStatement; + login_event->m_saml2Response = response; + if (ctx) + login_event->m_attributes = &ctx->getResolvedAttributes(); + application.getServiceProvider().getTransactionLog()->write(*login_event); + } + else { + m_log.warn("unable to audit event, log event object was of an incorrect type"); + } } - catch (exception&) { - if (ownedName) - delete ssoName; - for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - throw; + catch (std::exception& ex) { + m_log.warn("exception auditing event: %s", ex.what()); } }