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