SSPCPP-616 - clean up concatenated string literals
[shibboleth/cpp-sp.git] / shibsp / handler / impl / SAML2NameIDMgmt.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  * SAML2NameIDMgmt.cpp
23  *
24  * Handles SAML 2.0 NameID management protocol messages.
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/AbstractHandler.h"
33 #include "handler/RemotedHandler.h"
34 #include "util/SPConstants.h"
35
36 #ifndef SHIBSP_LITE
37 # include "SessionCache.h"
38 # include "security/SecurityPolicy.h"
39 # include "security/SecurityPolicyProvider.h"
40 # include <fstream>
41 # include <boost/algorithm/string.hpp>
42 # include <boost/iterator/indirect_iterator.hpp>
43 # include <saml/exceptions.h>
44 # include <saml/SAMLConfig.h>
45 # include <saml/saml2/core/Protocols.h>
46 # include <saml/saml2/metadata/EndpointManager.h>
47 # include <saml/saml2/metadata/Metadata.h>
48 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
49 # include <xmltooling/util/URLEncoder.h>
50 using namespace opensaml::saml2;
51 using namespace opensaml::saml2p;
52 using namespace opensaml::saml2md;
53 using namespace opensaml;
54 #else
55 # include "lite/SAMLConstants.h"
56 #endif
57
58 #include <boost/scoped_ptr.hpp>
59
60 using namespace shibsp;
61 using namespace xmltooling;
62 using namespace boost;
63 using namespace std;
64
65 namespace shibsp {
66
67 #if defined (_MSC_VER)
68     #pragma warning( push )
69     #pragma warning( disable : 4250 )
70 #endif
71
72     class SHIBSP_DLLLOCAL SAML2NameIDMgmt : public AbstractHandler, public RemotedHandler
73     {
74     public:
75         SAML2NameIDMgmt(const DOMElement* e, const char* appId);
76         virtual ~SAML2NameIDMgmt() {}
77
78         void receive(DDF& in, ostream& out);
79         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
80
81 #ifndef SHIBSP_LITE
82         void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
83             const char* loc = getString("Location").second;
84             string hurl(handlerURL);
85             if (*loc != '/')
86                 hurl += '/';
87             hurl += loc;
88             auto_ptr_XMLCh widen(hurl.c_str());
89             ManageNameIDService* ep = ManageNameIDServiceBuilder::buildManageNameIDService();
90             ep->setLocation(widen.get());
91             ep->setBinding(getXMLString("Binding").second);
92             role.getManageNameIDServices().push_back(ep);
93             role.addSupport(samlconstants::SAML20P_NS);
94         }
95
96         const char* getType() const {
97             return "ManageNameIDService";
98         }
99 #endif
100         const XMLCh* getProtocolFamily() const {
101             return samlconstants::SAML20P_NS;
102         }
103
104     private:
105         pair<bool,long> doRequest(const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse) const;
106
107 #ifndef SHIBSP_LITE
108         bool notifyBackChannel(const Application& application, const char* requestURL, const NameID& nameid, const NewID* newid) const;
109
110         pair<bool,long> sendResponse(
111             const XMLCh* requestID,
112             const XMLCh* code,
113             const XMLCh* subcode,
114             const char* msg,
115             const char* relayState,
116             const RoleDescriptor* role,
117             const Application& application,
118             HTTPResponse& httpResponse,
119             bool front
120             ) const;
121
122         scoped_ptr<MessageDecoder> m_decoder;
123         vector<string> m_bindings;
124         map< string,boost::shared_ptr<MessageEncoder> > m_encoders;
125 #endif
126     };
127
128 #if defined (_MSC_VER)
129     #pragma warning( pop )
130 #endif
131
132     Handler* SHIBSP_DLLLOCAL SAML2NameIDMgmtFactory(const pair<const DOMElement*,const char*>& p)
133     {
134         return new SAML2NameIDMgmt(p.first, p.second);
135     }
136 };
137
138 SAML2NameIDMgmt::SAML2NameIDMgmt(const DOMElement* e, const char* appId)
139     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".NameIDMgmt.SAML2"))
140 {
141 #ifndef SHIBSP_LITE
142     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
143         SAMLConfig& conf = SAMLConfig::getConfig();
144
145         // Handle incoming binding.
146         m_decoder.reset(
147             conf.MessageDecoderManager.newPlugin(
148                 getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
149                 )
150             );
151         m_decoder->setArtifactResolver(SPConfig::getConfig().getArtifactResolver());
152
153         if (m_decoder->isUserAgentPresent()) {
154             // Handle front-channel binding setup.
155             string dupBindings;
156             pair<bool,const char*> outgoing = getString("outgoingBindings", m_configNS.get());
157             if (outgoing.first) {
158                 dupBindings = outgoing.second;
159                 trim(dupBindings);
160             }
161             else {
162                 // No override, so we'll install a default binding precedence.
163                 dupBindings = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
164                     samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
165             }
166
167             split(m_bindings, dupBindings, is_space(), algorithm::token_compress_on);
168             for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
169                 try {
170                     boost::shared_ptr<MessageEncoder> encoder(
171                         conf.MessageEncoderManager.newPlugin(*b, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS))
172                         );
173                     if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
174                         m_encoders[*b] = encoder;
175                         m_log.debug("supporting outgoing binding (%s)", b->c_str());
176                     }
177                     else {
178                         m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b->c_str());
179                     }
180                 }
181                 catch (std::exception& ex) {
182                     m_log.error("error building MessageEncoder: %s", ex.what());
183                 }
184             }
185         }
186         else {
187             pair<bool,const char*> b = getString("Binding");
188             boost::shared_ptr<MessageEncoder> encoder(
189                 conf.MessageEncoderManager.newPlugin(b.second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS))
190                 );
191             m_encoders[b.second] = encoder;
192         }
193     }
194 #endif
195
196     string address(appId);
197     address += getString("Location").second;
198     setAddress(address.c_str());
199 }
200
201 pair<bool,long> SAML2NameIDMgmt::run(SPRequest& request, bool isHandler) const
202 {
203     SPConfig& conf = SPConfig::getConfig();
204     if (conf.isEnabled(SPConfig::OutOfProcess)) {
205         // When out of process, we run natively and directly process the message.
206         return doRequest(request.getApplication(), request, request);
207     }
208     else {
209         // When not out of process, we remote all the message processing.
210         vector<string> headers(1,"Cookie");
211         headers.push_back("User-Agent");
212         DDF out,in = wrap(request, &headers, true);
213         DDFJanitor jin(in), jout(out);
214         out=request.getServiceProvider().getListenerService()->send(in);
215         return unwrap(request, out);
216     }
217 }
218
219 void SAML2NameIDMgmt::receive(DDF& in, ostream& out)
220 {
221     // Find application.
222     const char* aid = in["application_id"].string();
223     const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
224     if (!app) {
225         // Something's horribly wrong.
226         m_log.error("couldn't find application (%s) for NameID mgmt", aid ? aid : "(missing)");
227         throw ConfigurationException("Unable to locate application for NameID mgmt, deleted?");
228     }
229
230     // Unpack the request.
231     scoped_ptr<HTTPRequest> req(getRequest(in));
232
233     // Wrap a response shim.
234     DDF ret(nullptr);
235     DDFJanitor jout(ret);
236     scoped_ptr<HTTPResponse> resp(getResponse(ret));
237
238     // Since we're remoted, the result should either be a throw, which we pass on,
239     // a false/0 return, which we just return as an empty structure, or a response/redirect,
240     // which we capture in the facade and send back.
241     doRequest(*app, *req, *resp);
242     out << ret;
243 }
244
245 pair<bool,long> SAML2NameIDMgmt::doRequest(const Application& application, const HTTPRequest& request, HTTPResponse& response) const
246 {
247 #ifndef SHIBSP_LITE
248     SessionCache* cache = application.getServiceProvider().getSessionCache();
249
250     // Locate policy key.
251     pair<bool,const char*> policyId = getString("policyId", m_configNS.get());  // may be namespace-qualified inside handler element
252     if (!policyId.first)
253         policyId = getString("policyId");   // try unqualified
254     if (!policyId.first)
255         policyId = application.getString("policyId");   // unqualified in Application(s) element
256
257     // Lock metadata for use by policy.
258     Locker metadataLocker(application.getMetadataProvider());
259
260     // Create the policy.
261     scoped_ptr<SecurityPolicy> policy(
262         application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, &IDPSSODescriptor::ELEMENT_QNAME, policyId.second)
263         );
264
265     // Decode the message.
266     string relayState;
267     scoped_ptr<XMLObject> msg(m_decoder->decode(relayState, request, *policy));
268     const ManageNameIDRequest* mgmtRequest = dynamic_cast<ManageNameIDRequest*>(msg.get());
269     if (mgmtRequest) {
270         if (!policy->isAuthenticated())
271             throw SecurityPolicyException("Security of ManageNameIDRequest not established.");
272
273         // Message from IdP to change or terminate a NameID.
274
275         // If this is front-channel, we have to have a session_id to use already.
276         string session_id = cache->active(application, request);
277         if (m_decoder->isUserAgentPresent() && session_id.empty()) {
278             m_log.error("no active session");
279             return sendResponse(
280                 mgmtRequest->getID(),
281                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "No active session found in request.",
282                 relayState.c_str(),
283                 policy->getIssuerMetadata(),
284                 application,
285                 response,
286                 true
287                 );
288         }
289
290         EntityDescriptor* entity = policy->getIssuerMetadata() ? dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) : nullptr;
291
292         scoped_ptr<XMLObject> decryptedID;
293         NameID* nameid = mgmtRequest->getNameID();
294         if (!nameid) {
295             // Check for EncryptedID.
296             EncryptedID* encname = mgmtRequest->getEncryptedID();
297             if (encname) {
298                 CredentialResolver* cr=application.getCredentialResolver();
299                 if (!cr)
300                     m_log.warn("found encrypted NameID, but no decryption credential was available");
301                 else {
302                     Locker credlocker(cr);
303                     scoped_ptr<MetadataCredentialCriteria> mcc(
304                         policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
305                         );
306                     try {
307                         decryptedID.reset(encname->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get()));
308                         nameid = dynamic_cast<NameID*>(decryptedID.get());
309                     }
310                     catch (std::exception& ex) {
311                         m_log.error(ex.what());
312                     }
313                 }
314             }
315         }
316         if (!nameid) {
317             // No NameID, so must respond with an error.
318             m_log.error("NameID not found in request");
319             return sendResponse(
320                 mgmtRequest->getID(),
321                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "NameID not found in request.",
322                 relayState.c_str(),
323                 policy->getIssuerMetadata(),
324                 application,
325                 response,
326                 m_decoder->isUserAgentPresent()
327                 );
328         }
329
330         // For a front-channel request, we have to match the information in the request
331         // against the current session.
332         if (!session_id.empty()) {
333             if (!cache->matches(application, request, entity, *nameid, nullptr)) {
334                 return sendResponse(
335                     mgmtRequest->getID(),
336                     StatusCode::REQUESTER, StatusCode::REQUEST_DENIED, "Active session did not match NameID mgmt request.",
337                     relayState.c_str(),
338                     policy->getIssuerMetadata(),
339                     application,
340                     response,
341                     true
342                     );
343             }
344
345         }
346
347         // Determine what's happening...
348         scoped_ptr<XMLObject> newDecryptedID;
349         NewID* newid = nullptr;
350         if (!mgmtRequest->getTerminate()) {
351             // Better be a NewID in there.
352             newid = mgmtRequest->getNewID();
353             if (!newid) {
354                 // Check for NewEncryptedID.
355                 NewEncryptedID* encnewid = mgmtRequest->getNewEncryptedID();
356                 if (encnewid) {
357                     CredentialResolver* cr=application.getCredentialResolver();
358                     if (!cr)
359                         m_log.warn("found encrypted NewID, but no decryption credential was available");
360                     else {
361                         Locker credlocker(cr);
362                         scoped_ptr<MetadataCredentialCriteria> mcc(
363                             policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
364                             );
365                         try {
366                             newDecryptedID.reset(encnewid->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get()));
367                             newid = dynamic_cast<NewID*>(newDecryptedID.get());
368                         }
369                         catch (std::exception& ex) {
370                             m_log.error(ex.what());
371                         }
372                     }
373                 }
374             }
375
376             if (!newid) {
377                 // No NewID, so must respond with an error.
378                 m_log.error("NewID not found in request");
379                 return sendResponse(
380                     mgmtRequest->getID(),
381                     StatusCode::REQUESTER, nullptr, "NewID not found in request.",
382                     relayState.c_str(),
383                     policy->getIssuerMetadata(),
384                     application,
385                     response,
386                     m_decoder->isUserAgentPresent()
387                     );
388             }
389         }
390
391         // TODO: maybe support in-place modification of sessions?
392         /*
393         vector<string> sessions;
394         try {
395             time_t expires = logoutRequest->getNotOnOrAfter() ? logoutRequest->getNotOnOrAfterEpoch() : 0;
396             cache->logout(entity, *nameid, &indexes, expires, application, sessions);
397
398             // Now we actually terminate everything except for the active session,
399             // if this is front-channel, for notification purposes.
400             for (vector<string>::const_iterator sit = sessions.begin(); sit != sessions.end(); ++sit)
401                 if (session_id && strcmp(sit->c_str(), session_id))
402                     cache->remove(sit->c_str(), application);
403         }
404         catch (exception& ex) {
405             m_log.error("error while logging out matching sessions: %s", ex.what());
406             return sendResponse(
407                 logoutRequest->getID(),
408                 StatusCode::RESPONDER, nullptr, ex.what(),
409                 relayState.c_str(),
410                 policy.getIssuerMetadata(),
411                 application,
412                 response,
413                 m_decoder->isUserAgentPresent()
414                 );
415         }
416         */
417
418         // Do back-channel app notifications.
419         // Not supporting front-channel due to privacy concerns.
420         bool worked = notifyBackChannel(application, request.getRequestURL(), *nameid, newid);
421
422         return sendResponse(
423             mgmtRequest->getID(),
424             worked ? StatusCode::SUCCESS : StatusCode::RESPONDER,
425             nullptr,
426             nullptr,
427             relayState.c_str(),
428             policy->getIssuerMetadata(),
429             application,
430             response,
431             m_decoder->isUserAgentPresent()
432             );
433     }
434
435     // A ManageNameIDResponse completes an SP-initiated sequence, currently not supported.
436     /*
437     const ManageNameIDResponse* mgmtResponse = dynamic_cast<ManageNameIDResponse*>(msg.get());
438     if (mgmtResponse) {
439         if (!policy.isAuthenticated()) {
440             SecurityPolicyException ex("Security of ManageNameIDResponse not established.");
441             if (policy.getIssuerMetadata())
442                 annotateException(&ex, policy.getIssuerMetadata()); // throws it
443             ex.raise();
444         }
445         checkError(mgmtResponse, policy.getIssuerMetadata()); // throws if Status doesn't look good...
446
447         // Return template for completion.
448         return sendLogoutPage(application, response, false, "Global logout completed.");
449     }
450     */
451
452     FatalProfileException ex("Incoming message was not a samlp:ManageNameIDRequest.");
453     annotateException(&ex, policy->getIssuerMetadata()); // throws it
454     return make_pair(false, 0L);  // never happen, satisfies compiler
455 #else
456     throw ConfigurationException("Cannot process NameID mgmt message using lite version of shibsp library.");
457 #endif
458 }
459
460 #ifndef SHIBSP_LITE
461
462 pair<bool,long> SAML2NameIDMgmt::sendResponse(
463     const XMLCh* requestID,
464     const XMLCh* code,
465     const XMLCh* subcode,
466     const char* msg,
467     const char* relayState,
468     const RoleDescriptor* role,
469     const Application& application,
470     HTTPResponse& httpResponse,
471     bool front
472     ) const
473 {
474     // Get endpoint and encoder to use.
475     const EndpointType* ep = nullptr;
476     const MessageEncoder* encoder = nullptr;
477     if (front) {
478         const IDPSSODescriptor* idp = dynamic_cast<const IDPSSODescriptor*>(role);
479         for (vector<string>::const_iterator b = m_bindings.begin(); idp && b != m_bindings.end(); ++b) {
480             auto_ptr_XMLCh wideb(b->c_str());
481             if ((ep = EndpointManager<ManageNameIDService>(idp->getManageNameIDServices()).getByBinding(wideb.get()))) {
482                 map< string,boost::shared_ptr<MessageEncoder> >::const_iterator enc = m_encoders.find(*b);
483                 if (enc != m_encoders.end())
484                     encoder = enc->second.get();
485                 break;
486             }
487         }
488         if (!ep || !encoder) {
489             auto_ptr_char id(dynamic_cast<EntityDescriptor*>(role->getParent())->getEntityID());
490             m_log.error("unable to locate compatible NIM service for provider (%s)", id.get());
491             MetadataException ex("Unable to locate endpoint at IdP ($entityID) to send ManageNameIDResponse.");
492             annotateException(&ex, role);   // throws it
493         }
494     }
495     else {
496         encoder = m_encoders.begin()->second.get();
497     }
498
499     // Prepare response.
500     auto_ptr<ManageNameIDResponse> nim(ManageNameIDResponseBuilder::buildManageNameIDResponse());
501     nim->setInResponseTo(requestID);
502     if (ep) {
503         const XMLCh* loc = ep->getResponseLocation();
504         if (!loc || !*loc)
505             loc = ep->getLocation();
506         nim->setDestination(loc);
507     }
508     Issuer* issuer = IssuerBuilder::buildIssuer();
509     nim->setIssuer(issuer);
510     issuer->setName(application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role->getParent()))->getXMLString("entityID").second);
511     fillStatus(*nim, code, subcode, msg);
512
513     auto_ptr_char dest(nim->getDestination());
514
515     long ret = sendMessage(*encoder, nim.get(), relayState, dest.get(), role, application, httpResponse);
516     nim.release();  // freed by encoder
517     return make_pair(true, ret);
518 }
519
520 #include "util/SPConstants.h"
521 #include <xmltooling/impl/AnyElement.h>
522 #include <xmltooling/soap/SOAP.h>
523 #include <xmltooling/soap/SOAPClient.h>
524 #include <xmltooling/soap/HTTPSOAPTransport.h>
525 using namespace soap11;
526 namespace {
527     static const XMLCh NameIDNotification[] =   UNICODE_LITERAL_18(N,a,m,e,I,D,N,o,t,i,f,i,c,a,t,i,o,n);
528
529     class SHIBSP_DLLLOCAL SOAPNotifier : public soap11::SOAPClient
530     {
531     public:
532         SOAPNotifier() {}
533         virtual ~SOAPNotifier() {}
534     private:
535         void prepareTransport(SOAPTransport& transport) {
536             transport.setVerifyHost(false);
537             HTTPSOAPTransport* http = dynamic_cast<HTTPSOAPTransport*>(&transport);
538             if (http) {
539                 http->useChunkedEncoding(false);
540                 http->setRequestHeader(PACKAGE_NAME, PACKAGE_VERSION);
541             }
542         }
543     };
544 };
545
546 bool SAML2NameIDMgmt::notifyBackChannel(
547     const Application& application, const char* requestURL, const NameID& nameid, const NewID* newid
548     ) const
549 {
550     unsigned int index = 0;
551     string endpoint = application.getNotificationURL(requestURL, false, index++);
552     if (endpoint.empty())
553         return true;
554
555     scoped_ptr<Envelope> env(EnvelopeBuilder::buildEnvelope());
556     Body* body = BodyBuilder::buildBody();
557     env->setBody(body);
558     ElementProxy* msg = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, NameIDNotification);
559     body->getUnknownXMLObjects().push_back(msg);
560     msg->getUnknownXMLObjects().push_back(nameid.clone());
561     if (newid)
562         msg->getUnknownXMLObjects().push_back(newid->clone());
563     else
564         msg->getUnknownXMLObjects().push_back(TerminateBuilder::buildTerminate());
565
566     bool result = true;
567     SOAPNotifier soaper;
568     while (!endpoint.empty()) {
569         try {
570             soaper.send(*env, SOAPTransport::Address(application.getId(), application.getId(), endpoint.c_str()));
571             delete soaper.receive();
572         }
573         catch (std::exception& ex) {
574             m_log.error("error notifying application of logout event: %s", ex.what());
575             result = false;
576         }
577         soaper.reset();
578         endpoint = application.getNotificationURL(requestURL, false, index++);
579     }
580     return result;
581 }
582
583 #endif