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