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