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