1 /**
2 * Licensed to the University Corporation for Advanced Internet
3 * Development, Inc. (UCAID) under one or more contributor license
4 * agreements. See the NOTICE file distributed with this work for
5 * additional information regarding copyright ownership.
6 *
7 * UCAID licenses this file to you under the Apache License,
8 * Version 2.0 (the "License"); you may not use this file except
9 * in compliance with the License. You may obtain a copy of the
10 * License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17 * either express or implied. See the License for the specific
18 * language governing permissions and limitations under the License.
19 */
20
21 /**
22 * adfs.cpp
23 *
24 * ADFSv1 extension library.
25 */
26
27 #if defined (_MSC_VER) || defined(__BORLANDC__)
28 # include "config_win32.h"
29 #else
30 # include "config.h"
31 #endif
32
33 #ifdef WIN32
34 # define _CRT_NONSTDC_NO_DEPRECATE 1
35 # define _CRT_SECURE_NO_DEPRECATE 1
36 # define ADFS_EXPORTS __declspec(dllexport)
37 #else
38 # define ADFS_EXPORTS
39 #endif
40
41 #include <shibsp/base.h>
42 #include <shibsp/exceptions.h>
43 #include <shibsp/Application.h>
44 #include <shibsp/ServiceProvider.h>
45 #include <shibsp/SessionCache.h>
46 #include <shibsp/SPConfig.h>
47 #include <shibsp/SPRequest.h>
48 #include <shibsp/TransactionLog.h>
49 #include <shibsp/handler/AssertionConsumerService.h>
50 #include <shibsp/handler/LogoutInitiator.h>
51 #include <shibsp/handler/SessionInitiator.h>
52 #include <xmltooling/logging.h>
53 #include <xmltooling/util/NDC.h>
54 #include <xmltooling/util/URLEncoder.h>
55 #include <xmltooling/util/XMLHelper.h>
56 #include <memory>
57
58 #ifndef SHIBSP_LITE
59 # include <shibsp/attribute/resolver/ResolutionContext.h>
60 # include <shibsp/metadata/MetadataProviderCriteria.h>
61 # include <saml/SAMLConfig.h>
62 # include <saml/exceptions.h>
63 # include <saml/binding/SecurityPolicy.h>
64 # include <saml/saml1/core/Assertions.h>
65 # include <saml/saml2/core/Assertions.h>
66 # include <saml/saml2/metadata/Metadata.h>
67 # include <saml/saml2/metadata/EndpointManager.h>
68 # include <xmltooling/XMLToolingConfig.h>
69 # include <xmltooling/impl/AnyElement.h>
70 # include <xmltooling/util/ParserPool.h>
71 # include <xmltooling/validation/ValidatorSuite.h>
72 using namespace opensaml::saml2md;
73 # ifndef min
74 # define min(a,b) (((a) < (b)) ? (a) : (b))
75 # endif
76 #endif
77 using namespace shibsp;
78 using namespace opensaml;
79 using namespace xmltooling::logging;
80 using namespace xmltooling;
81 using namespace xercesc;
82 using namespace boost;
83 using namespace std;
84
85 #define WSFED_NS "http://schemas.xmlsoap.org/ws/2003/07/secext"
86 #define WSTRUST_NS "http://schemas.xmlsoap.org/ws/2005/02/trust"
87
88 namespace {
89
90 #ifndef SHIBSP_LITE
91 class SHIBSP_DLLLOCAL ADFSDecoder : public MessageDecoder
92 {
93 auto_ptr_XMLCh m_ns;
94 public:
ADFSDecoder()95 ADFSDecoder() : m_ns(WSTRUST_NS) {}
~ADFSDecoder()96 virtual ~ADFSDecoder() {}
97
getProtocolFamily() const98 const XMLCh* getProtocolFamily() const {
99 return m_ns.get();
100 }
101
102 XMLObject* decode(
103 string& relayState,
104 const GenericRequest& genericRequest,
105 const GenericResponse* genericResponse,
106 SecurityPolicy& policy
107 ) const;
108
109 protected:
extractMessageDetails(const XMLObject & message,const GenericRequest & req,const XMLCh * protocol,SecurityPolicy & policy) const110 void extractMessageDetails(
111 const XMLObject& message, const GenericRequest& req, const XMLCh* protocol, SecurityPolicy& policy
112 ) const {
113 }
114 };
115
ADFSDecoderFactory(const DOMElement * const &,bool)116 MessageDecoder* ADFSDecoderFactory(const DOMElement* const &, bool)
117 {
118 return new ADFSDecoder();
119 }
120 #endif
121
122 #if defined (_MSC_VER)
123 #pragma warning( push )
124 #pragma warning( disable : 4250 )
125 #endif
126
127 class SHIBSP_DLLLOCAL ADFSSessionInitiator : public SessionInitiator, public AbstractHandler, public RemotedHandler
128 {
129 public:
ADFSSessionInitiator(const DOMElement * e,const char * appId)130 ADFSSessionInitiator(const DOMElement* e, const char* appId)
131 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".SessionInitiator.ADFS"), nullptr, this), m_appId(appId), m_binding(WSFED_NS) {
132 SPConfig::getConfig().deprecation().warn("ADFS SessionInitiator");
133
134 // If Location isn't set, defer address registration until the setParent call.
135 pair<bool,const char*> loc = getString("Location");
136 if (loc.first) {
137 string address = m_appId + loc.second + "::run::ADFSSI";
138 setAddress(address.c_str());
139 }
140 }
~ADFSSessionInitiator()141 virtual ~ADFSSessionInitiator() {}
142
setParent(const PropertySet * parent)143 void setParent(const PropertySet* parent) {
144 DOMPropertySet::setParent(parent);
145 pair<bool,const char*> loc = getString("Location");
146 if (loc.first) {
147 string address = m_appId + loc.second + "::run::ADFSSI";
148 setAddress(address.c_str());
149 }
150 else {
151 m_log.warn("no Location property in ADFS SessionInitiator (or parent), can't register as remoted handler");
152 }
153 }
154
155 void receive(DDF& in, ostream& out);
156 pair<bool,long> unwrap(SPRequest& request, DDF& out) const;
157 pair<bool,long> run(SPRequest& request, string& entityID, bool isHandler=true) const;
158
getProtocolFamily() const159 const XMLCh* getProtocolFamily() const {
160 return m_binding.get();
161 }
162
163 #ifndef SHIBSP_LITE
generateMetadata(saml2md::SPSSODescriptor & role,const char * handlerURL) const164 void generateMetadata(saml2md::SPSSODescriptor& role, const char* handlerURL) const {
165 doGenerateMetadata(role, handlerURL);
166 }
167 #endif
168
169 private:
170 pair<bool,long> doRequest(
171 const Application& application,
172 const HTTPRequest* httpRequest,
173 HTTPResponse& httpResponse,
174 const char* entityID,
175 const char* acsLocation,
176 const char* authnContextClassRef,
177 string& relayState
178 ) const;
179 string m_appId;
180 auto_ptr_XMLCh m_binding;
181 };
182
183 class SHIBSP_DLLLOCAL ADFSConsumer : public shibsp::AssertionConsumerService
184 {
185 auto_ptr_XMLCh m_protocol;
186 public:
ADFSConsumer(const DOMElement * e,const char * appId,bool deprecationSupport)187 ADFSConsumer(const DOMElement* e, const char* appId, bool deprecationSupport)
188 : shibsp::AssertionConsumerService(e, appId, Category::getInstance(SHIBSP_LOGCAT ".SSO.ADFS"), nullptr, nullptr, deprecationSupport),
189 m_protocol(WSFED_NS) {
190 SPConfig::getConfig().deprecation().warn("ADFS AssertionConsumerService");
191 }
~ADFSConsumer()192 virtual ~ADFSConsumer() {}
193
194 #ifndef SHIBSP_LITE
generateMetadata(SPSSODescriptor & role,const char * handlerURL) const195 void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
196 AssertionConsumerService::generateMetadata(role, handlerURL);
197 role.addSupport(m_protocol.get());
198 }
199
200 private:
getProfile() const201 const char* getProfile() const {
202 return WSFED_NS;
203 }
204
205 void implementProtocol(
206 const Application& application,
207 const HTTPRequest& httpRequest,
208 HTTPResponse& httpResponse,
209 SecurityPolicy& policy,
210 const PropertySet*,
211 const XMLObject& xmlObject
212 ) const;
213 #else
getProtocolFamily() const214 const XMLCh* getProtocolFamily() const {
215 return m_protocol.get();
216 }
217 #endif
218 };
219
220 class SHIBSP_DLLLOCAL ADFSLogoutInitiator : public AbstractHandler, public LogoutInitiator
221 {
222 public:
ADFSLogoutInitiator(const DOMElement * e,const char * appId)223 ADFSLogoutInitiator(const DOMElement* e, const char* appId)
224 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".LogoutInitiator.ADFS")), m_appId(appId), m_binding(WSFED_NS) {
225
226 SPConfig::getConfig().deprecation().warn("ADFS LogoutInitiator");
227
228 // If Location isn't set, defer address registration until the setParent call.
229 pair<bool,const char*> loc = getString("Location");
230 if (loc.first) {
231 string address = m_appId + loc.second + "::run::ADFSLI";
232 setAddress(address.c_str());
233 }
234 }
~ADFSLogoutInitiator()235 virtual ~ADFSLogoutInitiator() {}
236
setParent(const PropertySet * parent)237 void setParent(const PropertySet* parent) {
238 DOMPropertySet::setParent(parent);
239 pair<bool,const char*> loc = getString("Location");
240 if (loc.first) {
241 string address = m_appId + loc.second + "::run::ADFSLI";
242 setAddress(address.c_str());
243 }
244 else {
245 m_log.warn("no Location property in ADFS LogoutInitiator (or parent), can't register as remoted handler");
246 }
247 }
248
249 void receive(DDF& in, ostream& out);
250 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
251
getProtocolFamily() const252 const XMLCh* getProtocolFamily() const {
253 return m_binding.get();
254 }
255
256 private:
257 pair<bool,long> doRequest(const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session) const;
258
259 string m_appId;
260 auto_ptr_XMLCh m_binding;
261 };
262
263 class SHIBSP_DLLLOCAL ADFSLogout : public AbstractHandler, public LogoutHandler
264 {
265 public:
ADFSLogout(const DOMElement * e,const char * appId,bool deprecationSupport)266 ADFSLogout(const DOMElement* e, const char* appId, bool deprecationSupport)
267 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".Logout.ADFS")), m_login(e, appId, deprecationSupport) {
268
269 SPConfig::getConfig().deprecation().warn("ADFS Logout handler");
270
271 m_initiator = false;
272 #ifndef SHIBSP_LITE
273 m_preserve.push_back("wreply");
274 string address = string(appId) + getString("Location").second + "::run::ADFSLO";
275 setAddress(address.c_str());
276 #endif
277 }
~ADFSLogout()278 virtual ~ADFSLogout() {}
279
280 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
281
282 #ifndef SHIBSP_LITE
generateMetadata(SPSSODescriptor & role,const char * handlerURL) const283 void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
284 m_login.generateMetadata(role, handlerURL);
285 const char* loc = getString("Location").second;
286 string hurl(handlerURL);
287 if (*loc != '/')
288 hurl += '/';
289 hurl += loc;
290 auto_ptr_XMLCh widen(hurl.c_str());
291 SingleLogoutService* ep = SingleLogoutServiceBuilder::buildSingleLogoutService();
292 ep->setLocation(widen.get());
293 ep->setBinding(m_login.getProtocolFamily());
294 role.getSingleLogoutServices().push_back(ep);
295 }
296
getType() const297 const char* getType() const {
298 return m_login.getType();
299 }
300 #endif
getProtocolFamily() const301 const XMLCh* getProtocolFamily() const {
302 return m_login.getProtocolFamily();
303 }
304
305 private:
306 ADFSConsumer m_login;
307 };
308
309 #if defined (_MSC_VER)
310 #pragma warning( pop )
311 #endif
312
ADFSSessionInitiatorFactory(const pair<const DOMElement *,const char * > & p,bool)313 SessionInitiator* ADFSSessionInitiatorFactory(const pair<const DOMElement*,const char*>& p, bool)
314 {
315 return new ADFSSessionInitiator(p.first, p.second);
316 }
317
ADFSLogoutFactory(const pair<const DOMElement *,const char * > & p,bool deprecationSupport)318 Handler* ADFSLogoutFactory(const pair<const DOMElement*,const char*>& p, bool deprecationSupport)
319 {
320 return new ADFSLogout(p.first, p.second, deprecationSupport);
321 }
322
ADFSLogoutInitiatorFactory(const pair<const DOMElement *,const char * > & p,bool)323 Handler* ADFSLogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p, bool)
324 {
325 return new ADFSLogoutInitiator(p.first, p.second);
326 }
327
328 const XMLCh RequestedSecurityToken[] = UNICODE_LITERAL_22(R,e,q,u,e,s,t,e,d,S,e,c,u,r,i,t,y,T,o,k,e,n);
329 const XMLCh RequestSecurityTokenResponse[] =UNICODE_LITERAL_28(R,e,q,u,e,s,t,S,e,c,u,r,i,t,y,T,o,k,e,n,R,e,s,p,o,n,s,e);
330 };
331
xmltooling_extension_init(void *)332 extern "C" int ADFS_EXPORTS xmltooling_extension_init(void*)
333 {
334 SPConfig& conf=SPConfig::getConfig();
335 conf.SessionInitiatorManager.registerFactory("ADFS", ADFSSessionInitiatorFactory);
336 conf.LogoutInitiatorManager.registerFactory("ADFS", ADFSLogoutInitiatorFactory);
337 conf.AssertionConsumerServiceManager.registerFactory("ADFS", ADFSLogoutFactory);
338 conf.AssertionConsumerServiceManager.registerFactory(WSFED_NS, ADFSLogoutFactory);
339 #ifndef SHIBSP_LITE
340 SAMLConfig::getConfig().MessageDecoderManager.registerFactory(WSFED_NS, ADFSDecoderFactory);
341 XMLObjectBuilder::registerBuilder(xmltooling::QName(WSTRUST_NS,"RequestedSecurityToken"), new AnyElementBuilder());
342 XMLObjectBuilder::registerBuilder(xmltooling::QName(WSTRUST_NS,"RequestSecurityTokenResponse"), new AnyElementBuilder());
343 #endif
344 return 0;
345 }
346
xmltooling_extension_term()347 extern "C" void ADFS_EXPORTS xmltooling_extension_term()
348 {
349 /* should get unregistered during normal shutdown...
350 SPConfig& conf=SPConfig::getConfig();
351 conf.SessionInitiatorManager.deregisterFactory("ADFS");
352 conf.LogoutInitiatorManager.deregisterFactory("ADFS");
353 conf.AssertionConsumerServiceManager.deregisterFactory("ADFS");
354 conf.AssertionConsumerServiceManager.deregisterFactory(WSFED_NS);
355 #ifndef SHIBSP_LITE
356 SAMLConfig::getConfig().MessageDecoderManager.deregisterFactory(WSFED_NS);
357 #endif
358 */
359 }
360
run(SPRequest & request,string & entityID,bool isHandler) const361 pair<bool,long> ADFSSessionInitiator::run(SPRequest& request, string& entityID, bool isHandler) const
362 {
363 // We have to know the IdP to function.
364 if (entityID.empty() || !checkCompatibility(request, isHandler))
365 return make_pair(false, 0L);
366
367 string target;
368 pair<bool,const char*> prop;
369 pair<bool,const char*> acClass;
370 const Handler* ACS = nullptr;
371 const Application& app = request.getApplication();
372
373 if (isHandler) {
374 prop.second = request.getParameter("acsIndex");
375 if (prop.second && *prop.second) {
376 SPConfig::getConfig().deprecation().warn("Use of acsIndex when specifying response endpoint");
377 ACS = app.getAssertionConsumerServiceByIndex(atoi(prop.second));
378 if (!ACS)
379 request.log(SPRequest::SPWarn, "invalid acsIndex specified in request, using acsIndex property");
380 }
381
382 prop = getString("target", request);
383 if (prop.first)
384 target = prop.second;
385
386 // Since we're passing the ACS by value, we need to compute the return URL,
387 // so we'll need the target resource for real.
388 recoverRelayState(app, request, request, target, false);
389 app.limitRedirect(request, target.c_str());
390
391 // Default is to allow externally supplied settings.
392 pair<bool,bool> externalInput = getBool("externalInput");
393 unsigned int settingMask = HANDLER_PROPERTY_MAP | HANDLER_PROPERTY_FIXED;
394 if (!externalInput.first || externalInput.second)
395 settingMask |= HANDLER_PROPERTY_REQUEST;
396
397 acClass = getString("authnContextClassRef", request, settingMask);
398 }
399 else {
400 // Check for a hardwired target value in the map or handler.
401 prop = getString("target", request, HANDLER_PROPERTY_MAP|HANDLER_PROPERTY_FIXED);
402 if (prop.first)
403 target = prop.second;
404 else
405 target = request.getRequestURL();
406
407 acClass = getString("authnContextClassRef", request, HANDLER_PROPERTY_MAP|HANDLER_PROPERTY_FIXED);
408 }
409
410 if (!ACS) {
411 pair<bool,unsigned int> index = getUnsignedInt("acsIndex", request, HANDLER_PROPERTY_MAP|HANDLER_PROPERTY_FIXED);
412 if (index.first) {
413 SPConfig::getConfig().deprecation().warn("Use of acsIndex when specifying response endpoint");
414 ACS = app.getAssertionConsumerServiceByIndex(index.second);
415 }
416 }
417
418 // Validate the ACS for use with this protocol.
419 if (!ACS || !XMLString::equals(getProtocolFamily(), ACS->getProtocolFamily())) {
420 if (ACS)
421 request.log(SPRequest::SPWarn, "invalid acsIndex property, or non-ADFS ACS, using default ADFS ACS");
422 ACS = app.getAssertionConsumerServiceByProtocol(getProtocolFamily());
423 if (!ACS)
424 throw ConfigurationException("Unable to locate an ADFS-compatible ACS in the configuration.");
425 }
426
427 // Since we're not passing by index, we need to fully compute the return URL.
428 // Compute the ACS URL. We add the ACS location to the base handlerURL.
429 string ACSloc = request.getHandlerURL(target.c_str());
430 prop = ACS->getString("Location");
431 if (prop.first)
432 ACSloc += prop.second;
433
434 if (isHandler) {
435 // We may already have RelayState set if we looped back here,
436 // but we've turned it back into a resource by this point, so if there's
437 // a target on the URL, reset to that value.
438 prop.second = request.getParameter("target");
439 if (prop.second && *prop.second)
440 target = prop.second;
441 }
442
443 m_log.debug("attempting to initiate session using ADFS with provider (%s)", entityID.c_str());
444
445 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
446 // Out of process means the POST data via the request can be exposed directly to the private method.
447 // The method will handle POST preservation if necessary *before* issuing the response, but only if
448 // it dispatches to an IdP.
449 return doRequest(app, &request, request, entityID.c_str(), ACSloc.c_str(), (acClass.first ? acClass.second : nullptr), target);
450 }
451
452 // Remote the call.
453 DDF out,in = DDF(m_address.c_str()).structure();
454 DDFJanitor jin(in), jout(out);
455 in.addmember("application_id").string(app.getId());
456 in.addmember("entity_id").string(entityID.c_str());
457 in.addmember("acsLocation").string(ACSloc.c_str());
458 if (!target.empty())
459 in.addmember("RelayState").unsafe_string(target.c_str());
460 if (acClass.first)
461 in.addmember("authnContextClassRef").string(acClass.second);
462
463 // Remote the processing.
464 out = send(request, in);
465 return unwrap(request, out);
466 }
467
unwrap(SPRequest & request,DDF & out) const468 pair<bool,long> ADFSSessionInitiator::unwrap(SPRequest& request, DDF& out) const
469 {
470 // See if there's any response to send back.
471 if (!out["redirect"].isnull() || !out["response"].isnull()) {
472 // If so, we're responsible for handling the POST data, probably by dropping a cookie.
473 preservePostData(request.getApplication(), request, request, out["RelayState"].string());
474 }
475 return RemotedHandler::unwrap(request, out);
476 }
477
receive(DDF & in,ostream & out)478 void ADFSSessionInitiator::receive(DDF& in, ostream& out)
479 {
480 // Find application.
481 const char* aid = in["application_id"].string();
482 const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
483 if (!app) {
484 // Something's horribly wrong.
485 m_log.error("couldn't find application (%s) to generate ADFS request", aid ? aid : "(missing)");
486 throw ConfigurationException("Unable to locate application for new session, deleted?");
487 }
488
489 const char* entityID = in["entity_id"].string();
490 const char* acsLocation = in["acsLocation"].string();
491 if (!entityID || !acsLocation)
492 throw ConfigurationException("No entityID or acsLocation parameter supplied to remoted SessionInitiator.");
493
494 DDF ret(nullptr);
495 DDFJanitor jout(ret);
496
497 // Wrap the outgoing object with a Response facade.
498 scoped_ptr<HTTPResponse> http(getResponse(*app, ret));
499
500 string relayState(in["RelayState"].string() ? in["RelayState"].string() : "");
501
502 // Since we're remoted, the result should either be a throw, which we pass on,
503 // a false/0 return, which we just return as an empty structure, or a response/redirect,
504 // which we capture in the facade and send back.
505 doRequest(*app, nullptr, *http, entityID, acsLocation, in["authnContextClassRef"].string(), relayState);
506 if (!ret.isstruct())
507 ret.structure();
508 ret.addmember("RelayState").unsafe_string(relayState.c_str());
509 out << ret;
510 }
511
doRequest(const Application & app,const HTTPRequest * httpRequest,HTTPResponse & httpResponse,const char * entityID,const char * acsLocation,const char * authnContextClassRef,string & relayState) const512 pair<bool,long> ADFSSessionInitiator::doRequest(
513 const Application& app,
514 const HTTPRequest* httpRequest,
515 HTTPResponse& httpResponse,
516 const char* entityID,
517 const char* acsLocation,
518 const char* authnContextClassRef,
519 string& relayState
520 ) const
521 {
522 #ifndef SHIBSP_LITE
523 // Use metadata to invoke the SSO service directly.
524 MetadataProvider* m = app.getMetadataProvider();
525 Locker locker(m);
526 MetadataProviderCriteria mc(app, entityID, &IDPSSODescriptor::ELEMENT_QNAME, m_binding.get());
527 pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
528 if (!entity.first) {
529 m_log.warn("unable to locate metadata for provider (%s)", entityID);
530 throw MetadataException("Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", entityID));
531 }
532 else if (!entity.second) {
533 m_log.log(getParent() ? Priority::INFO : Priority::WARN, "unable to locate ADFS-aware identity provider role for provider (%s)", entityID);
534 if (getParent())
535 return make_pair(false, 0L);
536 throw MetadataException("Unable to locate ADFS-aware identity provider role for provider ($entityID)", namedparams(1, "entityID", entityID));
537 }
538 const EndpointType* ep = EndpointManager<SingleSignOnService>(
539 dynamic_cast<const IDPSSODescriptor*>(entity.second)->getSingleSignOnServices()
540 ).getByBinding(m_binding.get());
541 if (!ep) {
542 m_log.warn("unable to locate compatible SSO service for provider (%s)", entityID);
543 if (getParent())
544 return make_pair(false, 0L);
545 throw MetadataException("Unable to locate compatible SSO service for provider ($entityID)", namedparams(1, "entityID", entityID));
546 }
547
548 preserveRelayState(app, httpResponse, relayState);
549
550 scoped_ptr<AuthnRequestEvent> ar_event(newAuthnRequestEvent(app, httpRequest));
551 if (ar_event.get()) {
552 ar_event->m_binding = WSFED_NS;
553 ar_event->m_protocol = WSFED_NS;
554 ar_event->m_peer = entity.first;
555 app.getServiceProvider().getTransactionLog()->write(*ar_event);
556 }
557
558 // UTC timestamp
559 time_t epoch=time(nullptr);
560 #ifndef HAVE_GMTIME_R
561 struct tm* ptime=gmtime(&epoch);
562 #else
563 struct tm res;
564 struct tm* ptime=gmtime_r(&epoch,&res);
565 #endif
566 char timebuf[32];
567 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
568
569 auto_ptr_char dest(ep->getLocation());
570 const URLEncoder* urlenc = XMLToolingConfig::getConfig().getURLEncoder();
571
572 const PropertySet* relyingParty = app.getRelyingParty(entity.first);
573
574 string req=string(dest.get()) + (strchr(dest.get(),'?') ? '&' : '?') + "wa=wsignin1.0&wreply=" + urlenc->encode(acsLocation) +
575 "&wct=" + urlenc->encode(timebuf) + "&wtrealm=" + urlenc->encode(relyingParty->getString("entityID").second);
576 if (!authnContextClassRef) {
577 pair<bool,const char*> rpClass = relyingParty->getString("authnContextClassRef");
578 if (rpClass.first)
579 authnContextClassRef = rpClass.second;
580 }
581 if (authnContextClassRef)
582 req += "&wauth=" + urlenc->encode(authnContextClassRef);
583 if (!relayState.empty())
584 req += "&wctx=" + urlenc->encode(relayState.c_str());
585
586 if (httpRequest) {
587 // If the request object is available, we're responsible for the POST data.
588 preservePostData(app, *httpRequest, httpResponse, relayState.c_str());
589 }
590
591 return make_pair(true, httpResponse.sendRedirect(req.c_str()));
592 #else
593 return make_pair(false, 0L);
594 #endif
595 }
596
597 #ifndef SHIBSP_LITE
598
decode(string & relayState,const GenericRequest & genericRequest,const GenericResponse *,SecurityPolicy & policy) const599 XMLObject* ADFSDecoder::decode(
600 string& relayState, const GenericRequest& genericRequest, const GenericResponse*, SecurityPolicy& policy
601 ) const
602 {
603 #ifdef _DEBUG
604 xmltooling::NDC ndc("decode");
605 #endif
606 Category& log = Category::getInstance(SHIBSP_LOGCAT ".MessageDecoder.ADFS");
607
608 log.debug("validating input");
609 const HTTPRequest* httpRequest=dynamic_cast<const HTTPRequest*>(&genericRequest);
610 if (!httpRequest)
611 throw BindingException("Unable to cast request object to HTTPRequest type.");
612 if (strcmp(httpRequest->getMethod(),"POST"))
613 throw BindingException("Invalid HTTP method ($1).", params(1, httpRequest->getMethod()));
614 const char* param = httpRequest->getParameter("wa");
615 if (!param || strcmp(param, "wsignin1.0"))
616 throw BindingException("Missing or invalid wa parameter (should be wsignin1.0).");
617 param = httpRequest->getParameter("wctx");
618 if (param)
619 relayState = param;
620
621 param = httpRequest->getParameter("wresult");
622 if (!param)
623 throw BindingException("Request missing wresult parameter.");
624
625 log.debug("decoded ADFS response:\n%s", param);
626
627 // Parse and bind the document into an XMLObject.
628 istringstream is(param);
629 DOMDocument* doc = (policy.getValidating() ? XMLToolingConfig::getConfig().getValidatingParser()
630 : XMLToolingConfig::getConfig().getParser()).parse(is);
631 XercesJanitor<DOMDocument> janitor(doc);
632 auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
633 janitor.release();
634
635 if (!XMLString::equals(xmlObject->getElementQName().getLocalPart(), RequestSecurityTokenResponse)) {
636 log.error("unrecognized root element on message: %s", xmlObject->getElementQName().toString().c_str());
637 throw BindingException("Decoded message was not of the appropriate type.");
638 }
639
640 SchemaValidators.validate(xmlObject.get());
641
642 // Skip policy step here, there's no security in the wrapper.
643 // policy.evaluate(*xmlObject.get(), &genericRequest);
644
645 return xmlObject.release();
646 }
647
implementProtocol(const Application & application,const HTTPRequest & httpRequest,HTTPResponse & httpResponse,SecurityPolicy & policy,const PropertySet *,const XMLObject & xmlObject) const648 void ADFSConsumer::implementProtocol(
649 const Application& application,
650 const HTTPRequest& httpRequest,
651 HTTPResponse& httpResponse,
652 SecurityPolicy& policy,
653 const PropertySet*,
654 const XMLObject& xmlObject
655 ) const
656 {
657 // Implementation of ADFS profile.
658 m_log.debug("processing message against ADFS Passive Requester profile");
659
660 // With ADFS, all the security comes from the assertion, which is two levels down in the message.
661
662 const ElementProxy* response = dynamic_cast<const ElementProxy*>(&xmlObject);
663 if (!response || !response->hasChildren())
664 throw FatalProfileException("Incoming message was not of the proper type or contains no security token.");
665
666 const Assertion* token = nullptr;
667 for (vector<XMLObject*>::const_iterator xo = response->getUnknownXMLObjects().begin(); xo != response->getUnknownXMLObjects().end(); ++xo) {
668 // Look for the RequestedSecurityToken element.
669 if (XMLString::equals((*xo)->getElementQName().getLocalPart(), RequestedSecurityToken)) {
670 response = dynamic_cast<const ElementProxy*>(*xo);
671 if (!response || !response->hasChildren())
672 throw FatalProfileException("Token wrapper element did not contain a security token.");
673 token = dynamic_cast<const Assertion*>(response->getUnknownXMLObjects().front());
674 if (!token || !token->getSignature())
675 throw FatalProfileException("Incoming message did not contain a signed SAML assertion.");
676 break;
677 }
678 }
679
680 // Extract message and issuer details from assertion.
681 extractMessageDetails(*token, m_protocol.get(), policy);
682
683 // Populate recipient as audience.
684 const EntityDescriptor* entity = policy.getIssuerMetadata() ? dynamic_cast<const EntityDescriptor*>(policy.getIssuerMetadata()->getParent()) : nullptr;
685 policy.getAudiences().push_back(application.getRelyingParty(entity)->getXMLString("entityID").second);
686
687 // Run the policy over the assertion. Handles replay, freshness, and
688 // signature verification, assuming the relevant rules are configured,
689 // along with condition enforcement.
690 policy.evaluate(*token, &httpRequest);
691
692 // If no security is in place now, we kick it.
693 if (!policy.isAuthenticated())
694 throw SecurityPolicyException("Unable to establish security of incoming assertion.");
695
696 const saml1::NameIdentifier* saml1name=nullptr;
697 const saml1::AuthenticationStatement* saml1statement=nullptr;
698 const saml2::NameID* saml2name=nullptr;
699 const saml2::AuthnStatement* saml2statement=nullptr;
700 const XMLCh* authMethod=nullptr;
701 const XMLCh* authInstant=nullptr;
702 time_t now = time(nullptr), sessionExp = 0;
703 const PropertySet* sessionProps = application.getPropertySet("Sessions");
704
705 const saml1::Assertion* saml1token = dynamic_cast<const saml1::Assertion*>(token);
706 if (saml1token) {
707 // Now do profile validation to ensure we can use it for SSO.
708 if (!saml1token->getConditions() || !saml1token->getConditions()->getNotBefore() || !saml1token->getConditions()->getNotOnOrAfter())
709 throw FatalProfileException("Assertion did not contain time conditions.");
710 else if (saml1token->getAuthenticationStatements().empty())
711 throw FatalProfileException("Assertion did not contain an authentication statement.");
712
713 // authnskew allows rejection of SSO if AuthnInstant is too old.
714 pair<bool,unsigned int> authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair<bool,unsigned int>(false,0);
715
716 saml1statement = saml1token->getAuthenticationStatements().front();
717 if (saml1statement->getAuthenticationInstant()) {
718 if (saml1statement->getAuthenticationInstantEpoch() - XMLToolingConfig::getConfig().clock_skew_secs > now) {
719 throw FatalProfileException("The login time at your identity provider was future-dated.");
720 }
721 else if (authnskew.first && authnskew.second && saml1statement->getAuthenticationInstantEpoch() <= now &&
722 (now - saml1statement->getAuthenticationInstantEpoch() > authnskew.second)) {
723 throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the allowed limit.");
724 }
725 }
726 else if (authnskew.first && authnskew.second) {
727 throw FatalProfileException("Your identity provider did not supply a time of login, violating local policy.");
728 }
729
730 // Address checking.
731 saml1::SubjectLocality* locality = saml1statement->getSubjectLocality();
732 if (locality && locality->getIPAddress()) {
733 auto_ptr_char ip(locality->getIPAddress());
734 checkAddress(application, httpRequest, ip.get());
735 }
736
737 saml1name = saml1statement->getSubject()->getNameIdentifier();
738 authMethod = saml1statement->getAuthenticationMethod();
739 if (saml1statement->getAuthenticationInstant())
740 authInstant = saml1statement->getAuthenticationInstant()->getRawData();
741
742 // Session expiration.
743 pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
744 if (!lifetime.first || lifetime.second == 0)
745 lifetime.second = 28800;
746 sessionExp = now + lifetime.second;
747 }
748 else {
749 const saml2::Assertion* saml2token = dynamic_cast<const saml2::Assertion*>(token);
750 if (!saml2token)
751 throw FatalProfileException("Incoming message did not contain a recognized type of SAML assertion.");
752
753 // Now do profile validation to ensure we can use it for SSO.
754 if (!saml2token->getConditions() || !saml2token->getConditions()->getNotBefore() || !saml2token->getConditions()->getNotOnOrAfter())
755 throw FatalProfileException("Assertion did not contain time conditions.");
756 else if (saml2token->getAuthnStatements().empty())
757 throw FatalProfileException("Assertion did not contain an authentication statement.");
758
759 // authnskew allows rejection of SSO if AuthnInstant is too old.
760 pair<bool,unsigned int> authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair<bool,unsigned int>(false,0);
761
762 saml2statement = saml2token->getAuthnStatements().front();
763 if (authnskew.first && authnskew.second &&
764 saml2statement->getAuthnInstant() && (now - saml2statement->getAuthnInstantEpoch() > authnskew.second))
765 throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the limit.");
766
767 // Address checking.
768 saml2::SubjectLocality* locality = saml2statement->getSubjectLocality();
769 if (locality && locality->getAddress()) {
770 auto_ptr_char ip(locality->getAddress());
771 checkAddress(application, httpRequest, ip.get());
772 }
773
774 saml2name = saml2token->getSubject() ? saml2token->getSubject()->getNameID() : nullptr;
775 if (saml2statement->getAuthnContext() && saml2statement->getAuthnContext()->getAuthnContextClassRef())
776 authMethod = saml2statement->getAuthnContext()->getAuthnContextClassRef()->getReference();
777 if (saml2statement->getAuthnInstant())
778 authInstant = saml2statement->getAuthnInstant()->getRawData();
779
780 // Session expiration for SAML 2.0 is jointly IdP- and SP-driven.
781 sessionExp = saml2statement->getSessionNotOnOrAfter() ? saml2statement->getSessionNotOnOrAfterEpoch() : 0;
782 pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
783 if (!lifetime.first || lifetime.second == 0)
784 lifetime.second = 28800;
785 if (sessionExp == 0)
786 sessionExp = now + lifetime.second; // IdP says nothing, calulate based on SP.
787 else
788 sessionExp = min(sessionExp, now + lifetime.second); // Use the lowest.
789 }
790
791 m_log.debug("ADFS profile processing completed successfully");
792
793 // We've successfully "accepted" the SSO token.
794 // To complete processing, we need to extract and resolve attributes and then create the session.
795
796 // Normalize a SAML 1.x NameIdentifier...
797 scoped_ptr<saml2::NameID> nameid(saml1name ? saml2::NameIDBuilder::buildNameID() : nullptr);
798 if (saml1name) {
799 nameid->setName(saml1name->getName());
800 nameid->setFormat(saml1name->getFormat());
801 nameid->setNameQualifier(saml1name->getNameQualifier());
802 }
803
804 // The context will handle deleting attributes and new tokens.
805 vector<const Assertion*> tokens(1,token);
806 scoped_ptr<ResolutionContext> ctx(
807 resolveAttributes(
808 application,
809 &httpRequest,
810 policy.getIssuerMetadata(),
811 m_protocol.get(),
812 nullptr,
813 saml1name,
814 saml1statement,
815 (saml1name ? nameid.get() : saml2name),
816 saml2statement,
817 authMethod,
818 nullptr,
819 &tokens
820 )
821 );
822
823 if (ctx.get()) {
824 // Copy over any new tokens, but leave them in the context for cleanup.
825 tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
826 }
827
828 string session_id;
829 application.getServiceProvider().getSessionCache()->insert(
830 session_id,
831 application,
832 httpRequest,
833 httpResponse,
834 sessionExp,
835 entity,
836 m_protocol.get(),
837 (saml1name ? nameid.get() : saml2name),
838 authInstant,
839 nullptr,
840 authMethod,
841 nullptr,
842 &tokens,
843 ctx ? &ctx->getResolvedAttributes() : nullptr
844 );
845
846 scoped_ptr<LoginEvent> login_event(newLoginEvent(application, httpRequest));
847 if (login_event) {
848 login_event->m_sessionID = session_id.c_str();
849 login_event->m_peer = entity;
850 login_event->m_protocol = WSFED_NS;
851 login_event->m_binding = WSFED_NS;
852 login_event->m_saml1AuthnStatement = saml1statement;
853 login_event->m_nameID = (saml1name ? nameid.get() : saml2name);
854 login_event->m_saml2AuthnStatement = saml2statement;
855 if (ctx)
856 login_event->m_attributes = &ctx->getResolvedAttributes();
857 application.getServiceProvider().getTransactionLog()->write(*login_event);
858 }
859 }
860
861 #endif
862
run(SPRequest & request,bool) const863 pair<bool,long> ADFSLogoutInitiator::run(SPRequest& request, bool) const
864 {
865 // Normally we'd do notifications and session clearage here, but ADFS logout
866 // is missing the needed request/response features, so we have to rely on
867 // the IdP half to notify us back about the logout and do the work there.
868 // Basically we have no way to tell in the Logout receiving handler whether
869 // we initiated the logout or not.
870
871 Session* session = nullptr;
872 try {
873 session = request.getSession(false, true, false); // don't cache it and ignore all checks
874 if (!session)
875 return make_pair(false, 0L);
876
877 // We only handle ADFS sessions.
878 if (!XMLString::equals(session->getProtocol(), WSFED_NS) || !session->getEntityID()) {
879 session->unlock();
880 return make_pair(false, 0L);
881 }
882 }
883 catch (std::exception& ex) {
884 m_log.error("error accessing current session: %s", ex.what());
885 return make_pair(false,0L);
886 }
887
888 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
889 // When out of process, we run natively.
890 return doRequest(request.getApplication(), request, request, session);
891 }
892 else {
893 // When not out of process, we remote the request.
894 session->unlock();
895 vector<string> headers(1,"Cookie");
896 headers.push_back("User-Agent");
897 DDF out,in = wrap(request, &headers);
898 DDFJanitor jin(in), jout(out);
899 out = send(request, in);
900 return unwrap(request, out);
901 }
902 }
903
receive(DDF & in,ostream & out)904 void ADFSLogoutInitiator::receive(DDF& in, ostream& out)
905 {
906 #ifndef SHIBSP_LITE
907 // Defer to base class for notifications
908 if (in["notify"].integer() == 1)
909 return LogoutHandler::receive(in, out);
910
911 // Find application.
912 const char* aid = in["application_id"].string();
913 const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
914 if (!app) {
915 // Something's horribly wrong.
916 m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
917 throw ConfigurationException("Unable to locate application for logout, deleted?");
918 }
919
920 // Unpack the request.
921 scoped_ptr<HTTPRequest> req(getRequest(*app, in));
922
923 // Set up a response shim.
924 DDF ret(nullptr);
925 DDFJanitor jout(ret);
926 scoped_ptr<HTTPResponse> resp(getResponse(*app, ret));
927
928 Session* session = nullptr;
929 try {
930 session = app->getServiceProvider().getSessionCache()->find(*app, *req, nullptr, nullptr);
931 }
932 catch (std::exception& ex) {
933 m_log.error("error accessing current session: %s", ex.what());
934 }
935
936 // With no session, we just skip the request and let it fall through to an empty struct return.
937 if (session) {
938 if (session->getEntityID()) {
939 // Since we're remoted, the result should either be a throw, which we pass on,
940 // a false/0 return, which we just return as an empty structure, or a response/redirect,
941 // which we capture in the facade and send back.
942 doRequest(*app, *req, *resp, session);
943 }
944 else {
945 m_log.error("no issuing entityID found in session");
946 time_t revocationExp = session->getExpiration();
947 session->unlock();
948 app->getServiceProvider().getSessionCache()->remove(*app, *req, resp.get(), revocationExp);
949 }
950 }
951 out << ret;
952 #else
953 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
954 #endif
955 }
956
doRequest(const Application & application,const HTTPRequest & httpRequest,HTTPResponse & httpResponse,Session * session) const957 pair<bool,long> ADFSLogoutInitiator::doRequest(
958 const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
959 ) const
960 {
961 Locker sessionLocker(session, false);
962
963 // Do back channel notification.
964 vector<string> sessions(1, session->getID());
965 if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
966 #ifndef SHIBSP_LITE
967 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
968 if (logout_event) {
969 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
970 application.getServiceProvider().getTransactionLog()->write(*logout_event);
971 }
972 #endif
973 time_t revocationExp = session->getExpiration();
974 sessionLocker.assign();
975 session = nullptr;
976 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse, revocationExp);
977 return sendLogoutPage(application, httpRequest, httpResponse, "partial");
978 }
979
980 #ifndef SHIBSP_LITE
981 pair<bool,long> ret = make_pair(false, 0L);
982
983 try {
984 // With a session in hand, we can create a request message, if we can find a compatible endpoint.
985 MetadataProvider* m = application.getMetadataProvider();
986 Locker metadataLocker(m);
987 MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, m_binding.get());
988 pair<const EntityDescriptor*,const RoleDescriptor*> entity=m->getEntityDescriptor(mc);
989 if (!entity.first) {
990 throw MetadataException(
991 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
992 );
993 }
994 else if (!entity.second) {
995 throw MetadataException(
996 "Unable to locate ADFS IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
997 );
998 }
999
1000 const EndpointType* ep = EndpointManager<SingleLogoutService>(
1001 dynamic_cast<const IDPSSODescriptor*>(entity.second)->getSingleLogoutServices()
1002 ).getByBinding(m_binding.get());
1003 if (!ep) {
1004 throw MetadataException(
1005 "Unable to locate ADFS single logout service for identity provider ($entityID).",
1006 namedparams(1, "entityID", session->getEntityID())
1007 );
1008 }
1009
1010 const char* returnloc = httpRequest.getParameter("return");
1011 if (returnloc)
1012 application.limitRedirect(httpRequest, returnloc);
1013
1014 // Log the request.
1015 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
1016 if (logout_event) {
1017 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
1018 application.getServiceProvider().getTransactionLog()->write(*logout_event);
1019 }
1020
1021 auto_ptr_char dest(ep->getLocation());
1022 string req=string(dest.get()) + (strchr(dest.get(),'?') ? '&' : '?') + "wa=wsignout1.0";
1023 if (returnloc) {
1024 req += "&wreply=";
1025 if (*returnloc == '/') {
1026 string s(returnloc);
1027 httpRequest.absolutize(s);
1028 req += XMLToolingConfig::getConfig().getURLEncoder()->encode(s.c_str());
1029 }
1030 else {
1031 req += XMLToolingConfig::getConfig().getURLEncoder()->encode(returnloc);
1032 }
1033 }
1034 ret.second = httpResponse.sendRedirect(req.c_str());
1035 ret.first = true;
1036
1037 if (session) {
1038 time_t revocationExp = session->getExpiration();
1039 sessionLocker.assign();
1040 session = nullptr;
1041 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse, revocationExp);
1042 }
1043 }
1044 catch (const MetadataException& mex) {
1045 // Less noise for IdPs that don't support logout
1046 m_log.info("unable to issue ADFS logout request: %s", mex.what());
1047 }
1048 catch (const std::exception& ex) {
1049 m_log.error("error issuing ADFS logout request: %s", ex.what());
1050 }
1051
1052 return ret;
1053 #else
1054 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
1055 #endif
1056 }
1057
run(SPRequest & request,bool isHandler) const1058 pair<bool,long> ADFSLogout::run(SPRequest& request, bool isHandler) const
1059 {
1060 // Defer to base class for front-channel loop first.
1061 // This won't initiate the loop, only continue/end it.
1062 pair<bool,long> ret = LogoutHandler::run(request, isHandler);
1063 if (ret.first)
1064 return ret;
1065
1066 // wa parameter indicates the "action" to perform
1067 bool returning = false;
1068 const char* param = request.getParameter("wa");
1069 if (param) {
1070 if (!strcmp(param, "wsignin1.0"))
1071 return m_login.run(request, isHandler);
1072 else if (strcmp(param, "wsignout1.0") && strcmp(param, "wsignoutcleanup1.0"))
1073 throw FatalProfileException("Unsupported WS-Federation action parameter ($1).", params(1, param));
1074 }
1075 else if (strcmp(request.getMethod(),"GET") || !request.getParameter("notifying"))
1076 throw FatalProfileException("Unsupported request to ADFS protocol endpoint.");
1077 else
1078 returning = true;
1079
1080 param = request.getParameter("wreply");
1081 const Application& app = request.getApplication();
1082
1083 if (!returning) {
1084 // Pass control to the first front channel notification point, if any.
1085 map<string,string> parammap;
1086 if (param)
1087 parammap["wreply"] = param;
1088 pair<bool,long> result = notifyFrontChannel(app, request, request, ¶mmap);
1089 if (result.first)
1090 return result;
1091 }
1092
1093 // Best effort on back channel and to remove the user agent's session.
1094 string session_id = app.getServiceProvider().getSessionCache()->active(app, request);
1095 if (!session_id.empty()) {
1096 vector<string> sessions(1,session_id);
1097 notifyBackChannel(app, request.getRequestURL(), sessions, false);
1098 try {
1099 app.getServiceProvider().getSessionCache()->remove(app, request, &request);
1100 }
1101 catch (const std::exception& ex) {
1102 m_log.error("error removing session (%s): %s", session_id.c_str(), ex.what());
1103 }
1104 }
1105
1106 if (param) {
1107 if (*param == '/') {
1108 string p(param);
1109 request.absolutize(p);
1110 return make_pair(true, request.sendRedirect(p.c_str()));
1111 }
1112 else {
1113 app.limitRedirect(request, param);
1114 return make_pair(true, request.sendRedirect(param));
1115 }
1116 }
1117 return sendLogoutPage(app, request, request, "global");
1118 }
1119