Change audience handling and validators to separate out entityID.
[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         EntityDescriptor* entity = policy.getIssuerMetadata() ? dynamic_cast<EntityDescriptor*>(policy.getIssuerMetadata()->getParent()) : NULL;
290
291         bool ownedName = false;
292         NameID* nameid = mgmtRequest->getNameID();
293         if (!nameid) {
294             // Check for EncryptedID.
295             EncryptedID* encname = mgmtRequest->getEncryptedID();
296             if (encname) {
297                 CredentialResolver* cr=application.getCredentialResolver();
298                 if (!cr)
299                     m_log.warn("found encrypted NameID, but no decryption credential was available");
300                 else {
301                     Locker credlocker(cr);
302                     auto_ptr<MetadataCredentialCriteria> mcc(
303                         policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL
304                         );
305                     try {
306                         auto_ptr<XMLObject> decryptedID(encname->decrypt(*cr,application.getRelyingParty(entity)->getXMLString("entityID").second,mcc.get()));
307                         nameid = dynamic_cast<NameID*>(decryptedID.get());
308                         if (nameid) {
309                             ownedName = true;
310                             decryptedID.release();
311                         }
312                     }
313                     catch (exception& ex) {
314                         m_log.error(ex.what());
315                     }
316                 }
317             }
318         }
319         if (!nameid) {
320             // No NameID, so must respond with an error.
321             m_log.error("NameID not found in request");
322             return sendResponse(
323                 mgmtRequest->getID(),
324                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "NameID not found in request.",
325                 relayState.c_str(),
326                 policy.getIssuerMetadata(),
327                 application,
328                 response,
329                 m_decoder->isUserAgentPresent()
330                 );
331         }
332
333         auto_ptr<NameID> namewrapper(ownedName ? nameid : NULL);
334
335         // For a front-channel request, we have to match the information in the request
336         // against the current session.
337         if (!session_id.empty()) {
338             if (!cache->matches(application, request, entity, *nameid, NULL)) {
339                 return sendResponse(
340                     mgmtRequest->getID(),
341                     StatusCode::REQUESTER, StatusCode::REQUEST_DENIED, "Active session did not match NameID mgmt request.",
342                     relayState.c_str(),
343                     policy.getIssuerMetadata(),
344                     application,
345                     response,
346                     true
347                     );
348             }
349
350         }
351
352         // Determine what's happening...
353         bool ownedNewID = false;
354         NewID* newid = NULL;
355         if (!mgmtRequest->getTerminate()) {
356             // Better be a NewID in there.
357             newid = mgmtRequest->getNewID();
358             if (!newid) {
359                 // Check for NewEncryptedID.
360                 NewEncryptedID* encnewid = mgmtRequest->getNewEncryptedID();
361                 if (encnewid) {
362                     CredentialResolver* cr=application.getCredentialResolver();
363                     if (!cr)
364                         m_log.warn("found encrypted NewID, but no decryption credential was available");
365                     else {
366                         Locker credlocker(cr);
367                         auto_ptr<MetadataCredentialCriteria> mcc(
368                             policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL
369                             );
370                         try {
371                             auto_ptr<XMLObject> decryptedID(encnewid->decrypt(*cr,application.getRelyingParty(entity)->getXMLString("entityID").second,mcc.get()));
372                             newid = dynamic_cast<NewID*>(decryptedID.get());
373                             if (newid) {
374                                 ownedNewID = true;
375                                 decryptedID.release();
376                             }
377                         }
378                         catch (exception& ex) {
379                             m_log.error(ex.what());
380                         }
381                     }
382                 }
383             }
384
385             if (!newid) {
386                 // No NewID, so must respond with an error.
387                 m_log.error("NewID not found in request");
388                 return sendResponse(
389                     mgmtRequest->getID(),
390                     StatusCode::REQUESTER, NULL, "NewID not found in request.",
391                     relayState.c_str(),
392                     policy.getIssuerMetadata(),
393                     application,
394                     response,
395                     m_decoder->isUserAgentPresent()
396                     );
397             }
398         }
399
400         auto_ptr<NewID> newwrapper(ownedNewID ? newid : NULL);
401
402         // TODO: maybe support in-place modification of sessions?
403         /*
404         vector<string> sessions;
405         try {
406             time_t expires = logoutRequest->getNotOnOrAfter() ? logoutRequest->getNotOnOrAfterEpoch() : 0;
407             cache->logout(entity, *nameid, &indexes, expires, application, sessions);
408
409             // Now we actually terminate everything except for the active session,
410             // if this is front-channel, for notification purposes.
411             for (vector<string>::const_iterator sit = sessions.begin(); sit != sessions.end(); ++sit)
412                 if (session_id && strcmp(sit->c_str(), session_id))
413                     cache->remove(sit->c_str(), application);
414         }
415         catch (exception& ex) {
416             m_log.error("error while logging out matching sessions: %s", ex.what());
417             return sendResponse(
418                 logoutRequest->getID(),
419                 StatusCode::RESPONDER, NULL, ex.what(),
420                 relayState.c_str(),
421                 policy.getIssuerMetadata(),
422                 application,
423                 response,
424                 m_decoder->isUserAgentPresent()
425                 );
426         }
427         */
428
429         // Do back-channel app notifications.
430         // Not supporting front-channel due to privacy fears.
431         bool worked = notifyBackChannel(application, request.getRequestURL(), *nameid, newid);
432
433         return sendResponse(
434             mgmtRequest->getID(),
435             worked ? StatusCode::SUCCESS : StatusCode::RESPONDER,
436             NULL,
437             NULL,
438             relayState.c_str(),
439             policy.getIssuerMetadata(),
440             application,
441             response,
442             m_decoder->isUserAgentPresent()
443             );
444     }
445
446     // A ManageNameIDResponse completes an SP-initiated sequence, currently not supported.
447     /*
448     const ManageNameIDResponse* mgmtResponse = dynamic_cast<ManageNameIDResponse*>(msg.get());
449     if (mgmtResponse) {
450         if (!policy.isAuthenticated()) {
451             SecurityPolicyException ex("Security of ManageNameIDResponse not established.");
452             if (policy.getIssuerMetadata())
453                 annotateException(&ex, policy.getIssuerMetadata()); // throws it
454             ex.raise();
455         }
456         checkError(mgmtResponse, policy.getIssuerMetadata()); // throws if Status doesn't look good...
457
458         // Return template for completion.
459         return sendLogoutPage(application, response, false, "Global logout completed.");
460     }
461     */
462
463     FatalProfileException ex("Incoming message was not a samlp:ManageNameIDRequest.");
464     if (policy.getIssuerMetadata())
465         annotateException(&ex, policy.getIssuerMetadata()); // throws it
466     ex.raise();
467     return make_pair(false,0L);  // never happen, satisfies compiler
468 #else
469     throw ConfigurationException("Cannot process NameID mgmt message using lite version of shibsp library.");
470 #endif
471 }
472
473 #ifndef SHIBSP_LITE
474
475 pair<bool,long> SAML2NameIDMgmt::sendResponse(
476     const XMLCh* requestID,
477     const XMLCh* code,
478     const XMLCh* subcode,
479     const char* msg,
480     const char* relayState,
481     const RoleDescriptor* role,
482     const Application& application,
483     HTTPResponse& httpResponse,
484     bool front
485     ) const
486 {
487     // Get endpoint and encoder to use.
488     const EndpointType* ep = NULL;
489     const MessageEncoder* encoder = NULL;
490     if (front) {
491         const IDPSSODescriptor* idp = dynamic_cast<const IDPSSODescriptor*>(role);
492         for (vector<const XMLCh*>::const_iterator b = m_bindings.begin(); idp && b!=m_bindings.end(); ++b) {
493             if (ep=EndpointManager<ManageNameIDService>(idp->getManageNameIDServices()).getByBinding(*b)) {
494                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
495                 if (enc!=m_encoders.end())
496                     encoder = enc->second;
497                 break;
498             }
499         }
500         if (!ep || !encoder) {
501             auto_ptr_char id(dynamic_cast<EntityDescriptor*>(role->getParent())->getEntityID());
502             m_log.error("unable to locate compatible NIM service for provider (%s)", id.get());
503             MetadataException ex("Unable to locate endpoint at IdP ($entityID) to send ManageNameIDResponse.");
504             annotateException(&ex, role);   // throws it
505         }
506     }
507     else {
508         encoder = m_encoders.begin()->second;
509     }
510
511     // Prepare response.
512     auto_ptr<ManageNameIDResponse> nim(ManageNameIDResponseBuilder::buildManageNameIDResponse());
513     nim->setInResponseTo(requestID);
514     if (ep) {
515         const XMLCh* loc = ep->getResponseLocation();
516         if (!loc || !*loc)
517             loc = ep->getLocation();
518         nim->setDestination(loc);
519     }
520     Issuer* issuer = IssuerBuilder::buildIssuer();
521     nim->setIssuer(issuer);
522     issuer->setName(application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role->getParent()))->getXMLString("entityID").second);
523     fillStatus(*nim.get(), code, subcode, msg);
524
525     auto_ptr_char dest(nim->getDestination());
526
527     long ret = sendMessage(*encoder, nim.get(), relayState, dest.get(), role, application, httpResponse);
528     nim.release();  // freed by encoder
529     return make_pair(true,ret);
530 }
531
532 #include "util/SPConstants.h"
533 #include <xmltooling/impl/AnyElement.h>
534 #include <xmltooling/soap/SOAP.h>
535 #include <xmltooling/soap/SOAPClient.h>
536 using namespace soap11;
537 namespace {
538     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);
539
540     class SHIBSP_DLLLOCAL SOAPNotifier : public soap11::SOAPClient
541     {
542     public:
543         SOAPNotifier() {}
544         virtual ~SOAPNotifier() {}
545     private:
546         void prepareTransport(SOAPTransport& transport) {
547             transport.setVerifyHost(false);
548         }
549     };
550 };
551
552 bool SAML2NameIDMgmt::notifyBackChannel(
553     const Application& application, const char* requestURL, const NameID& nameid, const NewID* newid
554     ) const
555 {
556     unsigned int index = 0;
557     string endpoint = application.getNotificationURL(requestURL, false, index++);
558     if (endpoint.empty())
559         return true;
560
561     auto_ptr<Envelope> env(EnvelopeBuilder::buildEnvelope());
562     Body* body = BodyBuilder::buildBody();
563     env->setBody(body);
564     ElementProxy* msg = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, NameIDNotification);
565     body->getUnknownXMLObjects().push_back(msg);
566     msg->getUnknownXMLObjects().push_back(nameid.clone());
567     if (newid)
568         msg->getUnknownXMLObjects().push_back(newid->clone());
569     else
570         msg->getUnknownXMLObjects().push_back(TerminateBuilder::buildTerminate());
571
572     bool result = true;
573     SOAPNotifier soaper;
574     while (!endpoint.empty()) {
575         try {
576             soaper.send(*env.get(), SOAPTransport::Address(application.getId(), application.getId(), endpoint.c_str()));
577             delete soaper.receive();
578         }
579         catch (exception& ex) {
580             m_log.error("error notifying application of logout event: %s", ex.what());
581             result = false;
582         }
583         soaper.reset();
584         endpoint = application.getNotificationURL(requestURL, false, index++);
585     }
586     return result;
587 }
588
589 #endif