From 22a860b84dcd77f8a3163939dac7e0f7fb334911 Mon Sep 17 00:00:00 2001 From: cantor Date: Wed, 7 Mar 2007 20:36:08 +0000 Subject: [PATCH] Move token validation into SAML library, first draft SAML 1 SSO handler. git-svn-id: https://svn.middleware.georgetown.edu/cpp-sp/trunk@2186 cb58f699-b61c-0410-a6fe-9272a202ed29 --- shibsp/Application.h | 12 - shibsp/Makefile.am | 5 +- shibsp/SPConfig.cpp | 1 + shibsp/attribute/resolver/ResolutionContext.h | 1 + .../resolver/impl/SimpleAttributeResolver.cpp | 123 ++++------ shibsp/handler/AbstractHandler.h | 20 +- shibsp/handler/AssertionConsumerService.h | 130 ++++++++++ shibsp/handler/Handler.h | 3 + shibsp/handler/RemotedHandler.h | 15 +- shibsp/handler/impl/AbstractHandler.cpp | 73 +++++- shibsp/handler/impl/AssertionConsumerService.cpp | 266 +++++++++++++++++++++ shibsp/handler/impl/RemotedHandler.cpp | 50 +++- shibsp/handler/impl/SAML1Consumer.cpp | 208 ++++++++++++++++ shibsp/impl/XMLServiceProvider.cpp | 131 ---------- shibsp/shibsp.vcproj | 12 + 15 files changed, 815 insertions(+), 235 deletions(-) create mode 100644 shibsp/handler/AssertionConsumerService.h create mode 100644 shibsp/handler/impl/AssertionConsumerService.cpp create mode 100644 shibsp/handler/impl/SAML1Consumer.cpp diff --git a/shibsp/Application.h b/shibsp/Application.h index 89b31bb..113d743 100644 --- a/shibsp/Application.h +++ b/shibsp/Application.h @@ -26,7 +26,6 @@ #include #include #include -#include namespace shibsp { @@ -164,17 +163,6 @@ namespace shibsp { * @return set of audience values associated with the Application */ virtual const std::vector& getAudiences() const=0; - - /** - * Returns a validator for applying verification rules to incoming SAML tokens. - * - *

The validator must be freed by the caller. - * - * @param ts timestamp against which to evaluate the token's validity, or 0 to ignore - * @param role metadata role of token issuer, if known - * @return a validator - */ - virtual xmltooling::Validator* getTokenValidator(time_t ts=0, const opensaml::saml2md::RoleDescriptor* role=NULL) const=0; }; }; diff --git a/shibsp/Makefile.am b/shibsp/Makefile.am index 2d94a68..1a8af1e 100644 --- a/shibsp/Makefile.am +++ b/shibsp/Makefile.am @@ -54,6 +54,7 @@ bindinclude_HEADERS = \ handinclude_HEADERS = \ handler/AbstractHandler.h \ + handler/AssertionConsumerService.h \ handler/Handler.h \ handler/RemotedHandler.h @@ -90,8 +91,10 @@ libshibsp_la_SOURCES = \ attribute/resolver/impl/AttributeResolver.cpp \ attribute/resolver/impl/SimpleAttributeResolver.cpp \ binding/impl/SOAPClient.cpp \ - handler/impl/RemotedHandler.cpp \ handler/impl/AbstractHandler.cpp \ + handler/impl/AssertionConsumerService.cpp \ + handler/impl/RemotedHandler.cpp \ + handler/impl/SAML1Consumer.cpp \ impl/RemotedSessionCache.cpp \ impl/StorageServiceSessionCache.cpp \ impl/XMLAccessControl.cpp \ diff --git a/shibsp/SPConfig.cpp b/shibsp/SPConfig.cpp index baa1e40..ff635ab 100644 --- a/shibsp/SPConfig.cpp +++ b/shibsp/SPConfig.cpp @@ -109,6 +109,7 @@ bool SPInternalConfig::init(const char* catalog_path) registerAttributeDecoders(); registerAttributeFactories(); registerAttributeResolvers(); + registerHandlers(); registerListenerServices(); registerRequestMappers(); registerSessionCaches(); diff --git a/shibsp/attribute/resolver/ResolutionContext.h b/shibsp/attribute/resolver/ResolutionContext.h index f05bcd0..bdc0ecd 100644 --- a/shibsp/attribute/resolver/ResolutionContext.h +++ b/shibsp/attribute/resolver/ResolutionContext.h @@ -32,6 +32,7 @@ namespace shibsp { class SHIBSP_API Application; class SHIBSP_API Session; + class SHIBSP_API Attribute; /** * A context for a resolution request. diff --git a/shibsp/attribute/resolver/impl/SimpleAttributeResolver.cpp b/shibsp/attribute/resolver/impl/SimpleAttributeResolver.cpp index bff9752..268677f 100644 --- a/shibsp/attribute/resolver/impl/SimpleAttributeResolver.cpp +++ b/shibsp/attribute/resolver/impl/SimpleAttributeResolver.cpp @@ -22,6 +22,7 @@ #include "internal.h" #include "Application.h" +#include "ServiceProvider.h" #include "SessionCache.h" #include "attribute/AttributeDecoder.h" #include "attribute/resolver/AttributeResolver.h" @@ -35,10 +36,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -471,6 +474,8 @@ void SimpleResolverImpl::query(ResolutionContext& ctx, const NameIdentifier& nam SecurityPolicy policy; shibsp::SOAPClient soaper(ctx.getApplication(),policy); + const PropertySet* policySettings = ctx.getApplication().getServiceProvider().getPolicySettings(ctx.getApplication().getString("policyId").second); + pair signedAssertions = policySettings->getBool("signedAssertions"); auto_ptr_XMLCh binding(samlconstants::SAML1_BINDING_SOAP); saml1p::Response* response=NULL; @@ -504,44 +509,32 @@ void SimpleResolverImpl::query(ResolutionContext& ctx, const NameIdentifier& nam return; } - time_t now = time(NULL); - const Validator* tokval = ctx.getApplication().getTokenValidator(now, AA); const vector& assertions = const_cast(response)->getAssertions(); - if (assertions.size()==1) { - auto_ptr wrapper(response); - saml1::Assertion* newtoken = assertions.front(); - if (!XMLString::equals(policy.getIssuer() ? policy.getIssuer()->getName() : NULL, newtoken->getIssuer())) { - log.error("assertion issued by someone other than AA, rejecting it"); - return; - } - try { - tokval->validate(newtoken); - } - catch (exception& ex) { - log.error("assertion failed validation check: %s", ex.what()); - } - newtoken->detach(); - wrapper.release(); - ctx.getResolvedAssertions().push_back(newtoken); - resolve(ctx, newtoken, attributes); + if (assertions.size()>1) + log.warn("simple resolver only supports one assertion in the query response"); + + auto_ptr wrapper(response); + saml1::Assertion* newtoken = assertions.front(); + + if (!newtoken->getSignature() && signedAssertions.first && signedAssertions.second) { + log.error("assertion unsigned, rejecting it based on signedAssertions policy"); + return; } - else { - auto_ptr wrapper(response); - for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { - if (!XMLString::equals(policy.getIssuer() ? policy.getIssuer()->getName() : NULL, (*a)->getIssuer())) { - log.error("assertion issued by someone other than AA, rejecting it"); - continue; - } - try { - tokval->validate(*a); - } - catch (exception& ex) { - log.error("assertion failed validation check: %s", ex.what()); - } - resolve(ctx, *a, attributes); - ctx.getResolvedAssertions().push_back((*a)->cloneAssertion()); - } + + try { + policy.evaluate(*newtoken); + if (!policy.isSecure()) + throw SecurityPolicyException("Security of SAML 1.x query result not established."); + saml1::AssertionValidator tokval(ctx.getApplication().getAudiences(), time(NULL)); + tokval.validateAssertion(*newtoken); + } + catch (exception& ex) { + log.error("assertion failed policy/validation: %s", ex.what()); } + newtoken->detach(); + wrapper.release(); + ctx.getResolvedAssertions().push_back(newtoken); + resolve(ctx, newtoken, attributes); } void SimpleResolverImpl::query(ResolutionContext& ctx, const NameID& nameid, const vector* attributes) const @@ -564,6 +557,8 @@ void SimpleResolverImpl::query(ResolutionContext& ctx, const NameID& nameid, con SecurityPolicy policy; shibsp::SOAPClient soaper(ctx.getApplication(),policy); + const PropertySet* policySettings = ctx.getApplication().getServiceProvider().getPolicySettings(ctx.getApplication().getString("policyId").second); + pair signedAssertions = policySettings->getBool("signedAssertions"); auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP); saml2p::StatusResponseType* srt=NULL; @@ -602,44 +597,32 @@ void SimpleResolverImpl::query(ResolutionContext& ctx, const NameID& nameid, con return; } - time_t now = time(NULL); - const Validator* tokval = ctx.getApplication().getTokenValidator(now, AA); const vector& assertions = const_cast(response)->getAssertions(); - if (assertions.size()==1) { - auto_ptr wrapper(response); - saml2::Assertion* newtoken = assertions.front(); - if (!XMLString::equals(policy.getIssuer() ? policy.getIssuer()->getName() : NULL, newtoken->getIssuer() ? newtoken->getIssuer()->getName() : NULL)) { - log.error("assertion issued by someone other than AA, rejecting it"); - return; - } - try { - tokval->validate(newtoken); - } - catch (exception& ex) { - log.error("assertion failed validation check: %s", ex.what()); - } - newtoken->detach(); - wrapper.release(); - ctx.getResolvedAssertions().push_back(newtoken); - resolve(ctx, newtoken, attributes); + if (assertions.size()>1) + log.warn("simple resolver only supports one assertion in the query response"); + + auto_ptr wrapper(response); + saml2::Assertion* newtoken = assertions.front(); + + if (!newtoken->getSignature() && signedAssertions.first && signedAssertions.second) { + log.error("assertion unsigned, rejecting it based on signedAssertions policy"); + return; } - else { - auto_ptr wrapper(response); - for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { - if (!XMLString::equals(policy.getIssuer() ? policy.getIssuer()->getName() : NULL, (*a)->getIssuer() ? (*a)->getIssuer()->getName() : NULL)) { - log.error("assertion issued by someone other than AA, rejecting it"); - return; - } - try { - tokval->validate(*a); - } - catch (exception& ex) { - log.error("assertion failed validation check: %s", ex.what()); - } - resolve(ctx, *a, attributes); - ctx.getResolvedAssertions().push_back((*a)->cloneAssertion()); - } + + try { + policy.evaluate(*newtoken); + if (!policy.isSecure()) + throw SecurityPolicyException("Security of SAML 2.0 query result not established."); + saml2::AssertionValidator tokval(ctx.getApplication().getAudiences(), time(NULL)); + tokval.validateAssertion(*newtoken); + } + catch (exception& ex) { + log.error("assertion failed policy/validation: %s", ex.what()); } + newtoken->detach(); + wrapper.release(); + ctx.getResolvedAssertions().push_back(newtoken); + resolve(ctx, newtoken, attributes); } void SimpleResolver::resolveAttributes(ResolutionContext& ctx, const vector* attributes) const diff --git a/shibsp/handler/AbstractHandler.h b/shibsp/handler/AbstractHandler.h index ba79ea5..4f1cb85 100644 --- a/shibsp/handler/AbstractHandler.h +++ b/shibsp/handler/AbstractHandler.h @@ -25,6 +25,8 @@ #include #include +#include +#include namespace shibsp { @@ -47,11 +49,25 @@ namespace shibsp { * @param remapper optional map of property rename rules for legacy property support */ AbstractHandler( - const xercesc::DOMElement* e, - xercesc::DOMNodeFilter* filter=NULL, + const DOMElement* e, + log4cpp::Category& log, + DOMNodeFilter* filter=NULL, const std::map* remapper=NULL ); + + /** + * Examines a protocol response message for errors and raises an annotated exception + * if an error is found. + * + *

The base class version understands SAML 1.x and SAML 2.0 responses. + * + * @param response a response message of some known protocol + */ + virtual void checkError(const xmltooling::XMLObject* response) const; + /** Logging object. */ + log4cpp::Category& m_log; + public: virtual ~AbstractHandler() {} }; diff --git a/shibsp/handler/AssertionConsumerService.h b/shibsp/handler/AssertionConsumerService.h new file mode 100644 index 0000000..5466012 --- /dev/null +++ b/shibsp/handler/AssertionConsumerService.h @@ -0,0 +1,130 @@ +/* + * Copyright 2001-2007 Internet2 + * + * 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 + * + * 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. + */ + +/** + * @file shibsp/handler/AssertionConsumerService.h + * + * Base class for handlers that create sessions by consuming SSO protocol responses. + */ + +#ifndef __shibsp_acshandler_h__ +#define __shibsp_acshandler_h__ + +#include +#include +#include +#include + +namespace shibsp { + + class SHIBSP_API ResolutionContext; + +#if defined (_MSC_VER) + #pragma warning( push ) + #pragma warning( disable : 4250 ) +#endif + + /** + * Base class for handlers that create sessions by consuming SSO protocol responses. + */ + class SHIBSP_API AssertionConsumerService : public AbstractHandler, public RemotedHandler + { + public: + virtual ~AssertionConsumerService(); + + std::pair run(SPRequest& request, bool isHandler=true) const; + void receive(DDF& in, std::ostream& out); + + protected: + AssertionConsumerService(const DOMElement* e, log4cpp::Category& log); + + /** + * Implement protocol-specific handling of the incoming decoded message. + * + *

The result of implementing the protocol should be an exception or + * the key to a newly created session. + * + * @param application reference to application receiving message + * @param httpRequest client request that included message + * @param policy the SecurityPolicy in effect, after having evaluated the message + * @param settings policy configuration settings in effect + * @param xmlObject a protocol-specific message object + * @return the key to the newly created session + */ + virtual std::string implementProtocol( + const Application& application, + const opensaml::HTTPRequest& httpRequest, + opensaml::SecurityPolicy& policy, + const PropertySet* settings, + const xmltooling::XMLObject& xmlObject + ) const=0; + + /** + * Enforce address checking requirements. + * + * @param application reference to application receiving message + * @param httpRequest client request that initiated session + * @param issuedTo address for which security assertion was issued + */ + void checkAddress( + const Application& application, const opensaml::HTTPRequest& httpRequest, const char* issuedTo + ) const; + + /** + * Attempt SSO-initiated attribute resolution using the supplied information. + * + *

The caller must free the returned context handle. + * + * @param application reference to application receiving message + * @param httpRequest client request that initiated session + * @param issuer source of SSO tokens + * @param nameid identifier of principal + * @param tokens tokens to resolve, if any + */ + ResolutionContext* resolveAttributes( + const Application& application, + const opensaml::HTTPRequest& httpRequest, + const opensaml::saml2md::EntityDescriptor* issuer, + const opensaml::saml2::NameID& nameid, + const std::vector* tokens=NULL + ) const; + + private: + std::string processMessage( + const Application& application, + const opensaml::HTTPRequest& httpRequest, + std::string& providerId, + std::string& relayState + ) const; + + std::pair sendRedirect( + SPRequest& request, const char* key, const char* providerId, const char* relayState + ) const; + + void maintainHistory(SPRequest& request, const char* providerId, const char* cookieProps) const; + + opensaml::MessageDecoder* m_decoder; + xmltooling::auto_ptr_char m_configNS; + xmltooling::QName m_role; + }; + +#if defined (_MSC_VER) + #pragma warning( pop ) +#endif + +}; + +#endif /* __shibsp_acshandler_h__ */ diff --git a/shibsp/handler/Handler.h b/shibsp/handler/Handler.h index c26c3c8..94ccefa 100644 --- a/shibsp/handler/Handler.h +++ b/shibsp/handler/Handler.h @@ -52,6 +52,9 @@ namespace shibsp { */ virtual std::pair run(SPRequest& request, bool isHandler=true) const=0; }; + + /** Registers Handler implementations. */ + void SHIBSP_API registerHandlers(); }; #endif /* __shibsp_handler_h__ */ diff --git a/shibsp/handler/RemotedHandler.h b/shibsp/handler/RemotedHandler.h index 3006842..9420bb5 100644 --- a/shibsp/handler/RemotedHandler.h +++ b/shibsp/handler/RemotedHandler.h @@ -35,22 +35,21 @@ namespace shibsp { class SHIBSP_API RemotedHandler : public virtual Handler, public Remoted { public: - virtual ~RemotedHandler() {} + virtual ~RemotedHandler(); protected: - RemotedHandler() {} + RemotedHandler(); /** - * Wraps a request by annotating an outgoing data flow with the data needed + * Wraps a request by creating an outgoing data flow with the data needed * to remote the request information. * * @param request an SPRequest to remote - * @param in the dataflow object to annotate * @param headers array of request headers to copy to remote request * @param certs true iff client certificates should be available for the remote request * @return the input dataflow object */ - const DDF& wrap(const SPRequest& request, DDF& in, const std::vector& headers, bool certs=false) const; + DDF wrap(const SPRequest& request, const std::vector* headers=NULL, bool certs=false) const; /** * Unwraps a response by examining an incoming data flow to determine @@ -77,6 +76,12 @@ namespace shibsp { * @return a call-specific response object, to be freed by the caller */ opensaml::HTTPResponse* getResponse(DDF& out) const; + + /** Message address for remote half. */ + std::string m_address; + + private: + static unsigned int m_counter; }; }; diff --git a/shibsp/handler/impl/AbstractHandler.cpp b/shibsp/handler/impl/AbstractHandler.cpp index 9fc7cf3..823751c 100644 --- a/shibsp/handler/impl/AbstractHandler.cpp +++ b/shibsp/handler/impl/AbstractHandler.cpp @@ -21,14 +21,81 @@ */ #include "internal.h" +#include "exceptions.h" #include "handler/AbstractHandler.h" +#include +#include +#include + using namespace shibsp; +using namespace samlconstants; +using namespace opensaml; +using namespace xmltooling; using namespace xercesc; using namespace std; +namespace shibsp { + SHIBSP_DLLLOCAL PluginManager::Factory SAML1ConsumerFactory; +}; + +void SHIBSP_API shibsp::registerHandlers() +{ + SPConfig& conf=SPConfig::getConfig(); + conf.AssertionConsumerServiceManager.registerFactory(SAML1_PROFILE_BROWSER_ARTIFACT, SAML1ConsumerFactory); + conf.AssertionConsumerServiceManager.registerFactory(SAML1_PROFILE_BROWSER_POST, SAML1ConsumerFactory); +} + AbstractHandler::AbstractHandler( - const DOMElement* e, DOMNodeFilter* filter, const map* remapper - ) { - load(e,log4cpp::Category::getInstance(SHIBSP_LOGCAT".Handler"),filter,remapper); + const DOMElement* e, log4cpp::Category& log, DOMNodeFilter* filter, const map* remapper + ) : m_log(log) { + load(e,log,filter,remapper); +} + +void AbstractHandler::checkError(const XMLObject* response) const +{ + const saml2p::StatusResponseType* r2 = dynamic_cast(response); + if (r2) { + const saml2p::Status* status = r2->getStatus(); + if (status) { + const saml2p::StatusCode* sc = status->getStatusCode(); + const XMLCh* code = sc ? sc->getValue() : NULL; + if (code && !XMLString::equals(code,saml2p::StatusCode::SUCCESS)) { + FatalProfileException ex("SAML Response message contained an error."); + auto_ptr_char c1(code); + ex.addProperty("code", c1.get()); + if (sc->getStatusCode()) { + code = sc->getStatusCode()->getValue(); + auto_ptr_char c2(code); + ex.addProperty("code2", c2.get()); + } + if (status->getStatusMessage()) { + auto_ptr_char msg(status->getStatusMessage()->getMessage()); + ex.addProperty("message", msg.get()); + } + } + } + } + + const saml1p::Response* r1 = dynamic_cast(response); + if (r1) { + const saml1p::Status* status = r1->getStatus(); + if (status) { + const saml1p::StatusCode* sc = status->getStatusCode(); + const QName* code = sc ? sc->getValue() : NULL; + if (code && *code != saml1p::StatusCode::SUCCESS) { + FatalProfileException ex("SAML Response message contained an error."); + ex.addProperty("code", code->toString().c_str()); + if (sc->getStatusCode()) { + code = sc->getStatusCode()->getValue(); + if (code) + ex.addProperty("code2", code->toString().c_str()); + } + if (status->getStatusMessage()) { + auto_ptr_char msg(status->getStatusMessage()->getMessage()); + ex.addProperty("message", msg.get()); + } + } + } + } } diff --git a/shibsp/handler/impl/AssertionConsumerService.cpp b/shibsp/handler/impl/AssertionConsumerService.cpp new file mode 100644 index 0000000..2be942f --- /dev/null +++ b/shibsp/handler/impl/AssertionConsumerService.cpp @@ -0,0 +1,266 @@ +/* + * Copyright 2001-2007 Internet2 + * + * 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 + * + * 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. + */ + +/** + * AssertionConsumerService.cpp + * + * Base class for handlers that create sessions by consuming SSO protocol responses. + */ + +#include "internal.h" +#include "Application.h" +#include "exceptions.h" +#include "ServiceProvider.h" +#include "attribute/resolver/AttributeResolver.h" +#include "attribute/resolver/ResolutionContext.h" +#include "handler/AssertionConsumerService.h" +#include "util/SPConstants.h" + +#include +#include +#include +#include + +using namespace shibspconstants; +using namespace samlconstants; +using namespace shibsp; +using namespace opensaml; +using namespace xmltooling; +using namespace log4cpp; +using namespace std; + +AssertionConsumerService::AssertionConsumerService(const DOMElement* e, Category& log) + : AbstractHandler(e, log), m_configNS(SHIB2SPCONFIG_NS), + m_role(samlconstants::SAML20MD_NS, opensaml::saml2md::IDPSSODescriptor::LOCAL_NAME) +{ + if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) + m_decoder = SAMLConfig::getConfig().MessageDecoderManager.newPlugin(getString("Binding").second,e); +} + +AssertionConsumerService::~AssertionConsumerService() +{ + delete m_decoder; +} + +pair AssertionConsumerService::run(SPRequest& request, bool isHandler) const +{ + SPConfig& conf = SPConfig::getConfig(); + if (conf.isEnabled(SPConfig::OutOfProcess)) { + string relayState, providerId; + string key = processMessage(request.getApplication(), request, providerId, relayState); + return sendRedirect(request, key.c_str(), providerId.c_str(), relayState.c_str()); + } + else { + DDF in = wrap(request); + DDFJanitor jin(in); + in.addmember("application_id").string(request.getApplication().getId()); + DDF out=request.getServiceProvider().getListenerService()->send(in); + DDFJanitor jout(out); + if (!out["key"].isstring()) + throw FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key."); + return sendRedirect(request, out["key"].string(), out["provider_id"].string(), out["RelayState"].string()); + } +} + +void AssertionConsumerService::receive(DDF& in, ostream& out) +{ + // Find application. + const char* aid=in["application_id"].string(); + const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL; + if (!app) { + // Something's horribly wrong. + m_log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)"); + throw ConfigurationException("Unable to locate application for new session, deleted?"); + } + + // Unpack the request. + auto_ptr http(getRequest(in)); + + // Do the work. + string relayState, providerId; + string key = processMessage(*app, *http.get(), providerId, relayState); + + // Repack for return to caller. + DDF ret=DDF(NULL).structure(); + DDFJanitor jret(ret); + ret.addmember("key").string(key.c_str()); + if (!providerId.empty()) + ret.addmember("provider_id").string(providerId.c_str()); + if (!relayState.empty()) + ret.addmember("RelayState").string(relayState.c_str()); + out << ret; +} + +string AssertionConsumerService::processMessage( + const Application& application, const HTTPRequest& httpRequest, string& providerId, string& relayState + ) const +{ + // Locate policy key. + pair policyId = getString("policyId", m_configNS.get()); // namespace-qualified if inside handler element + if (!policyId.first) + policyId = application.getString("policyId"); // unqualified in Application(s) element + + // Access policy properties. + const PropertySet* settings = application.getServiceProvider().getPolicySettings(policyId.second); + pair validate = settings->getBool("validate"); + + // Lock metadata for use by policy. + Locker metadataLocker(application.getMetadataProvider()); + + // Create the policy. + SecurityPolicy policy( + application.getServiceProvider().getPolicyRules(policyId.second), + application.getMetadataProvider(), + &m_role, + application.getTrustEngine(), + validate.first && validate.second + ); + + // Decode the message and process it in a protocol-specific way. + auto_ptr msg(m_decoder->decode(relayState, httpRequest, policy)); + string key = implementProtocol(application, httpRequest, policy, settings, *msg.get()); + + auto_ptr_char issuer(policy.getIssuer() ? policy.getIssuer()->getName() : NULL); + if (issuer.get()) + providerId = issuer.get(); + + return key; +} + +pair AssertionConsumerService::sendRedirect( + SPRequest& request, const char* key, const char* providerId, const char* relayState + ) const +{ + string s,k(key); + + if (relayState && !strcmp(relayState,"default")) { + pair homeURL=request.getApplication().getString("homeURL"); + relayState=homeURL.first ? homeURL.second : "/"; + } + else if (!relayState || !strcmp(relayState,"cookie")) { + // Pull the value from the "relay state" cookie. + pair relay_cookie = request.getApplication().getCookieNameProps("_shibstate_"); + relayState = request.getCookie(relay_cookie.first.c_str()); + if (!relayState || !*relayState) { + // No apparent relay state value to use, so fall back on the default. + pair homeURL=request.getApplication().getString("homeURL"); + relayState=homeURL.first ? homeURL.second : "/"; + } + else { + char* rscopy=strdup(relayState); + SAMLConfig::getConfig().getURLEncoder()->decode(rscopy); + s=rscopy; + free(rscopy); + relayState=s.c_str(); + } + request.setCookie(relay_cookie.first.c_str(),relay_cookie.second); + } + + // We've got a good session, so set the session cookie. + pair shib_cookie=request.getApplication().getCookieNameProps("_shibsession_"); + k += shib_cookie.second; + request.setCookie(shib_cookie.first.c_str(), k.c_str()); + + // History cookie. + maintainHistory(request, providerId, shib_cookie.second); + + // Now redirect to the target. + return make_pair(true, request.sendRedirect(relayState)); +} + +void AssertionConsumerService::checkAddress( + const Application& application, const HTTPRequest& httpRequest, const char* issuedTo + ) const +{ + const PropertySet* props=application.getPropertySet("Sessions"); + pair checkAddress = props ? props->getBool("checkAddress") : make_pair(false,true); + if (!checkAddress.first) + checkAddress.second=true; + + if (checkAddress.second) { + m_log.debug("checking client address"); + if (httpRequest.getRemoteAddr() != issuedTo) { + throw FatalProfileException( + "Your client's current address ($client_addr) differs from the one used when you authenticated " + "to your identity provider. To correct this problem, you may need to bypass a proxy server. " + "Please contact your local support staff or help desk for assistance.", + namedparams(1,"client_addr",httpRequest.getRemoteAddr().c_str()) + ); + } + } +} + +ResolutionContext* AssertionConsumerService::resolveAttributes( + const Application& application, + const HTTPRequest& httpRequest, + const saml2md::EntityDescriptor* issuer, + const saml2::NameID& nameid, + const vector* tokens + ) const +{ + AttributeResolver* resolver = application.getAttributeResolver(); + if (!resolver) { + m_log.info("no AttributeResolver available, skipping resolution"); + return NULL; + } + + try { + m_log.debug("resolving attributes..."); + auto_ptr ctx( + resolver->createResolutionContext(application, httpRequest.getRemoteAddr().c_str(), issuer, nameid, tokens) + ); + resolver->resolveAttributes(*ctx.get()); + return ctx.release(); + } + catch (exception& ex) { + m_log.error("attribute resolution failed: %s", ex.what()); + } + + return NULL; +} + +void AssertionConsumerService::maintainHistory(SPRequest& request, const char* providerId, const char* cookieProps) const +{ + if (!providerId) + return; + + const PropertySet* sessionProps=request.getApplication().getPropertySet("Sessions"); + pair idpHistory=sessionProps->getBool("idpHistory"); + if (!idpHistory.first || idpHistory.second) { + // Set an IdP history cookie locally (essentially just a CDC). + CommonDomainCookie cdc(request.getCookie(CommonDomainCookie::CDCName)); + + // Either leave in memory or set an expiration. + pair days=sessionProps->getUnsignedInt("idpHistoryDays"); + if (!days.first || days.second==0) { + string c = string(cdc.set(providerId)) + cookieProps; + request.setCookie(CommonDomainCookie::CDCName, c.c_str()); + } + else { + time_t now=time(NULL) + (days.second * 24 * 60 * 60); +#ifdef HAVE_GMTIME_R + struct tm res; + struct tm* ptime=gmtime_r(&now,&res); +#else + struct tm* ptime=gmtime(&now); +#endif + char timebuf[64]; + strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime); + string c = string(cdc.set(providerId)) + cookieProps + "; expires=" + timebuf; + request.setCookie(CommonDomainCookie::CDCName, c.c_str()); + } + } +} diff --git a/shibsp/handler/impl/RemotedHandler.cpp b/shibsp/handler/impl/RemotedHandler.cpp index 0728b19..385fa6c 100644 --- a/shibsp/handler/impl/RemotedHandler.cpp +++ b/shibsp/handler/impl/RemotedHandler.cpp @@ -21,6 +21,7 @@ */ #include "internal.h" +#include "ServiceProvider.h" #include "handler/RemotedHandler.h" #include @@ -201,10 +202,35 @@ long RemotedResponse::sendRedirect(const char* url) } -const DDF& RemotedHandler::wrap(const SPRequest& request, DDF& in, const vector& headers, bool certs) const +unsigned int RemotedHandler::m_counter = 0; + +RemotedHandler::RemotedHandler() +{ + m_address += ('A' + (m_counter++)); + m_address += "::run::RemotedHandler"; + + SPConfig& conf = SPConfig::getConfig(); + if (conf.isEnabled(SPConfig::OutOfProcess)) { + ListenerService* listener = conf.getServiceProvider()->getListenerService(false); + if (listener) + listener->regListener(m_address.c_str(),this); + else + Category::getInstance(SHIBSP_LOGCAT".Handler").info("no ListenerService available, handler remoting disabled"); + } +} + +RemotedHandler::~RemotedHandler() { - if (!in.isstruct()) - in.structure(); + SPConfig& conf = SPConfig::getConfig(); + ListenerService* listener=conf.getServiceProvider()->getListenerService(false); + if (listener && conf.isEnabled(SPConfig::OutOfProcess)) + listener->unregListener(m_address.c_str(),this); + m_counter--; +} + +DDF RemotedHandler::wrap(const SPRequest& request, const vector* headers, bool certs) const +{ + DDF in = DDF(m_address.c_str()).structure(); in.addmember("scheme").string(request.getScheme()); in.addmember("hostname").string(request.getHostname()); in.addmember("port").integer(request.getPort()); @@ -218,21 +244,23 @@ const DDF& RemotedHandler::wrap(const SPRequest& request, DDF& in, const vector< in.addmember("url").string(request.getRequestURL()); in.addmember("query").string(request.getQueryString()); - string hdr; - DDF hin = in.addmember("headers").structure(); - for (vector::const_iterator h = headers.begin(); h!=headers.end(); ++h) { - hdr = request.getHeader(h->c_str()); - if (!hdr.empty()) - hin.addmember(h->c_str()).string(hdr.c_str()); + if (headers) { + string hdr; + DDF hin = in.addmember("headers").structure(); + for (vector::const_iterator h = headers->begin(); h!=headers->end(); ++h) { + hdr = request.getHeader(h->c_str()); + if (!hdr.empty()) + hin.addmember(h->c_str()).string(hdr.c_str()); + } } if (certs) { const vector& xvec = request.getClientCertificates(); if (!xvec.empty()) { - hin = in.addmember("certificates").list(); + DDF clist = in.addmember("certificates").list(); for (vector::const_iterator x = xvec.begin(); x!=xvec.end(); ++x) { DDF x509 = DDF(NULL).string((*x)->getDEREncodingSB().rawCharBuffer()); - hin.add(x509); + clist.add(x509); } } } diff --git a/shibsp/handler/impl/SAML1Consumer.cpp b/shibsp/handler/impl/SAML1Consumer.cpp new file mode 100644 index 0000000..b9d45fa --- /dev/null +++ b/shibsp/handler/impl/SAML1Consumer.cpp @@ -0,0 +1,208 @@ +/* + * Copyright 2001-2007 Internet2 + * + * 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 + * + * 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. + */ + +/** + * SAML1Consumer.cpp + * + * SAML 1.x assertion consumer service + */ + +#include "internal.h" +#include "Application.h" +#include "exceptions.h" +#include "ServiceProvider.h" +#include "SessionCache.h" +#include "attribute/resolver/ResolutionContext.h" +#include "handler/AssertionConsumerService.h" + +#include +#include +#include +#include + +using namespace shibsp; +using namespace opensaml::saml1; +using namespace opensaml::saml1p; +using namespace opensaml; +using namespace xmltooling; +using namespace log4cpp; +using namespace std; +using saml2::NameID; +using saml2::NameIDBuilder; +using saml2md::EntityDescriptor; + +namespace shibsp { + +#if defined (_MSC_VER) + #pragma warning( push ) + #pragma warning( disable : 4250 ) +#endif + + class SHIBSP_DLLLOCAL SAML1Consumer : public AssertionConsumerService + { + public: + SAML1Consumer(const DOMElement* e) : AssertionConsumerService(e, Category::getInstance(SHIBSP_LOGCAT".SAML1")) {} + virtual ~SAML1Consumer() {} + + private: + string implementProtocol( + const Application& application, + const HTTPRequest& httpRequest, + SecurityPolicy& policy, + const PropertySet* settings, + const XMLObject& xmlObject + ) const; + }; + +#if defined (_MSC_VER) + #pragma warning( pop ) +#endif + + Handler* SHIBSP_DLLLOCAL SAML1ConsumerFactory(const DOMElement* const & e) + { + return new SAML1Consumer(e); + } + +}; + +string SAML1Consumer::implementProtocol( + const Application& application, + const HTTPRequest& httpRequest, + SecurityPolicy& policy, + const PropertySet* settings, + const XMLObject& xmlObject + ) const +{ + // Implementation of SAML 1.x SSO profile(s). + m_log.debug("processing message against SAML 1.x SSO profile"); + + // With the binding aspects now moved out to the MessageDecoder, + // the focus here is on the assertion content. For SAML 1.x, + // all the security comes from the protocol layer, and signing + // the assertion isn't sufficient. So we can check the policy + // object now and bail if it's not a secure message. + if (!policy.isSecure()) + throw SecurityPolicyException("Security of SAML 1.x SSO response not established."); + + // Check for errors...this will throw if it's not a successful message. + checkError(&xmlObject); + + const Response* response = dynamic_cast(&xmlObject); + if (!response) + throw FatalProfileException("Incoming message was not a samlp:Response."); + + const vector& assertions = response->getAssertions(); + if (assertions.empty()) + throw FatalProfileException("Incoming message contained no SAML assertions."); + + // Maintain list of "legit" tokens to feed to SP subsystems. + const AuthenticationStatement* ssoStatement=NULL; + vector tokens; + + // Profile validator. + time_t now = time(NULL); + BrowserSSOProfileValidator ssoValidator(application.getAudiences(), now); + + // With this flag on, we ignore any unsigned assertions. + pair flag = settings->getBool("signedAssertions"); + 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"); + continue; + } + + try { + // Run the policy over the assertion. Handles issuer consistency, replay, freshness, + // and signature verification, assuming the relevant rules are configured. + policy.evaluate(*(*a)); + + // Now do profile and core semantic validation to ensure we can use it for SSO. + ssoValidator.validateAssertion(*(*a)); + + // Track it as a valid token. + tokens.push_back(*a); + + // Save off the first valid SSO statement. + if (!ssoStatement && !(*a)->getAuthenticationStatements().empty()) + ssoStatement = (*a)->getAuthenticationStatements().front(); + } + catch (exception& ex) { + m_log.warn("profile validation error in assertion: %s", ex.what()); + } + } + + if (!ssoStatement) + throw FatalProfileException("A valid authentication statement was not found in the incoming message."); + + // Address checking. + SubjectLocality* locality = ssoStatement->getSubjectLocality(); + if (locality && locality->getIPAddress()) { + auto_ptr_char ip(locality->getIPAddress()); + checkAddress(application, httpRequest, ip.get()); + } + + m_log.debug("SSO profile processing completed successfully"); + + // We've successfully "accepted" at least one SSO token, along with any additional valid tokens. + // To complete processing, we need to resolve attributes and then create the session. + + // First, normalize the SAML 1.x NameIdentifier... + auto_ptr nameid(NameIDBuilder::buildNameID()); + NameIdentifier* n = ssoStatement->getSubject()->getNameIdentifier(); + if (n) { + nameid->setName(n->getName()); + nameid->setFormat(n->getFormat()); + nameid->setNameQualifier(n->getNameQualifier()); + } + + const EntityDescriptor* issuerMetadata = dynamic_cast(policy.getIssuerMetadata()->getParent()); + auto_ptr ctx( + resolveAttributes(application, httpRequest, issuerMetadata, *nameid.get(), &tokens) + ); + + // Copy over any new tokens, but leave them in the context for cleanup. + tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end()); + + // Now we have to extract the authentication details for session setup. + + // Session expiration for SAML 1.x is purely SP-driven, and the method is mapped to a ctx class. + const PropertySet* sessionProps = application.getPropertySet("Sessions"); + pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : make_pair(true,28800); + if (!lifetime.first) + lifetime.second = 28800; + auto_ptr_char authnInstant( + ssoStatement->getAuthenticationInstant() ? ssoStatement->getAuthenticationInstant()->getRawData() : NULL + ); + auto_ptr_char authnMethod(ssoStatement->getAuthenticationMethod()); + + vector& attrs = ctx->getResolvedAttributes(); + string key = application.getServiceProvider().getSessionCache()->insert( + lifetime.second ? now + lifetime.second : 0, + application, + httpRequest.getRemoteAddr().c_str(), + issuerMetadata, + *nameid.get(), + authnInstant.get(), + NULL, + authnMethod.get(), + NULL, + &tokens, + &attrs + ); + attrs.clear(); // Attributes are owned by cache now. + return key; +} diff --git a/shibsp/impl/XMLServiceProvider.cpp b/shibsp/impl/XMLServiceProvider.cpp index b419c2e..b7d6f39 100644 --- a/shibsp/impl/XMLServiceProvider.cpp +++ b/shibsp/impl/XMLServiceProvider.cpp @@ -66,18 +66,6 @@ namespace { #pragma warning( disable : 4250 ) #endif - class SHIBSP_DLLLOCAL TokenValidator : public Validator - { - public: - TokenValidator(const Application& app, time_t ts=0, const RoleDescriptor* role=NULL) : m_app(app), m_ts(ts), m_role(role) {} - void validate(const XMLObject*) const; - - private: - const Application& m_app; - time_t m_ts; - const RoleDescriptor* m_role; - }; - static vector g_noHandlers; // Application configuration wrapper @@ -122,9 +110,6 @@ namespace { const vector& getAudiences() const { return (m_audiences.empty() && m_base) ? m_base->getAudiences() : m_audiences; } - Validator* getTokenValidator(time_t ts=0, const saml2md::RoleDescriptor* role=NULL) const { - return new TokenValidator(*this, ts, role); - } // Provides filter to exclude special config elements. short acceptNode(const DOMNode* node) const; @@ -358,122 +343,6 @@ namespace shibsp { } }; -void TokenValidator::validate(const XMLObject* xmlObject) const -{ -#ifdef _DEBUG - xmltooling::NDC ndc("validate"); -#endif - Category& log=Category::getInstance(SHIBSP_LOGCAT".Application"); - - const opensaml::Assertion* root = NULL; - const saml2::Assertion* token2 = dynamic_cast(xmlObject); - if (token2) { - const saml2::Conditions* conds = token2->getConditions(); - // First verify the time conditions, using the specified timestamp, if non-zero. - if (m_ts>0 && conds) { - unsigned int skew = XMLToolingConfig::getConfig().clock_skew_secs; - time_t t=conds->getNotBeforeEpoch(); - if (m_ts+skew < t) - throw ValidationException("Assertion is not yet valid."); - t=conds->getNotOnOrAfterEpoch(); - if (t <= m_ts-skew) - throw ValidationException("Assertion is no longer valid."); - } - - // Now we process conditions. Only audience restrictions at the moment. - const vector& convec = conds->getConditions(); - for (vector::const_iterator c = convec.begin(); c!=convec.end(); ++c) { - const saml2::AudienceRestriction* ac=dynamic_cast(*c); - if (!ac) { - log.error("unrecognized Condition in assertion (%s)", - (*c)->getSchemaType() ? (*c)->getSchemaType()->toString().c_str() : (*c)->getElementQName().toString().c_str()); - throw ValidationException("Assertion contains an unrecognized condition."); - } - - bool found = false; - const vector& auds1 = ac->getAudiences(); - const vector& auds2 = m_app.getAudiences(); - for (vector::const_iterator a = auds1.begin(); !found && a!=auds1.end(); ++a) { - for (vector::const_iterator a2 = auds2.begin(); !found && a2!=auds2.end(); ++a2) { - found = XMLString::equals((*a)->getAudienceURI(), *a2); - } - } - - if (!found) { - ostringstream os; - os << *ac; - log.error("unacceptable AudienceRestriction in assertion (%s)", os.str().c_str()); - throw ValidationException("Assertion contains an unacceptable AudienceRestriction."); - } - } - - root = token2; - } - else { - const saml1::Assertion* token1 = dynamic_cast(xmlObject); - if (token1) { - const saml1::Conditions* conds = token1->getConditions(); - // First verify the time conditions, using the specified timestamp, if non-zero. - if (m_ts>0 && conds) { - unsigned int skew = XMLToolingConfig::getConfig().clock_skew_secs; - time_t t=conds->getNotBeforeEpoch(); - if (m_ts+skew < t) - throw ValidationException("Assertion is not yet valid."); - t=conds->getNotOnOrAfterEpoch(); - if (t <= m_ts-skew) - throw ValidationException("Assertion is no longer valid."); - } - - // Now we process conditions. Only audience restrictions at the moment. - const vector& convec = conds->getConditions(); - for (vector::const_iterator c = convec.begin(); c!=convec.end(); ++c) { - const saml1::AudienceRestrictionCondition* ac=dynamic_cast(*c); - if (!ac) { - log.error("unrecognized Condition in assertion (%s)", - (*c)->getSchemaType() ? (*c)->getSchemaType()->toString().c_str() : (*c)->getElementQName().toString().c_str()); - throw ValidationException("Assertion contains an unrecognized condition."); - } - - bool found = false; - const vector& auds1 = ac->getAudiences(); - const vector& auds2 = m_app.getAudiences(); - for (vector::const_iterator a = auds1.begin(); !found && a!=auds1.end(); ++a) { - for (vector::const_iterator a2 = auds2.begin(); !found && a2!=auds2.end(); ++a2) { - found = XMLString::equals((*a)->getAudienceURI(), *a2); - } - } - - if (!found) { - ostringstream os; - os << *ac; - log.error("unacceptable AudienceRestrictionCondition in assertion (%s)", os.str().c_str()); - throw ValidationException("Assertion contains an unacceptable AudienceRestrictionCondition."); - } - } - - root = token1; - } - else { - throw ValidationException("Unknown object type passed to token validator."); - } - } - - if (!m_role || !m_app.getTrustEngine()) { - log.warn("no issuer role or TrustEngine provided, so no signature validation performed"); - return; - } - - const PropertySet* policy=m_app.getServiceProvider().getPolicySettings(m_app.getString("policyId").second); - pair signedAssertions=policy ? policy->getBool("signedAssertions") : make_pair(false,false); - - if (root->getSignature()) { - if (!m_app.getTrustEngine()->validate(*(root->getSignature()),*m_role)) - throw ValidationException("Assertion signature did not validate."); - } - else if (signedAssertions.first && signedAssertions.second) - throw ValidationException("Assertion was unsigned, violating policy."); -} - XMLApplication::XMLApplication( const ServiceProvider* sp, const DOMElement* e, diff --git a/shibsp/shibsp.vcproj b/shibsp/shibsp.vcproj index 3b0a311..c230b57 100644 --- a/shibsp/shibsp.vcproj +++ b/shibsp/shibsp.vcproj @@ -349,9 +349,17 @@ > + + + + @@ -516,6 +524,10 @@ > + + -- 2.1.4