https://issues.shibboleth.net/jira/browse/SSPCPP-349
[shibboleth/cpp-sp.git] / shibsp / handler / impl / AssertionConsumerService.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  * AssertionConsumerService.cpp
23  *
24  * Base class for handlers that create sessions by consuming SSO protocol responses.
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/AssertionConsumerService.h"
33 #include "util/SPConstants.h"
34
35 # include <ctime>
36 #ifndef SHIBSP_LITE
37 # include "attribute/Attribute.h"
38 # include "attribute/filtering/AttributeFilter.h"
39 # include "attribute/filtering/BasicFilteringContext.h"
40 # include "attribute/resolver/AttributeExtractor.h"
41 # include "attribute/resolver/AttributeResolver.h"
42 # include "attribute/resolver/ResolutionContext.h"
43 # include "metadata/MetadataProviderCriteria.h"
44 # include "security/SecurityPolicy.h"
45 # include "security/SecurityPolicyProvider.h"
46 # include <saml/exceptions.h>
47 # include <saml/SAMLConfig.h>
48 # include <saml/saml1/core/Assertions.h>
49 # include <saml/saml1/core/Protocols.h>
50 # include <saml/saml2/core/Protocols.h>
51 # include <saml/saml2/metadata/Metadata.h>
52 # include <saml/util/CommonDomainCookie.h>
53 using namespace samlconstants;
54 using opensaml::saml2md::MetadataProvider;
55 using opensaml::saml2md::RoleDescriptor;
56 using opensaml::saml2md::EntityDescriptor;
57 using opensaml::saml2md::IDPSSODescriptor;
58 using opensaml::saml2md::SPSSODescriptor;
59 #else
60 # include "lite/CommonDomainCookie.h"
61 #endif
62
63 using namespace shibspconstants;
64 using namespace shibsp;
65 using namespace opensaml;
66 using namespace xmltooling;
67 using namespace std;
68
69 AssertionConsumerService::AssertionConsumerService(
70     const DOMElement* e, const char* appId, Category& log, DOMNodeFilter* filter, const map<string,string>* remapper
71     ) : AbstractHandler(e, log, filter, remapper)
72 #ifndef SHIBSP_LITE
73         ,m_decoder(nullptr), m_role(samlconstants::SAML20MD_NS, opensaml::saml2md::IDPSSODescriptor::LOCAL_NAME)
74 #endif
75 {
76     if (!e)
77         return;
78     string address(appId);
79     address += getString("Location").second;
80     setAddress(address.c_str());
81 #ifndef SHIBSP_LITE
82     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
83         m_decoder = SAMLConfig::getConfig().MessageDecoderManager.newPlugin(
84             getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
85             );
86         m_decoder->setArtifactResolver(SPConfig::getConfig().getArtifactResolver());
87     }
88 #endif
89 }
90
91 AssertionConsumerService::~AssertionConsumerService()
92 {
93 #ifndef SHIBSP_LITE
94     delete m_decoder;
95 #endif
96 }
97
98 pair<bool,long> AssertionConsumerService::run(SPRequest& request, bool isHandler) const
99 {
100     string relayState;
101     SPConfig& conf = SPConfig::getConfig();
102
103     if (conf.isEnabled(SPConfig::OutOfProcess)) {
104         // When out of process, we run natively and directly process the message.
105         return processMessage(request.getApplication(), request, request);
106     }
107     else {
108         // When not out of process, we remote all the message processing.
109         vector<string> headers(1, "Cookie");
110         headers.push_back("User-Agent");
111         DDF out,in = wrap(request, &headers);
112         DDFJanitor jin(in), jout(out);
113         out=request.getServiceProvider().getListenerService()->send(in);
114         return unwrap(request, out);
115     }
116 }
117
118 void AssertionConsumerService::receive(DDF& in, ostream& out)
119 {
120     // Find application.
121     const char* aid=in["application_id"].string();
122     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
123     if (!app) {
124         // Something's horribly wrong.
125         m_log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)");
126         throw ConfigurationException("Unable to locate application for new session, deleted?");
127     }
128
129     // Unpack the request.
130     auto_ptr<HTTPRequest> req(getRequest(in));
131
132     // Wrap a response shim.
133     DDF ret(nullptr);
134     DDFJanitor jout(ret);
135     auto_ptr<HTTPResponse> resp(getResponse(ret));
136
137     // Since we're remoted, the result should either be a throw, a false/0 return,
138     // which we just return as an empty structure, or a response/redirect,
139     // which we capture in the facade and send back.
140     processMessage(*app, *req.get(), *resp.get());
141     out << ret;
142 }
143
144 pair<bool,long> AssertionConsumerService::processMessage(
145     const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse
146     ) const
147 {
148 #ifndef SHIBSP_LITE
149     // Locate policy key.
150     pair<bool,const char*> policyId = getString("policyId", m_configNS.get());  // namespace-qualified if inside handler element
151     if (!policyId.first)
152         policyId = application.getString("policyId");   // unqualified in Application(s) element
153
154     // Lock metadata for use by policy.
155     Locker metadataLocker(application.getMetadataProvider());
156
157     // Create the policy.
158     auto_ptr<opensaml::SecurityPolicy> policy(
159         application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, &m_role, policyId.second)
160         );
161
162     string relayState;
163     bool relayStateOK = true;
164     auto_ptr<XMLObject> msg(nullptr);
165     try {
166         // Decode the message and process it in a protocol-specific way.
167         auto_ptr<XMLObject> msg2(m_decoder->decode(relayState, httpRequest, *(policy.get())));
168         if (!msg2.get())
169             throw BindingException("Failed to decode an SSO protocol response.");
170         msg = msg2; // save off to allow access from within exception handler.
171         DDF postData = recoverPostData(application, httpRequest, httpResponse, relayState.c_str());
172         DDFJanitor postjan(postData);
173         recoverRelayState(application, httpRequest, httpResponse, relayState);
174         limitRelayState(m_log, application, httpRequest, relayState.c_str());
175         implementProtocol(application, httpRequest, httpResponse, *(policy.get()), NULL, *msg.get());
176
177         auto_ptr_char issuer(policy->getIssuer() ? policy->getIssuer()->getName() : nullptr);
178
179         // History cookie.
180         if (issuer.get() && *issuer.get())
181             maintainHistory(application, httpRequest, httpResponse, issuer.get());
182
183         // Now redirect to the state value. By now, it should be set to *something* usable.
184         // First check for POST data.
185         if (!postData.islist()) {
186             m_log.debug("ACS returning via redirect to: %s", relayState.c_str());
187             return make_pair(true, httpResponse.sendRedirect(relayState.c_str()));
188         }
189         else {
190             m_log.debug("ACS returning via POST to: %s", relayState.c_str());
191             return make_pair(true, sendPostResponse(application, httpResponse, relayState.c_str(), postData));
192         }
193     }
194     catch (XMLToolingException& ex) {
195         if (relayStateOK) {
196             // Check for isPassive error condition.
197             const char* sc2 = ex.getProperty("statusCode2");
198             if (sc2 && !strcmp(sc2, "urn:oasis:names:tc:SAML:2.0:status:NoPassive")) {
199                 pair<bool,bool> ignore = getBool("ignoreNoPassive", m_configNS.get());  // namespace-qualified if inside handler element
200                 if (ignore.first && ignore.second && !relayState.empty()) {
201                     m_log.debug("ignoring SAML status of NoPassive and redirecting to resource...");
202                     return make_pair(true, httpResponse.sendRedirect(relayState.c_str()));
203                 }
204             }
205         }
206         if (!relayState.empty())
207             ex.addProperty("RelayState", relayState.c_str());
208
209         // Log the error.
210         try {
211             auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGIN_EVENT, nullptr));
212             LoginEvent* error_event = dynamic_cast<LoginEvent*>(event.get());
213             if (error_event) {
214                 error_event->m_exception = &ex;
215                 error_event->m_request = &httpRequest;
216                 error_event->m_app = &application;
217                 if (policy->getIssuerMetadata())
218                     error_event->m_peer = dynamic_cast<const EntityDescriptor*>(policy->getIssuerMetadata()->getParent());
219                 auto_ptr_char prot(getProtocolFamily());
220                 error_event->m_protocol = prot.get();
221                 error_event->m_binding = getString("Binding").second;
222                 error_event->m_saml2Response = dynamic_cast<const saml2p::StatusResponseType*>(msg.get());
223                 if (!error_event->m_saml2Response)
224                     error_event->m_saml1Response = dynamic_cast<const saml1p::Response*>(msg.get());
225                 application.getServiceProvider().getTransactionLog()->write(*error_event);
226             }
227             else {
228                 m_log.warn("unable to audit event, log event object was of an incorrect type");
229             }
230         }
231         catch (exception& ex) {
232             m_log.warn("exception auditing event: %s", ex.what());
233         }
234
235         throw;
236     }
237 #else
238     throw ConfigurationException("Cannot process message using lite version of shibsp library.");
239 #endif
240 }
241
242 void AssertionConsumerService::checkAddress(const Application& application, const HTTPRequest& httpRequest, const char* issuedTo) const
243 {
244     if (!issuedTo || !*issuedTo)
245         return;
246
247     const PropertySet* props=application.getPropertySet("Sessions");
248     pair<bool,bool> checkAddress = props ? props->getBool("checkAddress") : make_pair(false,true);
249     if (!checkAddress.first)
250         checkAddress.second=true;
251
252     if (checkAddress.second) {
253         m_log.debug("checking client address");
254         if (httpRequest.getRemoteAddr() != issuedTo) {
255             throw FatalProfileException(
256                "Your client's current address ($client_addr) differs from the one used when you authenticated "
257                 "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
258                 "Please contact your local support staff or help desk for assistance.",
259                 namedparams(1,"client_addr",httpRequest.getRemoteAddr().c_str())
260                 );
261         }
262     }
263 }
264
265 #ifndef SHIBSP_LITE
266
267 const XMLCh* AssertionConsumerService::getProtocolFamily() const
268 {
269     return m_decoder ? m_decoder->getProtocolFamily() : nullptr;
270 }
271
272 const char* AssertionConsumerService::getType() const
273 {
274     return "AssertionConsumerService";
275 }
276
277 void AssertionConsumerService::generateMetadata(SPSSODescriptor& role, const char* handlerURL) const
278 {
279     // Initial guess at index to use.
280     pair<bool,unsigned int> ix = pair<bool,unsigned int>(false,0);
281     if (!strncmp(handlerURL, "https", 5))
282         ix = getUnsignedInt("sslIndex", shibspconstants::ASCII_SHIB2SPCONFIG_NS);
283     if (!ix.first)
284         ix = getUnsignedInt("index");
285     if (!ix.first)
286         ix.second = 1;
287
288     // Find maximum index in use and go one higher.
289     const vector<saml2md::AssertionConsumerService*>& services = const_cast<const SPSSODescriptor&>(role).getAssertionConsumerServices();
290     if (!services.empty() && ix.second <= services.back()->getIndex().second)
291         ix.second = services.back()->getIndex().second + 1;
292
293     const char* loc = getString("Location").second;
294     string hurl(handlerURL);
295     if (*loc != '/')
296         hurl += '/';
297     hurl += loc;
298     auto_ptr_XMLCh widen(hurl.c_str());
299
300     saml2md::AssertionConsumerService* ep = saml2md::AssertionConsumerServiceBuilder::buildAssertionConsumerService();
301     ep->setLocation(widen.get());
302     ep->setBinding(getXMLString("Binding").second);
303     ep->setIndex(ix.second);
304     role.getAssertionConsumerServices().push_back(ep);
305 }
306
307 opensaml::SecurityPolicy* AssertionConsumerService::createSecurityPolicy(
308     const Application& application, const xmltooling::QName* role, bool validate, const char* policyId
309     ) const
310 {
311     return new SecurityPolicy(application, role, validate, policyId);
312 }
313
314 class SHIBSP_DLLLOCAL DummyContext : public ResolutionContext
315 {
316 public:
317     DummyContext(const vector<Attribute*>& attributes) : m_attributes(attributes) {
318     }
319
320     virtual ~DummyContext() {
321         for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
322     }
323
324     vector<Attribute*>& getResolvedAttributes() {
325         return m_attributes;
326     }
327     vector<Assertion*>& getResolvedAssertions() {
328         return m_tokens;
329     }
330
331 private:
332     vector<Attribute*> m_attributes;
333     static vector<Assertion*> m_tokens; // never any tokens, so just share an empty vector
334 };
335
336 vector<Assertion*> DummyContext::m_tokens;
337
338 ResolutionContext* AssertionConsumerService::resolveAttributes(
339     const Application& application,
340     const saml2md::RoleDescriptor* issuer,
341     const XMLCh* protocol,
342     const saml1::NameIdentifier* v1nameid,
343     const saml2::NameID* nameid,
344     const XMLCh* authncontext_class,
345     const XMLCh* authncontext_decl,
346     const vector<const Assertion*>* tokens
347     ) const
348 {
349     return resolveAttributes(
350         application,
351         issuer,
352         protocol,
353         v1nameid,
354         nullptr,
355         nameid,
356         nullptr,
357         authncontext_class,
358         authncontext_decl,
359         tokens
360         );
361 }
362
363 ResolutionContext* AssertionConsumerService::resolveAttributes(
364     const Application& application,
365     const saml2md::RoleDescriptor* issuer,
366     const XMLCh* protocol,
367     const saml1::NameIdentifier* v1nameid,
368     const saml1::AuthenticationStatement* v1statement,
369     const saml2::NameID* nameid,
370     const saml2::AuthnStatement* statement,
371     const XMLCh* authncontext_class,
372     const XMLCh* authncontext_decl,
373     const vector<const Assertion*>* tokens
374     ) const
375 {
376     // First we do the extraction of any pushed information, including from metadata.
377     vector<Attribute*> resolvedAttributes;
378     AttributeExtractor* extractor = application.getAttributeExtractor();
379     if (extractor) {
380         Locker extlocker(extractor);
381         if (issuer) {
382             pair<bool,const char*> mprefix = application.getString("metadataAttributePrefix");
383             if (mprefix.first) {
384                 m_log.debug("extracting metadata-derived attributes...");
385                 try {
386                     // We pass nullptr for "issuer" because the IdP isn't the one asserting metadata-based attributes.
387                     extractor->extractAttributes(application, nullptr, *issuer, resolvedAttributes);
388                     for (vector<Attribute*>::iterator a = resolvedAttributes.begin(); a != resolvedAttributes.end(); ++a) {
389                         vector<string>& ids = (*a)->getAliases();
390                         for (vector<string>::iterator id = ids.begin(); id != ids.end(); ++id)
391                             *id = mprefix.second + *id;
392                     }
393                 }
394                 catch (exception& ex) {
395                     m_log.error("caught exception extracting attributes: %s", ex.what());
396                 }
397             }
398         }
399
400         m_log.debug("extracting pushed attributes...");
401
402         if (v1nameid || nameid) {
403             try {
404                 if (v1nameid)
405                     extractor->extractAttributes(application, issuer, *v1nameid, resolvedAttributes);
406                 else
407                     extractor->extractAttributes(application, issuer, *nameid, resolvedAttributes);
408             }
409             catch (exception& ex) {
410                 m_log.error("caught exception extracting attributes: %s", ex.what());
411             }
412         }
413
414         if (v1statement || statement) {
415             try {
416                 if (v1statement)
417                     extractor->extractAttributes(application, issuer, *v1statement, resolvedAttributes);
418                 else
419                     extractor->extractAttributes(application, issuer, *statement, resolvedAttributes);
420             }
421             catch (exception& ex) {
422                 m_log.error("caught exception extracting attributes: %s", ex.what());
423             }
424         }
425
426         if (tokens) {
427             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
428                 try {
429                     extractor->extractAttributes(application, issuer, *(*t), resolvedAttributes);
430                 }
431                 catch (exception& ex) {
432                     m_log.error("caught exception extracting attributes: %s", ex.what());
433                 }
434             }
435         }
436
437         AttributeFilter* filter = application.getAttributeFilter();
438         if (filter && !resolvedAttributes.empty()) {
439             BasicFilteringContext fc(application, resolvedAttributes, issuer, authncontext_class);
440             Locker filtlocker(filter);
441             try {
442                 filter->filterAttributes(fc, resolvedAttributes);
443             }
444             catch (exception& ex) {
445                 m_log.error("caught exception filtering attributes: %s", ex.what());
446                 m_log.error("dumping extracted attributes due to filtering exception");
447                 for_each(resolvedAttributes.begin(), resolvedAttributes.end(), xmltooling::cleanup<shibsp::Attribute>());
448                 resolvedAttributes.clear();
449             }
450         }
451     }
452     else {
453         m_log.warn("no AttributeExtractor plugin installed, check log during startup");
454     }
455
456     try {
457         AttributeResolver* resolver = application.getAttributeResolver();
458         if (resolver) {
459             m_log.debug("resolving attributes...");
460
461             Locker locker(resolver);
462             auto_ptr<ResolutionContext> ctx(
463                 resolver->createResolutionContext(
464                     application,
465                     issuer ? dynamic_cast<const saml2md::EntityDescriptor*>(issuer->getParent()) : nullptr,
466                     protocol,
467                     nameid,
468                     authncontext_class,
469                     authncontext_decl,
470                     tokens,
471                     &resolvedAttributes
472                     )
473                 );
474             resolver->resolveAttributes(*ctx.get());
475             // Copy over any pushed attributes.
476             if (!resolvedAttributes.empty())
477                 ctx->getResolvedAttributes().insert(ctx->getResolvedAttributes().end(), resolvedAttributes.begin(), resolvedAttributes.end());
478             return ctx.release();
479         }
480     }
481     catch (exception& ex) {
482         m_log.error("attribute resolution failed: %s", ex.what());
483     }
484
485     if (!resolvedAttributes.empty())
486         return new DummyContext(resolvedAttributes);
487     return nullptr;
488 }
489
490 void AssertionConsumerService::extractMessageDetails(const Assertion& assertion, const XMLCh* protocol, opensaml::SecurityPolicy& policy) const
491 {
492     policy.setMessageID(assertion.getID());
493     policy.setIssueInstant(assertion.getIssueInstantEpoch());
494
495     if (XMLString::equals(assertion.getElementQName().getNamespaceURI(), samlconstants::SAML20_NS)) {
496         const saml2::Assertion* a2 = dynamic_cast<const saml2::Assertion*>(&assertion);
497         if (a2) {
498             m_log.debug("extracting issuer from SAML 2.0 assertion");
499             policy.setIssuer(a2->getIssuer());
500         }
501     }
502     else {
503         const saml1::Assertion* a1 = dynamic_cast<const saml1::Assertion*>(&assertion);
504         if (a1) {
505             m_log.debug("extracting issuer from SAML 1.x assertion");
506             policy.setIssuer(a1->getIssuer());
507         }
508     }
509
510     if (policy.getIssuer() && !policy.getIssuerMetadata() && policy.getMetadataProvider()) {
511         if (policy.getIssuer()->getFormat() && !XMLString::equals(policy.getIssuer()->getFormat(), saml2::NameIDType::ENTITY)) {
512             m_log.warn("non-system entity issuer, skipping metadata lookup");
513             return;
514         }
515         m_log.debug("searching metadata for assertion issuer...");
516         pair<const EntityDescriptor*,const RoleDescriptor*> entity;
517         MetadataProvider::Criteria& mc = policy.getMetadataProviderCriteria();
518         mc.entityID_unicode = policy.getIssuer()->getName();
519         mc.role = &IDPSSODescriptor::ELEMENT_QNAME;
520         mc.protocol = protocol;
521         entity = policy.getMetadataProvider()->getEntityDescriptor(mc);
522         if (!entity.first) {
523             auto_ptr_char iname(policy.getIssuer()->getName());
524             m_log.warn("no metadata found, can't establish identity of issuer (%s)", iname.get());
525         }
526         else if (!entity.second) {
527             m_log.warn("unable to find compatible IdP role in metadata");
528         }
529         else {
530             policy.setIssuerMetadata(entity.second);
531         }
532     }
533 }
534
535 LoginEvent* AssertionConsumerService::newLoginEvent(const Application& application, const xmltooling::HTTPRequest& request) const
536 {
537     if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
538         return nullptr;
539     try {
540         auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGIN_EVENT, nullptr));
541         LoginEvent* login_event = dynamic_cast<LoginEvent*>(event.get());
542         if (login_event) {
543             login_event->m_request = &request;
544             login_event->m_app = &application;
545             login_event->m_binding = getString("Binding").second;
546             event.release();
547             return login_event;
548         }
549         else {
550             m_log.warn("unable to audit event, log event object was of an incorrect type");
551         }
552     }
553     catch (exception& ex) {
554         m_log.warn("exception auditing event: %s", ex.what());
555     }
556     return nullptr;
557 }
558
559 #endif
560
561 void AssertionConsumerService::maintainHistory(
562     const Application& application, const HTTPRequest& request, HTTPResponse& response, const char* entityID
563     ) const
564 {
565     static const char* defProps="; path=/";
566
567     const PropertySet* sessionProps=application.getPropertySet("Sessions");
568     pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
569
570     if (idpHistory.first && idpHistory.second) {
571         pair<bool,const char*> cookieProps=sessionProps->getString("idpHistoryProps");
572         if (!cookieProps.first)
573             cookieProps=sessionProps->getString("cookieProps");
574         if (!cookieProps.first)
575             cookieProps.second=defProps;
576
577         // Set an IdP history cookie locally (essentially just a CDC).
578         CommonDomainCookie cdc(request.getCookie(CommonDomainCookie::CDCName));
579
580         // Either leave in memory or set an expiration.
581         pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
582         if (!days.first || days.second==0) {
583             string c = string(cdc.set(entityID)) + cookieProps.second;
584             response.setCookie(CommonDomainCookie::CDCName, c.c_str());
585         }
586         else {
587             time_t now=time(nullptr) + (days.second * 24 * 60 * 60);
588 #ifdef HAVE_GMTIME_R
589             struct tm res;
590             struct tm* ptime=gmtime_r(&now,&res);
591 #else
592             struct tm* ptime=gmtime(&now);
593 #endif
594             char timebuf[64];
595             strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
596             string c = string(cdc.set(entityID)) + cookieProps.second + "; expires=" + timebuf;
597             response.setCookie(CommonDomainCookie::CDCName, c.c_str());
598         }
599     }
600 }