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