2 * Licensed to the University Corporation for Advanced Internet
3 * Development, Inc. (UCAID) under one or more contributor license
4 * agreements. See the NOTICE file distributed with this work for
5 * additional information regarding copyright ownership.
7 * UCAID licenses this file to you under the Apache License,
8 * Version 2.0 (the "License"); you may not use this file except
9 * in compliance with the License. You may obtain a copy of the
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17 * either express or implied. See the License for the specific
18 * language governing permissions and limitations under the License.
22 * ExternalAuthHandler.cpp
24 * Handler for integrating with external authentication mechanisms.
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SPRequest.h"
32 #include "handler/RemotedHandler.h"
33 #include "handler/SecuredHandler.h"
36 #include <boost/scoped_ptr.hpp>
39 # include "SessionCache.h"
40 # include "TransactionLog.h"
41 # include "attribute/SimpleAttribute.h"
42 # include "attribute/filtering/AttributeFilter.h"
43 # include "attribute/filtering/BasicFilteringContext.h"
44 # include "attribute/resolver/AttributeExtractor.h"
45 # include "attribute/resolver/AttributeResolver.h"
46 # include "attribute/resolver/ResolutionContext.h"
47 # include <boost/tokenizer.hpp>
48 # include <boost/iterator/indirect_iterator.hpp>
49 # include <saml/exceptions.h>
50 # include <saml/saml2/core/Assertions.h>
51 # include <saml/saml2/metadata/Metadata.h>
52 # include <saml/saml2/metadata/MetadataProvider.h>
53 # include <xmltooling/XMLToolingConfig.h>
54 # include <xmltooling/util/DateTime.h>
55 # include <xmltooling/util/ParserPool.h>
56 # include <xmltooling/util/XMLHelper.h>
57 # include <xercesc/framework/MemBufInputSource.hpp>
58 # include <xercesc/framework/Wrapper4InputSource.hpp>
59 using namespace opensaml::saml2md;
60 using namespace opensaml;
62 using saml2::AuthnStatement;
63 using saml2::AuthnContext;
65 # define min(a,b) (((a) < (b)) ? (a) : (b))
69 using namespace shibspconstants;
70 using namespace shibsp;
71 using namespace xmltooling;
72 using namespace boost;
77 #if defined (_MSC_VER)
78 #pragma warning( push )
79 #pragma warning( disable : 4250 )
82 class SHIBSP_API ExternalAuth : public SecuredHandler, public RemotedHandler
85 ExternalAuth(const DOMElement* e, const char* appId);
86 virtual ~ExternalAuth() {}
88 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
89 void receive(DDF& in, ostream& out);
91 const char* getType() const {
92 return "ExternalAuth";
96 pair<bool,long> processMessage(
97 const Application& application, HTTPRequest& httpRequest, HTTPResponse& httpResponse, const DDF* respDDF=nullptr
100 LoginEvent* newLoginEvent(const Application& application, const HTTPRequest& request) const;
101 ResolutionContext* resolveAttributes(
102 const Application& application,
103 const GenericRequest* request,
104 const saml2md::RoleDescriptor* issuer,
105 const XMLCh* protocol,
106 const saml2::NameID* nameid,
107 const saml2::AuthnStatement* statement,
108 const XMLCh* authncontext_class,
109 const XMLCh* authncontext_decl,
110 const vector<const Assertion*>* tokens=nullptr,
111 const vector<Attribute*>* inputAttributes=nullptr
116 #if defined (_MSC_VER)
117 #pragma warning( pop )
120 Handler* SHIBSP_DLLLOCAL ExternalAuthFactory(const pair<const DOMElement*,const char*>& p)
122 return new ExternalAuth(p.first, p.second);
128 static ostream& json_safe(ostream& os, const char* buf)
131 for (; *buf; ++buf) {
162 ExternalAuth::ExternalAuth(const DOMElement* e, const char* appId)
163 : SecuredHandler(e, Category::getInstance(SHIBSP_LOGCAT".ExternalAuth"), "acl", "127.0.0.1 ::1")
165 setAddress("run::ExternalAuth");
168 pair<bool,long> ExternalAuth::run(SPRequest& request, bool isHandler) const
170 // Check ACL in base class.
171 pair<bool,long> ret = SecuredHandler::run(request, isHandler);
176 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
177 // When out of process, we run natively and directly process the message.
178 return processMessage(request.getApplication(), request, request);
181 // When not out of process, we remote all the message processing.
182 vector<string> headers(1, "User-Agent");
183 headers.push_back("Accept");
184 headers.push_back("Accept-Language");
185 headers.push_back("Cookie");
186 DDF out,in = wrap(request, &headers);
187 DDFJanitor jin(in), jout(out);
188 out=request.getServiceProvider().getListenerService()->send(in);
189 return unwrap(request, out);
192 catch (std::exception& ex) {
193 m_log.error("error while processing request: %s", ex.what());
194 istringstream msg("External Authentication Failed");
195 return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_ERROR));
199 void ExternalAuth::receive(DDF& in, ostream& out)
202 const char* aid = in["application_id"].string();
203 const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
205 // Something's horribly wrong.
206 m_log.error("couldn't find application (%s) for external authentication", aid ? aid : "(missing)");
207 throw ConfigurationException("Unable to locate application for external authentication, deleted?");
210 // Unpack the request.
211 scoped_ptr<HTTPRequest> req(getRequest(in));
213 // Wrap a response shim.
215 DDFJanitor jout(ret);
216 scoped_ptr<HTTPResponse> resp(getResponse(ret));
218 // Since we're remoted, the result should either be a throw, a false/0 return,
219 // which we just return as an empty structure, or a response/redirect,
220 // which we capture in the facade and send back.
222 processMessage(*app, *req, *resp, &ret);
224 catch (std::exception& ex) {
225 m_log.error("raising exception: %s", ex.what());
231 pair<bool,long> ExternalAuth::processMessage(
232 const Application& application, HTTPRequest& httpRequest, HTTPResponse& httpResponse, const DDF* respDDF
237 SessionCache* cache = application.getServiceProvider().getSessionCache();
238 MetadataProvider* m = application.getMetadataProvider(false);
241 string ctype(httpRequest.getContentType());
242 if (ctype == "text/xml" || ctype == "application/samlassertion+xml") {
243 // Body should contain an assertion.
244 const char* body = httpRequest.getRequestBody();
246 throw FatalProfileException("Request body was empty.");
248 // Parse and bind the document into an XMLObject.
249 MemBufInputSource src(reinterpret_cast<const XMLByte*>(body), httpRequest.getContentLength(), "SAMLAssertion");
250 Wrapper4InputSource dsrc(&src, false);
251 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(dsrc);
252 XercesJanitor<DOMDocument> janitor(doc);
253 auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
256 saml2::Assertion* token = dynamic_cast<saml2::Assertion*>(xmlObject.get());
258 throw FatalProfileException("Request body did not contain a SAML 2.0 assertion.");
259 else if (token->getAuthnStatements().empty())
260 throw FatalProfileException("Assertion in request did not contain an AuthnStatement.");
262 // We're not implementing a full SAML profile here, only a minimal one that ignores most
263 // security checking, conditions, etc. The caller is in full control here and we just consume
264 // what we're given. The only thing we're honoring is the authentication information we find
265 // and processing any attributes.
267 const XMLCh* protocol = nullptr;
268 pair<const EntityDescriptor*, const RoleDescriptor*> issuer = pair<const EntityDescriptor*, const RoleDescriptor*>(nullptr,nullptr);
269 if (m && token->getIssuer() && token->getIssuer()->getName()) {
270 MetadataProvider::Criteria mc;
271 mc.entityID_unicode = token->getIssuer()->getName();
272 mc.role = &IDPSSODescriptor::ELEMENT_QNAME;
273 mc.protocol = samlconstants::SAML20P_NS;
274 issuer = m->getEntityDescriptor(mc);
276 auto_ptr_char iname(token->getIssuer()->getName());
277 m_log.warn("no metadata found for issuer (%s)", iname.get());
279 else if (!issuer.second) {
280 auto_ptr_char iname(token->getIssuer()->getName());
281 m_log.warn("no IdP role found in metadata for issuer (%s)", iname.get());
283 protocol = mc.protocol;
286 const saml2::NameID* nameid = nullptr;
287 if (token->getSubject())
288 nameid = token->getSubject()->getNameID();
289 const AuthnStatement* ssoStatement = token->getAuthnStatements().front();
291 // authnskew allows rejection of SSO if AuthnInstant is too old.
292 const PropertySet* sessionProps = application.getPropertySet("Sessions");
293 pair<bool,unsigned int> authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair<bool,unsigned int>(false,0);
295 time_t now(time(nullptr));
296 if (ssoStatement->getAuthnInstant() &&
297 ssoStatement->getAuthnInstantEpoch() - XMLToolingConfig::getConfig().clock_skew_secs > now) {
298 throw FatalProfileException("The AuthnInstant was future-dated.");
300 else if (authnskew.first && authnskew.second && ssoStatement->getAuthnInstant() &&
301 ssoStatement->getAuthnInstantEpoch() <= now && (now - ssoStatement->getAuthnInstantEpoch() > authnskew.second)) {
302 throw FatalProfileException("The gap between now and the AuthnInstant exceeds the allowed limit.");
304 else if (authnskew.first && authnskew.second && ssoStatement->getAuthnInstant() == nullptr) {
305 throw FatalProfileException("No AuthnInstant was supplied, violating local policy.");
308 // Session expiration for SAML 2.0 is jointly IdP- and SP-driven.
309 time_t sessionExp = ssoStatement->getSessionNotOnOrAfter() ?
310 (ssoStatement->getSessionNotOnOrAfterEpoch() + XMLToolingConfig::getConfig().clock_skew_secs) : 0;
311 pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
312 if (!lifetime.first || lifetime.second == 0)
313 lifetime.second = 28800;
315 sessionExp = now + lifetime.second; // IdP says nothing, calulate based on SP.
317 sessionExp = min(sessionExp, now + lifetime.second); // Use the lowest.
319 const XMLCh* authncontext_class = nullptr;
320 const XMLCh* authncontext_decl = nullptr;
321 const AuthnContext* authnContext = ssoStatement->getAuthnContext();
323 authncontext_class = authnContext->getAuthnContextClassRef() ? authnContext->getAuthnContextClassRef()->getReference() : nullptr;
324 authncontext_decl = authnContext->getAuthnContextDeclRef() ? authnContext->getAuthnContextDeclRef()->getReference() : nullptr;
327 // The context will handle deleting attributes and tokens.
328 vector<const Assertion*> tokens(1, token);
329 scoped_ptr<ResolutionContext> ctx(
342 tokens.clear(); // don't store the original token in the session, since it was contrived
345 // Copy over any new tokens, but leave them in the context for cleanup.
346 tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
358 ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : nullptr,
359 ssoStatement->getSessionIndex(),
363 &ctx->getResolvedAttributes()
366 else if (ctype == "application/x-www-form-urlencoded") {
367 auto_ptr_XMLCh protocol(httpRequest.getParameter("protocol"));
368 const char* param = httpRequest.getParameter("issuer");
369 pair<const EntityDescriptor*, const RoleDescriptor*> issuer = pair<const EntityDescriptor*, const RoleDescriptor*>(nullptr,nullptr);
370 if (m && param && *param) {
371 MetadataProvider::Criteria mc;
372 mc.entityID_ascii = param;
373 mc.role = &IDPSSODescriptor::ELEMENT_QNAME;
374 mc.protocol = protocol.get();
375 issuer = m->getEntityDescriptor(mc);
377 m_log.warn("no metadata found for issuer (%s)", param);
378 else if (!issuer.second)
379 m_log.warn("no IdP role found in metadata for issuer (%s)", param);
382 scoped_ptr<saml2::NameID> nameid;
383 param = httpRequest.getParameter("NameID");
384 if (param && *param) {
385 nameid.reset(saml2::NameIDBuilder::buildNameID());
386 auto_arrayptr<XMLCh> n(fromUTF8(param));
387 nameid->setName(n.get());
388 param = httpRequest.getParameter("Format");
389 if (param && param) {
390 auto_ptr_XMLCh f(param);
391 nameid->setFormat(f.get());
395 scoped_ptr<DateTime> authn_instant;
396 param = httpRequest.getParameter("AuthnInstant");
397 if (param && *param) {
398 auto_ptr_XMLCh d(param);
399 authn_instant.reset(new DateTime(d.get()));
400 authn_instant->parseDateTime();
403 auto_ptr_XMLCh session_index(httpRequest.getParameter("SessionIndex"));
404 auto_ptr_XMLCh authncontext_class(httpRequest.getParameter("AuthnContextClassRef"));
405 auto_ptr_XMLCh authncontext_decl(httpRequest.getParameter("AuthnContextDeclRef"));
407 time_t sessionExp = 0;
408 param = httpRequest.getParameter("lifetime");
410 sessionExp = atol(param);
412 sessionExp += time(nullptr);
415 const PropertySet* sessionProps = application.getPropertySet("Sessions");
416 pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
417 if (!lifetime.first || lifetime.second == 0)
418 lifetime.second = 28800;
419 sessionExp = time(nullptr) + lifetime.second;
422 // Create simple attributes around whatever parameters are specified.
423 vector<Attribute*> resolvedAttributes;
424 param = httpRequest.getParameter("attributes");
425 if (param && *param) {
426 char_separator<char> sep(", ");
428 tokenizer< char_separator<char> > tokens(dup, sep);
430 for (tokenizer< char_separator<char> >::iterator t = tokens.begin(); t != tokens.end(); ++t) {
431 vector<const char*> vals;
432 if (httpRequest.getParameters(t->c_str(), vals)) {
433 vector<string> ids(1, *t);
434 auto_ptr<SimpleAttribute> attr(new SimpleAttribute(ids));
435 vector<string>& dest = attr->getValues();
436 for (vector<const char*>::const_iterator v = vals.begin(); v != vals.end(); ++v)
438 resolvedAttributes.push_back(attr.get());
443 catch (std::exception&) {
444 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
449 scoped_ptr<ResolutionContext> ctx(
457 authncontext_class.get(),
458 authncontext_decl.get(),
464 vector<const Assertion*> tokens;
466 // Copy over any new tokens, but leave them in the context for cleanup.
467 tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
479 authn_instant ? authn_instant->getRawData() : nullptr,
481 authncontext_class.get(),
482 authncontext_decl.get(),
484 &ctx->getResolvedAttributes()
488 throw FatalProfileException("Submission was not in a recognized SAML assertion or form-encoded format.");
491 const char* param = httpRequest.getParameter("RelayState");
492 string target(param ? param : "");
494 recoverRelayState(application, httpRequest, httpResponse, target);
496 catch (std::exception& ex) {
497 m_log.error("error recovering relay state: %s", ex.what());
502 string accept = httpRequest.getHeader("Accept");
503 if (accept.find("application/json") != string::npos) {
504 httpResponse.setContentType("application/json");
505 os << "{ \"SessionID\": "; json_safe(os, session_id.c_str());
506 bool firstCookie = true;
509 DDF hdrs = respDDF->getmember("headers");
511 while (hdr.isstring()) {
512 if (!strcmp(hdr.name(), "Set-Cookie")) {
514 os << ", \"Cookies\": [ ";
520 json_safe(os, hdr.string());
527 os << ", \"RelayState\": "; json_safe(os, target.c_str());
531 httpResponse.setContentType("text/xml");
532 static const XMLCh _ExternalAuth[] = UNICODE_LITERAL_12(E,x,t,e,r,n,a,l,A,u,t,h);
533 static const XMLCh _SessionID[] = UNICODE_LITERAL_9(S,e,s,s,i,o,n,I,D);
534 static const XMLCh _RelayState[] = UNICODE_LITERAL_10(R,e,l,a,y,S,t,a,t,e);
535 static const XMLCh _Cookie[] = UNICODE_LITERAL_6(C,o,o,k,i,e);
536 DOMDocument* retdoc = XMLToolingConfig::getConfig().getParser().newDocument();
537 XercesJanitor<DOMDocument> retjanitor(retdoc);
538 retdoc->appendChild(retdoc->createElement(_ExternalAuth));
539 auto_ptr_XMLCh wideid(session_id.c_str());
540 DOMElement* child = retdoc->createElement(_SessionID);
541 child->appendChild(retdoc->createTextNode(wideid.get()));
542 retdoc->getDocumentElement()->appendChild(child);
545 DDF hdrs = respDDF->getmember("headers");
547 while (hdr.isstring()) {
548 if (!strcmp(hdr.name(), "Set-Cookie")) {
549 child = retdoc->createElement(_Cookie);
550 auto_ptr_XMLCh wideval(hdr.string());
551 child->appendChild(retdoc->createTextNode(wideval.get()));
552 retdoc->getDocumentElement()->appendChild(child);
557 if (!target.empty()) {
558 auto_ptr_XMLCh widetar(target.c_str());
559 child = retdoc->createElement(_RelayState);
560 child->appendChild(retdoc->createTextNode(widetar.get()));
561 retdoc->getDocumentElement()->appendChild(child);
563 XMLHelper::serialize(retdoc->getDocumentElement(), os, true);
565 return make_pair(true, httpResponse.sendResponse(os));
567 return make_pair(false, 0L);
574 class SHIBSP_DLLLOCAL DummyContext : public ResolutionContext
577 DummyContext(const vector<Attribute*>& attributes) : m_attributes(attributes) {
580 virtual ~DummyContext() {
581 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
584 vector<Attribute*>& getResolvedAttributes() {
587 vector<Assertion*>& getResolvedAssertions() {
592 vector<Attribute*> m_attributes;
593 static vector<Assertion*> m_tokens; // never any tokens, so just share an empty vector
597 vector<Assertion*> DummyContext::m_tokens;
599 ResolutionContext* ExternalAuth::resolveAttributes(
600 const Application& application,
601 const GenericRequest* request,
602 const RoleDescriptor* issuer,
603 const XMLCh* protocol,
604 const saml2::NameID* nameid,
605 const saml2::AuthnStatement* statement,
606 const XMLCh* authncontext_class,
607 const XMLCh* authncontext_decl,
608 const vector<const Assertion*>* tokens,
609 const vector<Attribute*>* inputAttributes
612 vector<Attribute*> resolvedAttributes;
614 resolvedAttributes = *inputAttributes;
616 // First we do the extraction of any pushed information, including from metadata.
617 AttributeExtractor* extractor = application.getAttributeExtractor();
619 Locker extlocker(extractor);
621 pair<bool,const char*> mprefix = application.getString("metadataAttributePrefix");
623 m_log.debug("extracting metadata-derived attributes...");
625 // We pass nullptr for "issuer" because the IdP isn't the one asserting metadata-based attributes.
626 extractor->extractAttributes(application, request, nullptr, *issuer, resolvedAttributes);
627 for (indirect_iterator<vector<Attribute*>::iterator> a = make_indirect_iterator(resolvedAttributes.begin());
628 a != make_indirect_iterator(resolvedAttributes.end()); ++a) {
629 vector<string>& ids = a->getAliases();
630 for (vector<string>::iterator id = ids.begin(); id != ids.end(); ++id)
631 *id = mprefix.second + *id;
634 catch (std::exception& ex) {
635 m_log.error("caught exception extracting attributes: %s", ex.what());
640 m_log.debug("extracting pushed attributes...");
644 extractor->extractAttributes(application, request, issuer, *nameid, resolvedAttributes);
646 catch (std::exception& ex) {
647 m_log.error("caught exception extracting attributes: %s", ex.what());
653 extractor->extractAttributes(application, request, issuer, *statement, resolvedAttributes);
655 catch (std::exception& ex) {
656 m_log.error("caught exception extracting attributes: %s", ex.what());
661 for (indirect_iterator<vector<const Assertion*>::const_iterator> t = make_indirect_iterator(tokens->begin());
662 t != make_indirect_iterator(tokens->end()); ++t) {
664 extractor->extractAttributes(application, request, issuer, *t, resolvedAttributes);
666 catch (std::exception& ex) {
667 m_log.error("caught exception extracting attributes: %s", ex.what());
672 AttributeFilter* filter = application.getAttributeFilter();
673 if (filter && !resolvedAttributes.empty()) {
674 BasicFilteringContext fc(application, resolvedAttributes, issuer, authncontext_class, authncontext_decl);
675 Locker filtlocker(filter);
677 filter->filterAttributes(fc, resolvedAttributes);
679 catch (std::exception& ex) {
680 m_log.error("caught exception filtering attributes: %s", ex.what());
681 m_log.error("dumping extracted attributes due to filtering exception");
682 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
683 resolvedAttributes.clear();
688 m_log.warn("no AttributeExtractor plugin installed, check log during startup");
692 AttributeResolver* resolver = application.getAttributeResolver();
694 m_log.debug("resolving attributes...");
696 Locker locker(resolver);
697 auto_ptr<ResolutionContext> ctx(
698 resolver->createResolutionContext(
701 issuer ? dynamic_cast<const EntityDescriptor*>(issuer->getParent()) : nullptr,
710 resolver->resolveAttributes(*ctx);
711 // Copy over any pushed attributes.
712 while (!resolvedAttributes.empty()) {
713 ctx->getResolvedAttributes().push_back(resolvedAttributes.back());
714 resolvedAttributes.pop_back();
716 return ctx.release();
719 catch (std::exception& ex) {
720 m_log.error("attribute resolution failed: %s", ex.what());
723 if (!resolvedAttributes.empty()) {
725 return new DummyContext(resolvedAttributes);
728 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
734 LoginEvent* ExternalAuth::newLoginEvent(const Application& application, const HTTPRequest& request) const
736 if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
739 auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGIN_EVENT, nullptr));
740 LoginEvent* login_event = dynamic_cast<LoginEvent*>(event.get());
742 login_event->m_request = &request;
743 login_event->m_app = &application;
744 login_event->m_binding = "ExternalAuth";
749 m_log.warn("unable to audit event, log event object was of an incorrect type");
752 catch (std::exception& ex) {
753 m_log.warn("exception auditing event: %s", ex.what());