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