Change audience handling and validators to separate out entityID.
[shibboleth/sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.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  * SAML2LogoutInitiator.cpp
19  * 
20  * Triggers SP-initiated logout for SAML 2.0 sessions.
21  */
22
23 #include "internal.h"
24 #include "exceptions.h"
25 #include "Application.h"
26 #include "ServiceProvider.h"
27 #include "SessionCache.h"
28 #include "handler/AbstractHandler.h"
29 #include "handler/LogoutHandler.h"
30
31 #ifndef SHIBSP_LITE
32 # include "binding/SOAPClient.h"
33 # include <saml/SAMLConfig.h>
34 # include <saml/saml2/core/Protocols.h>
35 # include <saml/saml2/binding/SAML2SOAPClient.h>
36 # include <saml/saml2/metadata/EndpointManager.h>
37 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
38 using namespace opensaml::saml2;
39 using namespace opensaml::saml2p;
40 using namespace opensaml::saml2md;
41 using namespace opensaml;
42 #else
43 # include "lite/SAMLConstants.h"
44 #endif
45
46 using namespace shibsp;
47 using namespace xmltooling;
48 using namespace std;
49
50 namespace shibsp {
51
52 #if defined (_MSC_VER)
53     #pragma warning( push )
54     #pragma warning( disable : 4250 )
55 #endif
56     
57     class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutHandler
58     {
59     public:
60         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
61         virtual ~SAML2LogoutInitiator() {
62 #ifndef SHIBSP_LITE
63             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
64                 XMLString::release(&m_outgoing);
65                 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
66             }
67 #endif
68         }
69         
70         void setParent(const PropertySet* parent);
71         void receive(DDF& in, ostream& out);
72         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
73
74 #ifndef SHIBSP_LITE
75         const char* getType() const {
76             return "LogoutInitiator";
77         }
78 #endif
79
80     private:
81         pair<bool,long> doRequest(
82             const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
83             ) const;
84
85         string m_appId;
86 #ifndef SHIBSP_LITE
87         LogoutRequest* buildRequest(
88             const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=NULL
89             ) const;
90
91         XMLCh* m_outgoing;
92         vector<const XMLCh*> m_bindings;
93         map<const XMLCh*,MessageEncoder*> m_encoders;
94 #endif
95         auto_ptr_char m_protocol;
96     };
97
98 #if defined (_MSC_VER)
99     #pragma warning( pop )
100 #endif
101
102     Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
103     {
104         return new SAML2LogoutInitiator(p.first, p.second);
105     }
106 };
107
108 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
109     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
110 #ifndef SHIBSP_LITE
111         m_outgoing(NULL),
112 #endif
113         m_protocol(samlconstants::SAML20P_NS)
114 {
115 #ifndef SHIBSP_LITE
116     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
117         // Handle outgoing binding setup.
118         pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings");
119         if (outgoing.first) {
120             m_outgoing = XMLString::replicate(outgoing.second);
121             XMLString::trim(m_outgoing);
122         }
123         else {
124             // No override, so we'll install a default binding precedence.
125             string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
126                 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
127             m_outgoing = XMLString::transcode(prec.c_str());
128         }
129
130         int pos;
131         XMLCh* start = m_outgoing;
132         while (start && *start) {
133             pos = XMLString::indexOf(start,chSpace);
134             if (pos != -1)
135                 *(start + pos)=chNull;
136             m_bindings.push_back(start);
137             try {
138                 auto_ptr_char b(start);
139                 MessageEncoder * encoder =
140                     SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,NULL));
141                 m_encoders[start] = encoder;
142                 m_log.debug("supporting outgoing binding (%s)", b.get());
143             }
144             catch (exception& ex) {
145                 m_log.error("error building MessageEncoder: %s", ex.what());
146             }
147             if (pos != -1)
148                 start = start + pos + 1;
149             else
150                 break;
151         }
152     }
153 #endif
154
155     pair<bool,const char*> loc = getString("Location");
156     if (loc.first) {
157         string address = m_appId + loc.second + "::run::SAML2LI";
158         setAddress(address.c_str());
159     }
160 }
161
162 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
163 {
164     DOMPropertySet::setParent(parent);
165     pair<bool,const char*> loc = getString("Location");
166     if (loc.first) {
167         string address = m_appId + loc.second + "::run::SAML2LI";
168         setAddress(address.c_str());
169     }
170     else {
171         m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
172     }
173 }
174
175 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
176 {
177     // Defer to base class for front-channel loop first.
178     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
179     if (ret.first)
180         return ret;
181
182     // At this point we know the front-channel is handled.
183     // We need the session to do any other work.
184
185     Session* session = NULL;
186     try {
187         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
188         if (!session)
189             return make_pair(false,0L);
190
191         // We only handle SAML 2.0 sessions.
192         if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
193             session->unlock();
194             return make_pair(false,0L);
195         }
196     }
197     catch (exception& ex) {
198         m_log.error("error accessing current session: %s", ex.what());
199         return make_pair(false,0L);
200     }
201
202     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
203         // When out of process, we run natively.
204         return doRequest(request.getApplication(), request, request, session);
205     }
206     else {
207         // When not out of process, we remote the request.
208         session->unlock();
209         vector<string> headers(1,"Cookie");
210         DDF out,in = wrap(request,&headers);
211         DDFJanitor jin(in), jout(out);
212         out=request.getServiceProvider().getListenerService()->send(in);
213         return unwrap(request, out);
214     }
215 }
216
217 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
218 {
219 #ifndef SHIBSP_LITE
220     // Defer to base class for notifications
221     if (in["notify"].integer() == 1)
222         return LogoutHandler::receive(in, 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 logout", aid ? aid : "(missing)");
230         throw ConfigurationException("Unable to locate application for logout, deleted?");
231     }
232     
233     // Unpack the request.
234     auto_ptr<HTTPRequest> req(getRequest(in));
235
236     // Set up a response shim.
237     DDF ret(NULL);
238     DDFJanitor jout(ret);
239     auto_ptr<HTTPResponse> resp(getResponse(ret));
240     
241     Session* session = NULL;
242     try {
243          session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), NULL, NULL);
244     }
245     catch (exception& ex) {
246         m_log.error("error accessing current session: %s", ex.what());
247     }
248
249     // With no session, we just skip the request and let it fall through to an empty struct return.
250     if (session) {
251         if (session->getNameID() && session->getEntityID()) {
252             // Since we're remoted, the result should either be a throw, which we pass on,
253             // a false/0 return, which we just return as an empty structure, or a response/redirect,
254             // which we capture in the facade and send back.
255             doRequest(*app, *req.get(), *resp.get(), session);
256         }
257         else {
258              m_log.error("no NameID or issuing entityID found in session");
259              session->unlock();
260              app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
261         }
262     }
263     out << ret;
264 #else
265     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
266 #endif
267 }
268
269 pair<bool,long> SAML2LogoutInitiator::doRequest(
270     const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
271     ) const
272 {
273     // Do back channel notification.
274     vector<string> sessions(1, session->getID());
275     if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
276         session->unlock();
277         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
278         return sendLogoutPage(application, httpRequest, httpResponse, true, "Partial logout failure.");
279     }
280
281 #ifndef SHIBSP_LITE
282     pair<bool,long> ret = make_pair(false,0L);
283     try {
284         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
285         MetadataProvider* m = application.getMetadataProvider();
286         Locker metadataLocker(m);
287         MetadataProvider::Criteria mc(session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
288         pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
289         if (!entity.first) {
290             throw MetadataException(
291                 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
292                 );
293         }
294         else if (!entity.second) {
295             throw MetadataException(
296                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
297                 );
298         }
299
300         const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
301         const EndpointType* ep=NULL;
302         const MessageEncoder* encoder=NULL;
303         vector<const XMLCh*>::const_iterator b;
304         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
305             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
306                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
307                 if (enc!=m_encoders.end())
308                     encoder = enc->second;
309                 break;
310             }
311         }
312         if (!ep || !encoder) {
313             m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
314             shibsp::SecurityPolicy policy(application);
315             shibsp::SOAPClient soaper(policy);
316             MetadataCredentialCriteria mcc(*role);
317
318             LogoutResponse* logoutResponse=NULL;
319             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
320             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
321             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
322                 try {
323                     if (!XMLString::equals((*epit)->getBinding(),binding.get()))
324                         continue;
325                     LogoutRequest* msg = buildRequest(application, *session, *role);
326                     auto_ptr_char dest((*epit)->getLocation());
327
328                     SAML2SOAPClient client(soaper, false);
329                     client.sendSAML(msg, application.getId(), mcc, dest.get());
330                     StatusResponseType* srt = client.receiveSAML();
331                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
332                         delete srt;
333                         break;
334                     }
335                 }
336                 catch (exception& ex) {
337                     m_log.error("error sending LogoutRequest message: %s", ex.what());
338                     soaper.reset();
339                 }
340             }
341
342             if (!logoutResponse)
343                 ret = sendLogoutPage(application, httpRequest, httpResponse, false, "Identity provider did not respond to logout request.");
344             else if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
345                    !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
346                 delete logoutResponse;
347                 ret = sendLogoutPage(application, httpRequest, httpResponse, false, "Identity provider returned a SAML error in response to logout request.");
348             }
349             else {
350                 delete logoutResponse;
351                 const char* returnloc = httpRequest.getParameter("return");
352                 if (returnloc) {
353                     ret.second = httpResponse.sendRedirect(returnloc);
354                     ret.first = true;
355                 }
356                 ret = sendLogoutPage(application, httpRequest, httpResponse, false, "Logout completed successfully.");
357             }
358
359             if (session) {
360                 session->unlock();
361                 session = NULL;
362                 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
363             }
364             return ret;
365         }
366
367         // Save off return location as RelayState.
368         string relayState;
369         const char* returnloc = httpRequest.getParameter("return");
370         if (returnloc) {
371             relayState = returnloc;
372             preserveRelayState(application, httpResponse, relayState);
373         }
374
375         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
376
377         msg->setDestination(ep->getLocation());
378         auto_ptr_char dest(ep->getLocation());
379         ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
380         ret.first = true;
381         msg.release();  // freed by encoder
382     }
383     catch (exception& ex) {
384         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
385     }
386
387     if (session) {
388         session->unlock();
389         session = NULL;
390         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
391     }
392
393     return ret;
394 #else
395     session->unlock();
396     application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
397     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
398 #endif
399 }
400
401 #ifndef SHIBSP_LITE
402
403 LogoutRequest* SAML2LogoutInitiator::buildRequest(
404     const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
405     ) const
406 {
407     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
408
409     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
410     Issuer* issuer = IssuerBuilder::buildIssuer();
411     msg->setIssuer(issuer);
412     issuer->setName(relyingParty->getXMLString("entityID").second);
413     auto_ptr_XMLCh index(session.getSessionIndex());
414     if (index.get() && *index.get()) {
415         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
416         msg->getSessionIndexs().push_back(si);
417         si->setSessionIndex(index.get());
418     }
419
420     const NameID* nameid = session.getNameID();
421     pair<bool,const char*> flag = relyingParty->getString("encryption");
422     if (flag.first &&
423         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
424         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
425         MetadataCredentialCriteria mcc(role);
426         encrypted->encrypt(
427             *nameid,
428             *(application.getMetadataProvider()),
429             mcc,
430             encoder ? encoder->isCompact() : false,
431             relyingParty->getXMLString("encryptionAlg").second
432             );
433         msg->setEncryptedID(encrypted.release());
434     }
435
436     if (!encoder) {
437         // No encoder being used, so sign for SOAP client manually.
438         flag = relyingParty->getString("signing");
439         if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
440             CredentialResolver* credResolver=application.getCredentialResolver();
441             if (credResolver) {
442                 Locker credLocker(credResolver);
443                 // Fill in criteria to use.
444                 MetadataCredentialCriteria mcc(role);
445                 mcc.setUsage(Credential::SIGNING_CREDENTIAL);
446                 pair<bool,const char*> keyName = relyingParty->getString("keyName");
447                 if (keyName.first)
448                     mcc.getKeyNames().insert(keyName.second);
449                 pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signingAlg");
450                 if (sigalg.first)
451                     mcc.setXMLAlgorithm(sigalg.second);
452                 const Credential* cred = credResolver->resolve(&mcc);
453                 if (cred) {
454                     xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();
455                     msg->setSignature(sig);
456                     if (sigalg.first)
457                         sig->setSignatureAlgorithm(sigalg.second);
458                     sigalg = relyingParty->getXMLString("digestAlg");
459                     if (sigalg.first) {
460                         ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());
461                         if (cr)
462                             cr->setDigestAlgorithm(sigalg.second);
463                     }
464             
465                     // Sign response while marshalling.
466                     vector<xmlsignature::Signature*> sigs(1,sig);
467                     msg->marshall((DOMDocument*)NULL,&sigs,cred);
468                 }
469                 else {
470                     m_log.warn("no signing credential resolved, leaving message unsigned");
471                 }
472             }
473             else {
474                 m_log.warn("no credential resolver installed, leaving message unsigned");
475             }
476         }
477     }
478
479     return msg.release();
480 }
481
482 #endif