813530fbefba77aefbe6819dc08eea081621e3ea
[shibboleth/cpp-sp.git] / shibsp / handler / impl / ExternalAuthHandler.cpp
1 /**
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.
6  *
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
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  */
20
21 /**
22  * ExternalAuthHandler.cpp
23  *
24  * Handler for integrating with external authentication mechanisms.
25  */
26
27 #include "internal.h"
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"
34
35 #include <sstream>
36 #include <boost/scoped_ptr.hpp>
37
38 #ifndef SHIBSP_LITE
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;
61 using saml2::NameID;
62 using saml2::AuthnStatement;
63 using saml2::AuthnContext;
64 # ifndef min
65 #  define min(a,b)            (((a) < (b)) ? (a) : (b))
66 # endif
67 #endif
68
69 using namespace shibspconstants;
70 using namespace shibsp;
71 using namespace xmltooling;
72 using namespace boost;
73 using namespace std;
74
75 namespace shibsp {
76
77 #if defined (_MSC_VER)
78     #pragma warning( push )
79     #pragma warning( disable : 4250 )
80 #endif
81
82     class SHIBSP_API ExternalAuth : public SecuredHandler, public RemotedHandler
83     {
84     public:
85         ExternalAuth(const DOMElement* e, const char* appId);
86         virtual ~ExternalAuth() {}
87
88         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
89         void receive(DDF& in, ostream& out);
90
91         const char* getType() const {
92             return "ExternalAuth";
93         }
94
95     private:
96         pair<bool,long> processMessage(
97             const Application& application,
98             HTTPRequest& httpRequest,
99             HTTPResponse& httpResponse,
100             DDF& reqDDF,
101             const DDF* respDDF=nullptr
102             ) const;
103 #ifndef SHIBSP_LITE
104         LoginEvent* newLoginEvent(const Application& application, const HTTPRequest& request) const;
105         ResolutionContext* resolveAttributes(
106             const Application& application,
107             const GenericRequest* request,
108             const saml2md::RoleDescriptor* issuer,
109             const XMLCh* protocol,
110             const saml2::NameID* nameid,
111             const saml2::AuthnStatement* statement,
112             const XMLCh* authncontext_class,
113             const XMLCh* authncontext_decl,
114             const vector<const Assertion*>* tokens=nullptr,
115             const vector<Attribute*>* inputAttributes=nullptr
116             ) const;
117 #endif
118     };
119
120 #if defined (_MSC_VER)
121     #pragma warning( pop )
122 #endif
123
124     Handler* SHIBSP_DLLLOCAL ExternalAuthFactory(const pair<const DOMElement*,const char*>& p)
125     {
126         return new ExternalAuth(p.first, p.second);
127     }
128
129 };
130
131 namespace {
132     static ostream& json_safe(ostream& os, const char* buf)
133     {
134         os << '"';
135         for (; *buf; ++buf) {
136             switch (*buf) {
137                 case '\\':
138                 case '"':
139                     os << '\\';
140                     os << *buf;
141                     break;
142                 case '\b':
143                     os << "\\b";
144                     break;
145                 case '\t':
146                     os << "\\t";
147                     break;
148                 case '\n':
149                     os << "\\n";
150                     break;
151                 case '\f':
152                     os << "\\f";
153                     break;
154                 case '\r':
155                     os << "\\r";
156                     break;
157                 default:
158                     os << *buf;
159             }
160         }
161         os << '"';
162         return os;
163     }
164 };
165
166 ExternalAuth::ExternalAuth(const DOMElement* e, const char* appId)
167     : SecuredHandler(e, Category::getInstance(SHIBSP_LOGCAT".ExternalAuth"), "acl", "127.0.0.1 ::1")
168 {
169     setAddress("run::ExternalAuth");
170 }
171
172 pair<bool,long> ExternalAuth::run(SPRequest& request, bool isHandler) const
173 {
174     // Check ACL in base class.
175     pair<bool,long> ret = SecuredHandler::run(request, isHandler);
176     if (ret.first)
177         return ret;
178
179     try {
180         if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
181             // When out of process, we run natively and directly process the message, except that we
182             // have to indirect the request anyway in order to override the client address. This is
183             // the simplest way to get a delegated HTTPRequest object, and since this code path is
184             // not really one we expect to use, it's good enough.
185             vector<string> headers(1, "User-Agent");
186             headers.push_back("Accept");
187             headers.push_back("Accept-Language");
188             headers.push_back("Cookie");
189             DDF in = wrap(request, &headers);
190             DDFJanitor jin(in);
191             scoped_ptr<HTTPRequest> fakedreq(getRequest(in));
192             return processMessage(request.getApplication(), *fakedreq, request, in);
193         }
194         else {
195             // When not out of process, we remote all the message processing.
196             vector<string> headers(1, "User-Agent");
197             headers.push_back("Accept");
198             headers.push_back("Accept-Language");
199             headers.push_back("Cookie");
200             DDF out,in = wrap(request, &headers);
201             DDFJanitor jin(in), jout(out);
202             out=request.getServiceProvider().getListenerService()->send(in);
203             return unwrap(request, out);
204         }
205     }
206     catch (std::exception& ex) {
207         m_log.error("error while processing request: %s", ex.what());
208         istringstream msg("External Authentication Failed");
209         return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_ERROR));
210     }
211 }
212
213 void ExternalAuth::receive(DDF& in, ostream& out)
214 {
215     // Find application.
216     const char* aid = in["application_id"].string();
217     const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
218     if (!app) {
219         // Something's horribly wrong.
220         m_log.error("couldn't find application (%s) for external authentication", aid ? aid : "(missing)");
221         throw ConfigurationException("Unable to locate application for external authentication, deleted?");
222     }
223
224     // Unpack the request.
225     scoped_ptr<HTTPRequest> req(getRequest(in));
226
227     // Wrap a response shim.
228     DDF ret(nullptr);
229     DDFJanitor jout(ret);
230     scoped_ptr<HTTPResponse> resp(getResponse(ret));
231
232     // Since we're remoted, the result should either be a throw, a false/0 return,
233     // which we just return as an empty structure, or a response/redirect,
234     // which we capture in the facade and send back.
235     try {
236         processMessage(*app, *req, *resp, in, &ret);
237     }
238     catch (std::exception& ex) {
239         m_log.error("raising exception: %s", ex.what());
240         throw;
241     }
242     out << ret;
243 }
244
245 pair<bool,long> ExternalAuth::processMessage(
246     const Application& application, HTTPRequest& httpRequest, HTTPResponse& httpResponse, DDF& reqDDF, const DDF* respDDF
247     ) const
248 {
249 #ifndef SHIBSP_LITE
250     string session_id;
251     SessionCache* cache = application.getServiceProvider().getSessionCache();
252     MetadataProvider* m = application.getMetadataProvider(false);
253     Locker mocker(m);
254
255     scoped_ptr<TransactionLog::Event> event;
256     LoginEvent* login_event = nullptr;
257     if (SPConfig::getConfig().isEnabled(SPConfig::Logging)) {
258         event.reset(SPConfig::getConfig().EventManager.newPlugin(LOGIN_EVENT, nullptr));
259         login_event = dynamic_cast<LoginEvent*>(event.get());
260         if (login_event)
261             login_event->m_app = &application;
262         else
263             m_log.warn("unable to audit event, log event object was of an incorrect type");
264     }
265
266     string ctype(httpRequest.getContentType());
267     if (ctype == "text/xml" || ctype == "application/samlassertion+xml") {
268         const char* body = httpRequest.getRequestBody();
269         if (!body)
270             throw FatalProfileException("Request body was empty.");
271
272         // Parse and bind the document into an XMLObject.
273         MemBufInputSource src(reinterpret_cast<const XMLByte*>(body), httpRequest.getContentLength(), "SAMLAssertion");
274         Wrapper4InputSource dsrc(&src, false);
275         DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(dsrc);
276         XercesJanitor<DOMDocument> janitor(doc);
277         auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
278         janitor.release();
279
280         saml2::Assertion* token = dynamic_cast<saml2::Assertion*>(xmlObject.get());
281         if (!token)
282             throw FatalProfileException("Request body did not contain a SAML 2.0 assertion.");
283         else if (token->getAuthnStatements().empty())
284             throw FatalProfileException("Assertion in request did not contain an AuthnStatement.");
285
286         // We're not implementing a full SAML profile here, only a minimal one that ignores most
287         // security checking, conditions, etc. The caller is in full control here and we just consume
288         // what we're given. The only thing we're honoring is the authentication information we find
289         // and processing any attributes.
290
291         const XMLCh* protocol = nullptr;
292         pair<const EntityDescriptor*, const RoleDescriptor*> issuer = pair<const EntityDescriptor*, const RoleDescriptor*>(nullptr,nullptr);
293         if (m && token->getIssuer() && token->getIssuer()->getName()) {
294             MetadataProvider::Criteria mc;
295             mc.entityID_unicode = token->getIssuer()->getName();
296             mc.role = &IDPSSODescriptor::ELEMENT_QNAME;
297             mc.protocol = samlconstants::SAML20P_NS;
298             issuer = m->getEntityDescriptor(mc);
299             if (!issuer.first) {
300                 auto_ptr_char iname(token->getIssuer()->getName());
301                 m_log.warn("no metadata found for issuer (%s)", iname.get());
302             }
303             else if (!issuer.second) {
304                 auto_ptr_char iname(token->getIssuer()->getName());
305                 m_log.warn("no IdP role found in metadata for issuer (%s)", iname.get());
306             }
307             protocol = mc.protocol;
308         }
309
310         const saml2::NameID* nameid = nullptr;
311         if (token->getSubject())
312             nameid = token->getSubject()->getNameID();
313         const AuthnStatement* ssoStatement = token->getAuthnStatements().front();
314
315         // authnskew allows rejection of SSO if AuthnInstant is too old.
316         const PropertySet* sessionProps = application.getPropertySet("Sessions");
317         pair<bool,unsigned int> authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair<bool,unsigned int>(false,0);
318
319         time_t now(time(nullptr));
320         if (ssoStatement->getAuthnInstant() &&
321                 ssoStatement->getAuthnInstantEpoch() - XMLToolingConfig::getConfig().clock_skew_secs > now) {
322             throw FatalProfileException("The AuthnInstant was future-dated.");
323         }
324         else if (authnskew.first && authnskew.second && ssoStatement->getAuthnInstant() &&
325                 ssoStatement->getAuthnInstantEpoch() <= now && (now - ssoStatement->getAuthnInstantEpoch() > authnskew.second)) {
326             throw FatalProfileException("The gap between now and the AuthnInstant exceeds the allowed limit.");
327         }
328         else if (authnskew.first && authnskew.second && ssoStatement->getAuthnInstant() == nullptr) {
329             throw FatalProfileException("No AuthnInstant was supplied, violating local policy.");
330         }
331
332         // Session expiration for SAML 2.0 is jointly IdP- and SP-driven.
333         time_t sessionExp = ssoStatement->getSessionNotOnOrAfter() ?
334             (ssoStatement->getSessionNotOnOrAfterEpoch() + XMLToolingConfig::getConfig().clock_skew_secs) : 0;
335         pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
336         if (!lifetime.first || lifetime.second == 0)
337             lifetime.second = 28800;
338         if (sessionExp == 0)
339             sessionExp = now + lifetime.second;     // IdP says nothing, calulate based on SP.
340         else
341             sessionExp = min(sessionExp, now + lifetime.second);    // Use the lowest.
342
343         const XMLCh* authncontext_class = nullptr;
344         const XMLCh* authncontext_decl = nullptr;
345         const AuthnContext* authnContext = ssoStatement->getAuthnContext();
346         if (authnContext) {
347             authncontext_class = authnContext->getAuthnContextClassRef() ? authnContext->getAuthnContextClassRef()->getReference() : nullptr;
348             authncontext_decl = authnContext->getAuthnContextDeclRef() ? authnContext->getAuthnContextDeclRef()->getReference() : nullptr;
349         }
350
351         // Extract client address.
352         reqDDF.addmember("client_addr").string((const char*)nullptr);
353         if (ssoStatement->getSubjectLocality() && ssoStatement->getSubjectLocality()->getAddress()) {
354             auto_ptr_char addr(ssoStatement->getSubjectLocality()->getAddress());
355             if (addr.get())
356                 reqDDF.getmember("client_addr").string(addr.get());
357         }
358
359         // The context will handle deleting attributes and tokens.
360         vector<const Assertion*> tokens(1, token);
361         scoped_ptr<ResolutionContext> ctx(
362             resolveAttributes(
363                 application,
364                 &httpRequest,
365                 issuer.second,
366                 protocol,
367                 nameid,
368                 ssoStatement,
369                 authncontext_class,
370                 authncontext_decl,
371                 &tokens
372                 )
373             );
374         tokens.clear(); // don't store the original token in the session, since it was contrived
375
376         if (ctx) {
377             // Copy over any new tokens, but leave them in the context for cleanup.
378             tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
379         }
380
381         cache->insert(
382             session_id,
383             application,
384             httpRequest,
385             httpResponse,
386             sessionExp,
387             issuer.first,
388             protocol,
389             nameid,
390             ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : nullptr,
391             ssoStatement->getSessionIndex(),
392             authncontext_class,
393             authncontext_decl,
394             &tokens,
395             &ctx->getResolvedAttributes()
396             );
397
398         if (login_event) {
399             login_event->m_binding = "ExternalAuth/XML";
400             login_event->m_sessionID = session_id.c_str();
401             login_event->m_peer = issuer.first;
402             auto_ptr_char prot(protocol);
403             login_event->m_protocol = prot.get();
404             login_event->m_nameID = nameid;
405             login_event->m_saml2AuthnStatement = ssoStatement;
406             if (ctx)
407                 login_event->m_attributes = &ctx->getResolvedAttributes();
408             try {
409                 application.getServiceProvider().getTransactionLog()->write(*login_event);
410             }
411             catch (std::exception& ex) {
412                 m_log.warn("exception auditing event: %s", ex.what());
413             }
414         }
415     }
416     else if (ctype == "application/x-www-form-urlencoded") {
417         auto_ptr_XMLCh protocol(httpRequest.getParameter("protocol"));
418         const char* param = httpRequest.getParameter("issuer");
419         pair<const EntityDescriptor*, const RoleDescriptor*> issuer = pair<const EntityDescriptor*, const RoleDescriptor*>(nullptr,nullptr);
420         if (m && param && *param) {
421             MetadataProvider::Criteria mc;
422             mc.entityID_ascii = param;
423             mc.role = &IDPSSODescriptor::ELEMENT_QNAME;
424             mc.protocol = protocol.get();
425             issuer = m->getEntityDescriptor(mc);
426             if (!issuer.first)
427                 m_log.warn("no metadata found for issuer (%s)", param);
428             else if (!issuer.second)
429                 m_log.warn("no IdP role found in metadata for issuer (%s)", param);
430         }
431
432         scoped_ptr<saml2::NameID> nameid;
433         param = httpRequest.getParameter("NameID");
434         if (param && *param) {
435             nameid.reset(saml2::NameIDBuilder::buildNameID());
436             auto_arrayptr<XMLCh> n(fromUTF8(param));
437             nameid->setName(n.get());
438             param = httpRequest.getParameter("Format");
439             if (param && param) {
440                 auto_ptr_XMLCh f(param);
441                 nameid->setFormat(f.get());
442             }
443         }
444
445         scoped_ptr<DateTime> authn_instant;
446         param = httpRequest.getParameter("AuthnInstant");
447         if (param && *param) {
448             auto_ptr_XMLCh d(param);
449             authn_instant.reset(new DateTime(d.get()));
450             authn_instant->parseDateTime();
451         }
452
453         auto_ptr_XMLCh session_index(httpRequest.getParameter("SessionIndex"));
454         auto_ptr_XMLCh authncontext_class(httpRequest.getParameter("AuthnContextClassRef"));
455         auto_ptr_XMLCh authncontext_decl(httpRequest.getParameter("AuthnContextDeclRef"));
456
457         time_t sessionExp = 0;
458         param = httpRequest.getParameter("lifetime");
459         if (param && param)
460             sessionExp = atol(param);
461         if (sessionExp) {
462             sessionExp += time(nullptr);
463         }
464         else {
465             const PropertySet* sessionProps = application.getPropertySet("Sessions");
466             pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
467             if (!lifetime.first || lifetime.second == 0)
468                 lifetime.second = 28800;
469             sessionExp = time(nullptr) + lifetime.second;
470         }
471
472         // Create simple attributes around whatever parameters are specified.
473         vector<Attribute*> resolvedAttributes;
474         param = httpRequest.getParameter("attributes");
475         if (param && *param) {
476             char_separator<char> sep(", ");
477             string dup(param);
478             tokenizer< char_separator<char> > tokens(dup, sep);
479             try {
480                 for (tokenizer< char_separator<char> >::iterator t = tokens.begin(); t != tokens.end(); ++t) {
481                     vector<const char*> vals;
482                     if (httpRequest.getParameters(t->c_str(), vals)) {
483                         vector<string> ids(1, *t);
484                         auto_ptr<SimpleAttribute> attr(new SimpleAttribute(ids));
485                         vector<string>& dest = attr->getValues();
486                         for (vector<const char*>::const_iterator v = vals.begin(); v != vals.end(); ++v)
487                             dest.push_back(*v);
488                         resolvedAttributes.push_back(attr.get());
489                         attr.release();
490                     }
491                 }
492             }
493             catch (std::exception&) {
494                 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
495                 throw;
496             }
497         }
498
499         // Get actual client address.
500         reqDDF.addmember("client_addr").string(httpRequest.getParameter("address"));
501
502         scoped_ptr<ResolutionContext> ctx(
503             resolveAttributes(
504                 application,
505                 &httpRequest,
506                 issuer.second,
507                 protocol.get(),
508                 nameid.get(),
509                 nullptr,
510                 authncontext_class.get(),
511                 authncontext_decl.get(),
512                 nullptr,
513                 &resolvedAttributes
514                 )
515             );
516
517         vector<const Assertion*> tokens;
518         if (ctx) {
519             // Copy over any new tokens, but leave them in the context for cleanup.
520             tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
521         }
522
523         cache->insert(
524             session_id,
525             application,
526             httpRequest,
527             httpResponse,
528             sessionExp,
529             issuer.first,
530             protocol.get(),
531             nameid.get(),
532             authn_instant ? authn_instant->getRawData() : nullptr,
533             session_index.get(),
534             authncontext_class.get(),
535             authncontext_decl.get(),
536             &tokens,
537             &ctx->getResolvedAttributes()
538             );
539
540         if (login_event) {
541             login_event->m_binding = "ExternalAuth/POST";
542             login_event->m_sessionID = session_id.c_str();
543             login_event->m_peer = issuer.first;
544             login_event->m_protocol = httpRequest.getParameter("protocol");
545             login_event->m_nameID = nameid.get();
546             if (ctx)
547                 login_event->m_attributes = &ctx->getResolvedAttributes();
548             try {
549                 application.getServiceProvider().getTransactionLog()->write(*login_event);
550             }
551             catch (std::exception& ex) {
552                 m_log.warn("exception auditing event: %s", ex.what());
553             }
554         }
555     }
556     else {
557         throw FatalProfileException("Submission was not in a recognized SAML assertion or form-encoded format.");
558     }
559
560     const char* param = httpRequest.getParameter("RelayState");
561     string target(param ? param : "");
562     try {
563         recoverRelayState(application, httpRequest, httpResponse, target);
564     }
565     catch (std::exception& ex) {
566         m_log.error("error recovering relay state: %s", ex.what());
567         target.erase();
568     }
569
570     stringstream os;
571     string accept = httpRequest.getHeader("Accept");
572     if (accept.find("application/json") != string::npos) {
573         httpResponse.setContentType("application/json");
574         os << "{ \"SessionID\": "; json_safe(os, session_id.c_str());
575         bool firstCookie = true;
576         if (respDDF) {
577             DDF hdr;
578             DDF hdrs = respDDF->getmember("headers");
579             hdr = hdrs.first();
580             while (hdr.isstring()) {
581                 if (!strcmp(hdr.name(), "Set-Cookie")) {
582                     if (firstCookie) {
583                         os << ", \"Cookies\": [ ";
584                         firstCookie = false;
585                     }
586                     else {
587                         os << ", ";
588                     }
589                     json_safe(os, hdr.string());
590                 }
591                 hdr = hdrs.next();
592             }
593         }
594         os << " ]";
595         if (!target.empty())
596             os << ", \"RelayState\": "; json_safe(os, target.c_str());
597         os << " }";
598     }
599     else {
600         httpResponse.setContentType("text/xml");
601         static const XMLCh _ExternalAuth[] = UNICODE_LITERAL_12(E,x,t,e,r,n,a,l,A,u,t,h);
602         static const XMLCh _SessionID[] = UNICODE_LITERAL_9(S,e,s,s,i,o,n,I,D);
603         static const XMLCh _RelayState[] = UNICODE_LITERAL_10(R,e,l,a,y,S,t,a,t,e);
604         static const XMLCh _Cookie[] = UNICODE_LITERAL_6(C,o,o,k,i,e);
605         DOMDocument* retdoc = XMLToolingConfig::getConfig().getParser().newDocument();
606         XercesJanitor<DOMDocument> retjanitor(retdoc);
607         retdoc->appendChild(retdoc->createElement(_ExternalAuth));
608         auto_ptr_XMLCh wideid(session_id.c_str());
609         DOMElement* child = retdoc->createElement(_SessionID);
610         child->appendChild(retdoc->createTextNode(wideid.get()));
611         retdoc->getDocumentElement()->appendChild(child);
612         if (respDDF) {
613             DDF hdr;
614             DDF hdrs = respDDF->getmember("headers");
615             hdr = hdrs.first();
616             while (hdr.isstring()) {
617                 if (!strcmp(hdr.name(), "Set-Cookie")) {
618                     child = retdoc->createElement(_Cookie);
619                     auto_ptr_XMLCh wideval(hdr.string());
620                     child->appendChild(retdoc->createTextNode(wideval.get()));
621                     retdoc->getDocumentElement()->appendChild(child);
622                 }
623                 hdr = hdrs.next();
624             }
625         }
626         if (!target.empty()) {
627             auto_ptr_XMLCh widetar(target.c_str());
628             child = retdoc->createElement(_RelayState);
629             child->appendChild(retdoc->createTextNode(widetar.get()));
630             retdoc->getDocumentElement()->appendChild(child);
631         }
632         XMLHelper::serialize(retdoc->getDocumentElement(), os, true);
633     }
634     return make_pair(true, httpResponse.sendResponse(os));
635 #else
636     return make_pair(false, 0L);
637 #endif
638 }
639
640 #ifndef SHIBSP_LITE
641
642 namespace {
643     class SHIBSP_DLLLOCAL DummyContext : public ResolutionContext
644     {
645     public:
646         DummyContext(const vector<Attribute*>& attributes) : m_attributes(attributes) {
647         }
648
649         virtual ~DummyContext() {
650             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
651         }
652
653         vector<Attribute*>& getResolvedAttributes() {
654             return m_attributes;
655         }
656         vector<Assertion*>& getResolvedAssertions() {
657             return m_tokens;
658         }
659
660     private:
661         vector<Attribute*> m_attributes;
662         static vector<Assertion*> m_tokens; // never any tokens, so just share an empty vector
663     };
664 };
665
666 vector<Assertion*> DummyContext::m_tokens;
667
668 ResolutionContext* ExternalAuth::resolveAttributes(
669     const Application& application,
670     const GenericRequest* request,
671     const RoleDescriptor* issuer,
672     const XMLCh* protocol,
673     const saml2::NameID* nameid,
674     const saml2::AuthnStatement* statement,
675     const XMLCh* authncontext_class,
676     const XMLCh* authncontext_decl,
677     const vector<const Assertion*>* tokens,
678     const vector<Attribute*>* inputAttributes
679     ) const
680 {
681     vector<Attribute*> resolvedAttributes;
682     if (inputAttributes)
683         resolvedAttributes = *inputAttributes;
684
685     // First we do the extraction of any pushed information, including from metadata.
686     AttributeExtractor* extractor = application.getAttributeExtractor();
687     if (extractor) {
688         Locker extlocker(extractor);
689         if (issuer) {
690             pair<bool,const char*> mprefix = application.getString("metadataAttributePrefix");
691             if (mprefix.first) {
692                 m_log.debug("extracting metadata-derived attributes...");
693                 try {
694                     // We pass nullptr for "issuer" because the IdP isn't the one asserting metadata-based attributes.
695                     extractor->extractAttributes(application, request, nullptr, *issuer, resolvedAttributes);
696                     for (indirect_iterator<vector<Attribute*>::iterator> a = make_indirect_iterator(resolvedAttributes.begin());
697                             a != make_indirect_iterator(resolvedAttributes.end()); ++a) {
698                         vector<string>& ids = a->getAliases();
699                         for (vector<string>::iterator id = ids.begin(); id != ids.end(); ++id)
700                             *id = mprefix.second + *id;
701                     }
702                 }
703                 catch (std::exception& ex) {
704                     m_log.error("caught exception extracting attributes: %s", ex.what());
705                 }
706             }
707         }
708
709         m_log.debug("extracting pushed attributes...");
710
711         if (nameid) {
712             try {
713                 extractor->extractAttributes(application, request, issuer, *nameid, resolvedAttributes);
714             }
715             catch (std::exception& ex) {
716                 m_log.error("caught exception extracting attributes: %s", ex.what());
717             }
718         }
719
720         if (statement) {
721             try {
722                 extractor->extractAttributes(application, request, issuer, *statement, resolvedAttributes);
723             }
724             catch (std::exception& ex) {
725                 m_log.error("caught exception extracting attributes: %s", ex.what());
726             }
727         }
728
729         if (tokens) {
730             for (indirect_iterator<vector<const Assertion*>::const_iterator> t = make_indirect_iterator(tokens->begin());
731                     t != make_indirect_iterator(tokens->end()); ++t) {
732                 try {
733                     extractor->extractAttributes(application, request, issuer, *t, resolvedAttributes);
734                 }
735                 catch (std::exception& ex) {
736                     m_log.error("caught exception extracting attributes: %s", ex.what());
737                 }
738             }
739         }
740
741         AttributeFilter* filter = application.getAttributeFilter();
742         if (filter && !resolvedAttributes.empty()) {
743             BasicFilteringContext fc(application, resolvedAttributes, issuer, authncontext_class, authncontext_decl);
744             Locker filtlocker(filter);
745             try {
746                 filter->filterAttributes(fc, resolvedAttributes);
747             }
748             catch (std::exception& ex) {
749                 m_log.error("caught exception filtering attributes: %s", ex.what());
750                 m_log.error("dumping extracted attributes due to filtering exception");
751                 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
752                 resolvedAttributes.clear();
753             }
754         }
755     }
756     else {
757         m_log.warn("no AttributeExtractor plugin installed, check log during startup");
758     }
759
760     try {
761         AttributeResolver* resolver = application.getAttributeResolver();
762         if (resolver) {
763             m_log.debug("resolving attributes...");
764
765             Locker locker(resolver);
766             auto_ptr<ResolutionContext> ctx(
767                 resolver->createResolutionContext(
768                     application,
769                     request,
770                     issuer ? dynamic_cast<const EntityDescriptor*>(issuer->getParent()) : nullptr,
771                     protocol,
772                     nameid,
773                     authncontext_class,
774                     authncontext_decl,
775                     tokens,
776                     &resolvedAttributes
777                     )
778                 );
779             resolver->resolveAttributes(*ctx);
780             // Copy over any pushed attributes.
781             while (!resolvedAttributes.empty()) {
782                 ctx->getResolvedAttributes().push_back(resolvedAttributes.back());
783                 resolvedAttributes.pop_back();
784             }
785             return ctx.release();
786         }
787     }
788     catch (std::exception& ex) {
789         m_log.error("attribute resolution failed: %s", ex.what());
790     }
791
792     if (!resolvedAttributes.empty()) {
793         try {
794             return new DummyContext(resolvedAttributes);
795         }
796         catch (bad_alloc&) {
797             for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
798         }
799     }
800     return nullptr;
801 }
802
803 LoginEvent* ExternalAuth::newLoginEvent(const Application& application, const HTTPRequest& request) const
804 {
805     if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
806         return nullptr;
807     try {
808         auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGIN_EVENT, nullptr));
809         LoginEvent* login_event = dynamic_cast<LoginEvent*>(event.get());
810         if (login_event) {
811             login_event->m_request = &request;
812             login_event->m_app = &application;
813             login_event->m_binding = "ExternalAuth";
814             event.release();
815             return login_event;
816         }
817         else {
818             m_log.warn("unable to audit event, log event object was of an incorrect type");
819         }
820     }
821     catch (std::exception& ex) {
822         m_log.warn("exception auditing event: %s", ex.what());
823     }
824     return nullptr;
825 }
826
827 #endif