shibboleth-2.0-afp-mf-basic.xsd \
shibboleth-2.0-afp-mf-saml.xsd \
shibboleth-2.0-attribute-map.xsd \
+ shibboleth-2.0-sp-notify.xsd \
metadata_v12_to_v13.xsl \
metadata_v13_to_v12.xsl \
trust_v13_to_v12.xsl
<system systemId="urn:mace:shibboleth:2.0:afp:mf:basic" uri="@-PKGXMLDIR-@/shibboleth-2.0-afp-mf-basic.xsd"/>
<system systemId="urn:mace:shibboleth:2.0:afp:mf:saml" uri="@-PKGXMLDIR-@/shibboleth-2.0-afp-mf-saml.xsd"/>
<system systemId="urn:mace:shibboleth:2.0:attribute-map" uri="@-PKGXMLDIR-@/shibboleth-2.0-attribute-map.xsd"/>
+ <system systemId="urn:mace:shibboleth:2.0:sp:notify" uri="@-PKGXMLDIR-@/shibboleth-2.0-sp-notify.xsd"/>
<system systemId="urn:mace:shibboleth:1.0" uri="@-PKGXMLDIR-@/shibboleth.xsd"/>
</catalog>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="urn:mace:shibboleth:2.0:sp:notify"
+ xmlns="http://www.w3.org/2001/XMLSchema"
+ xmlns:notify="urn:mace:shibboleth:2.0:sp:notify"
+ elementFormDefault="qualified">
+
+ <annotation>
+ <documentation>
+ Defines local application notification protocol used by SP.
+ </documentation>
+ </annotation>
+
+ <simpleType name="string">
+ <restriction base="string">
+ <minLength value="1"/>
+ </restriction>
+ </simpleType>
+
+ <element name="OK" type="notify:OKType"/>
+ <complexType name="OKType">
+ <sequence/>
+ </complexType>
+
+ <element name="LogoutNotification" type="notify:LogoutNotificationType"/>
+ <complexType name="LogoutNotificationType">
+ <annotation>
+ <documentation>Notifies application of a logout event.</documentation>
+ </annotation>
+ <sequence>
+ <element name="SessionID" type="notify:string" maxOccurs="unbounded"/>
+ </sequence>
+ <attribute name="type">
+ <simpleType>
+ <restriction base="string">
+ <enumeration value="local"/>
+ <enumeration value="global"/>
+ </restriction>
+ </simpleType>
+ </attribute>
+ </complexType>
+
+</schema>
/**
* Returns the designated notification URL, or an empty string if no more locations are specified.
*
- * @param request SP request to use to fill in missing pieces of URL
+ * @param request requested URL to use to fill in missing pieces of notification URL
* @param front true iff front channel notification is desired, false iff back channel is desired
* @param index zero-based index of URL to return
* @return the designated URL, or an empty string
*/
- virtual std::string getNotificationURL(const xmltooling::HTTPRequest& request, bool front, unsigned int index) const=0;
+ virtual std::string getNotificationURL(const char* request, bool front, unsigned int index) const=0;
/**
* Returns a set of attribute IDs to use as a REMOTE_USER value.
* Perform back-channel logout notifications for an Application.
*
* @param application the Application to notify
+ * @param requestURL requestURL that resulted in method call
* @param sessions array of session keys being logged out
+ * @param local true iff the logout operation is local to the SP, false iff global
* @return true iff all notifications succeeded
*/
- bool notifyBackChannel(const Application& application, const std::vector<std::string>& sessions) const;
+ bool notifyBackChannel(
+ const Application& application, const char* requestURL, const std::vector<std::string>& sessions, bool local
+ ) const;
};
#if defined (_MSC_VER)
if (session_id) {
// Do back channel notification.
vector<string> sessions(1, session_id);
- if (!notifyBackChannel(request.getApplication(), sessions)) {
+ if (!notifyBackChannel(request.getApplication(), request.getRequestURL(), sessions, true)) {
request.getApplication().getServiceProvider().getSessionCache()->remove(session_id, request.getApplication());
return sendLogoutPage(request.getApplication(), request, true, "Partial logout failure.");
}
#include <xmltooling/XMLToolingConfig.h>
#include <xmltooling/util/URLEncoder.h>
-#ifndef SHIBSP_LITE
-#endif
-
using namespace shibsp;
using namespace xmltooling;
using namespace std;
+pair<bool,long> LogoutHandler::sendLogoutPage(const Application& application, HTTPResponse& response, bool local, const char* status) const
+{
+ pair<bool,const char*> prop = application.getString(local ? "localLogout" : "globalLogout");
+ if (prop.first) {
+ response.setContentType("text/html");
+ response.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
+ response.setResponseHeader("Cache-Control","private,no-store,no-cache");
+ ifstream infile(prop.second);
+ if (!infile)
+ throw ConfigurationException("Unable to access $1 HTML template.", params(1,local ? "localLogout" : "globalLogout"));
+ TemplateParameters tp;
+ tp.setPropertySet(application.getPropertySet("Errors"));
+ if (status)
+ tp.m_map["logoutStatus"] = status;
+ stringstream str;
+ XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp);
+ return make_pair(true,response.sendResponse(str));
+ }
+ prop = application.getString("homeURL");
+ if (!prop.first)
+ prop.second = "/";
+ return make_pair(true,response.sendRedirect(prop.second));
+}
+
pair<bool,long> LogoutHandler::run(SPRequest& request, bool isHandler) const
{
// If we're inside a chain, so do nothing.
while (temp.isstring()) {
sessions.push_back(temp.string());
temp = s.next();
- if (notifyBackChannel(*app, sessions))
+ if (notifyBackChannel(*app, in["url"].string(), sessions, in["local"].integer()==1))
ret.integer(1);
}
index = atoi(param);
// Fetch the next front notification URL and bump the index for the next round trip.
- string loc = application.getNotificationURL(request, true, index++);
+ string loc = application.getNotificationURL(request.getRequestURL(), true, index++);
if (loc.empty())
return make_pair(false,0);
return make_pair(true,response.sendRedirect(loc.c_str()));
}
-bool LogoutHandler::notifyBackChannel(const Application& application, const vector<string>& sessions) const
+#ifndef SHIBSP_LITE
+#include "util/SPConstants.h"
+#include <xmltooling/impl/AnyElement.h>
+#include <xmltooling/soap/SOAP.h>
+#include <xmltooling/soap/SOAPClient.h>
+using namespace soap11;
+namespace {
+ static const XMLCh LogoutNotification[] = UNICODE_LITERAL_18(L,o,g,o,u,t,N,o,t,i,f,i,c,a,t,i,o,n);
+ static const XMLCh SessionID[] = UNICODE_LITERAL_9(S,e,s,s,i,o,n,I,D);
+ static const XMLCh _type[] = UNICODE_LITERAL_4(t,y,p,e);
+ static const XMLCh _local[] = UNICODE_LITERAL_5(l,o,c,a,l);
+ static const XMLCh _global[] = UNICODE_LITERAL_6(g,l,o,b,a,l);
+
+ class SHIBSP_DLLLOCAL SOAPNotifier : public soap11::SOAPClient
+ {
+ public:
+ SOAPNotifier() {}
+ virtual ~SOAPNotifier() {}
+ private:
+ void prepareTransport(SOAPTransport& transport) {
+ transport.setVerifyHost(false);
+ }
+ };
+};
+#endif
+
+bool LogoutHandler::notifyBackChannel(
+ const Application& application, const char* requestURL, const vector<string>& sessions, bool local
+ ) const
{
+ unsigned int index = 0;
+ string endpoint = application.getNotificationURL(requestURL, false, index++);
+ if (endpoint.empty())
+ return true;
+
if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
#ifndef SHIBSP_LITE
- return true;
+ auto_ptr<Envelope> env(EnvelopeBuilder::buildEnvelope());
+ Body* body = BodyBuilder::buildBody();
+ env->setBody(body);
+ ElementProxy* msg = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, LogoutNotification);
+ body->getUnknownXMLObjects().push_back(msg);
+ msg->setAttribute(QName(NULL, _type), local ? _local : _global);
+ for (vector<string>::const_iterator s = sessions.begin(); s!=sessions.end(); ++s) {
+ auto_ptr_XMLCh temp(s->c_str());
+ ElementProxy* child = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, SessionID);
+ child->setTextContent(temp.get());
+ msg->getUnknownXMLObjects().push_back(child);
+ }
+
+ bool result = true;
+ SOAPNotifier soaper;
+ while (!endpoint.empty()) {
+ try {
+ soaper.send(*env.get(), application.getId(), endpoint.c_str());
+ delete soaper.receive();
+ }
+ catch (exception& ex) {
+ log4cpp::Category::getInstance(SHIBSP_LOGCAT".Logout").error("error notifying application of logout event: %s", ex.what());
+ result = false;
+ }
+ soaper.reset();
+ endpoint = application.getNotificationURL(requestURL, false, index++);
+ }
+ return result;
#else
return false;
#endif
DDFJanitor jin(in), jout(out);
in.addmember("notify").integer(1);
in.addmember("application_id").string(application.getId());
+ in.addmember("url").string(requestURL);
+ if (local)
+ in.addmember("local").integer(1);
DDF s = in.addmember("sessions").list();
for (vector<string>::const_iterator i = sessions.begin(); i!=sessions.end(); ++i) {
DDF temp = DDF(NULL).string(i->c_str());
out=application.getServiceProvider().getListenerService()->send(in);
return (out.integer() == 1);
}
-
-pair<bool,long> LogoutHandler::sendLogoutPage(const Application& application, HTTPResponse& response, bool local, const char* status) const
-{
- pair<bool,const char*> prop = application.getString(local ? "localLogout" : "globalLogout");
- if (prop.first) {
- response.setContentType("text/html");
- response.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
- response.setResponseHeader("Cache-Control","private,no-store,no-cache");
- ifstream infile(prop.second);
- if (!infile)
- throw ConfigurationException("Unable to access $1 HTML template.", params(1,local ? "localLogout" : "globalLogout"));
- TemplateParameters tp;
- tp.setPropertySet(application.getPropertySet("Errors"));
- if (status)
- tp.m_map["logoutStatus"] = status;
- stringstream str;
- XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp);
- return make_pair(true,response.sendResponse(str));
- }
- prop = application.getString("homeURL");
- if (!prop.first)
- prop.second = "/";
- return make_pair(true,response.sendRedirect(prop.second));
-}
bool worked1 = false,worked2 = false;
if (session_id) {
vector<string> sessions(1,session_id);
- worked1 = notifyBackChannel(application, sessions);
+ worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
try {
cache->remove(session_id, application);
worked2 = true;
// For back-channel requests, or if no front-channel notification is needed...
bool worked1 = false,worked2 = false;
- worked1 = notifyBackChannel(application, sessions);
+ worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
if (session_id) {
// One last session to yoink...
try {
pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
private:
- pair<bool,long> doRequest(const Application& application, Session* session_id, HTTPResponse& httpResponse) const;
+ pair<bool,long> doRequest(const Application& application, const char* requestURL, Session* session_id, HTTPResponse& httpResponse) const;
string m_appId;
#ifndef SHIBSP_LITE
if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
// When out of process, we run natively.
- return doRequest(request.getApplication(), session, request);
+ return doRequest(request.getApplication(), request.getRequestURL(), session, request);
}
else {
// When not out of process, we remote the request.
DDFJanitor jin(in), jout(out);
in.addmember("application_id").string(request.getApplication().getId());
in.addmember("session_id").string(session->getID());
+ in.addmember("url").string(request.getRequestURL());
out=request.getServiceProvider().getListenerService()->send(in);
return unwrap(request, out);
}
// Since we're remoted, the result should either be a throw, which we pass on,
// a false/0 return, which we just return as an empty structure, or a response/redirect,
// which we capture in the facade and send back.
- doRequest(*app, session, *resp.get());
+ doRequest(*app, in["url"].string(), session, *resp.get());
}
else {
m_log.error("no NameID or issuing entityID found in session");
#endif
}
-pair<bool,long> SAML2LogoutInitiator::doRequest(const Application& application, Session* session, HTTPResponse& response) const
+pair<bool,long> SAML2LogoutInitiator::doRequest(
+ const Application& application, const char* requestURL, Session* session, HTTPResponse& response
+ ) const
{
// Do back channel notification.
vector<string> sessions(1, session->getID());
- if (!notifyBackChannel(application, sessions)) {
+ if (!notifyBackChannel(application, requestURL, sessions, false)) {
session->unlock();
application.getServiceProvider().getSessionCache()->remove(sessions.front().c_str(), application);
return sendLogoutPage(application, response, true, "Partial logout failure.");
// Since we can't guarantee uniqueness, check for an existing record.
string record;
time_t recordexp;
- int ver = m_storage->readText("Logout", name, &record, &recordexp);
+ int ver = m_storage->readText("NameID", name, &record, &recordexp);
if (ver > 0) {
// Existing record, so we need to unmarshall it.
istringstream in(record);
// Try and store it back...
if (ver > 0) {
- ver = m_storage->updateText("Logout", name, out.str().c_str(), max(expires, recordexp), ver);
+ ver = m_storage->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
if (ver <= 0) {
// Out of sync, or went missing, so retry.
return insert(key, expires, name, index);
}
}
- else if (!m_storage->createText("Logout", name, out.str().c_str(), expires)) {
+ else if (!m_storage->createText("NameID", name, out.str().c_str(), expires)) {
// Hit a dup, so just retry, hopefully hitting the other branch.
return insert(key, expires, name, index);
}
m_log.debug("creating new session");
+ time_t now = time(NULL);
+ auto_ptr_char index(session_index);
+ auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL);
+ auto_ptr_char name(nameid ? nameid->getName() : NULL);
+
+ if (nameid) {
+ // Check for a pending logout.
+ if (strlen(name.get()) > 255)
+ const_cast<char*>(name.get())[255] = 0;
+ string pending;
+ int ver = m_storage->readText("Logout", name.get(), &pending);
+ if (ver > 0) {
+ DDF pendobj;
+ DDFJanitor jpend(pendobj);
+ istringstream pstr(pending);
+ pstr >> pendobj;
+ // IdP.SP.index contains logout expiration, if any.
+ DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getString("entityID").second];
+ const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
+ if (!logexpstr && session_index) // we tried an exact session match, now try for NULL
+ logexpstr = deadmenwalking["_shibnull"].string();
+ if (logexpstr) {
+ auto_ptr_XMLCh dt(logexpstr);
+ DateTime dtobj(dt.get());
+ dtobj.parseDateTime();
+ time_t logexp = dtobj.getEpoch();
+ if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
+ throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
+ }
+ }
+ }
+
auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());
// Store session properties in DDF.
obj.addmember("version").integer(1);
obj.addmember("application_id").string(application.getId());
- // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
+ // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
#ifndef HAVE_GMTIME_R
struct tm* ptime=gmtime(&expires);
#else
if (client_addr)
obj.addmember("client_addr").string(client_addr);
- if (issuer) {
- auto_ptr_char entity_id(issuer->getEntityID());
+ if (issuer)
obj.addmember("entity_id").string(entity_id.get());
- }
if (protocol) {
auto_ptr_char prot(protocol);
obj.addmember("protocol").string(prot.get());
auto_ptr_char instant(authn_instant);
obj.addmember("authn_instant").string(instant.get());
}
- auto_ptr_char index(session_index);
if (session_index)
obj.addmember("session_index").string(index.get());
if (authncontext_class) {
record << obj;
m_log.debug("storing new session...");
- time_t now = time(NULL);
if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))
throw FatalProfileException("Attempted to create a session with a duplicate key.");
// Store the reverse mapping for logout.
- auto_ptr_char name(nameid ? nameid->getName() : NULL);
try {
- if (name.get())
+ if (nameid)
insert(key.get(), expires, name.get(), index.get());
}
catch (exception& ex) {
") at (ClientAddress: " <<
(client_addr ? client_addr : "none") <<
") with (NameIdentifier: " <<
- (name.get() ? name.get() : "none") <<
+ (nameid ? name.get() : "none") <<
")";
if (attributes) {
if (strlen(name.get()) > 255)
const_cast<char*>(name.get())[255] = 0;
- // Read in potentially matching sessions.
+ DDF obj;
+ DDFJanitor jobj(obj);
string record;
- int ver = m_storage->readText("Logout", name.get(), &record);
+ int ver;
+
+ if (expires) {
+ // Record the logout to prevent post-delivered assertions.
+ // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
+#ifndef HAVE_GMTIME_R
+ struct tm* ptime=gmtime(&expires);
+#else
+ struct tm res;
+ struct tm* ptime=gmtime_r(&expires,&res);
+#endif
+ char timebuf[32];
+ strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
+
+ time_t oldexp = 0;
+ ver = m_storage->readText("Logout", name.get(), &record, &oldexp);
+ if (ver > 0) {
+ istringstream lin(record);
+ lin >> obj;
+ }
+ else {
+ obj = DDF(NULL).structure();
+ }
+
+ // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
+ DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getString("entityID").second);
+ if (indexes) {
+ for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
+ root.addmember(x->c_str()).string(timebuf);
+ }
+ else {
+ root.addmember("_shibnull").string(timebuf);
+ }
+
+ // Write it back.
+ ostringstream lout;
+ lout << obj;
+
+ if (ver > 0) {
+ ver = m_storage->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
+ if (ver <= 0) {
+ // Out of sync, or went missing, so retry.
+ return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
+ }
+ }
+ else if (!m_storage->createText("Logout", name.get(), lout.str().c_str(), expires)) {
+ // Hit a dup, so just retry, hopefully hitting the other branch.
+ return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
+ }
+
+ obj.destroy();
+ record.erase();
+ }
+
+ // Read in potentially matching sessions.
+ ver = m_storage->readText("NameID", name.get(), &record);
if (ver == 0) {
m_log.debug("no active sessions to logout for supplied issuer and subject");
return 0;
}
- DDF obj;
- DDFJanitor jobj(obj);
istringstream in(record);
in >> obj;
// If possible, write back the mapping record (this isn't crucial).
try {
if (obj.isnull()) {
- m_storage->deleteText("Logout", name.get());
+ m_storage->deleteText("NameID", name.get());
}
else if (!sessionsKilled.empty()) {
ostringstream out;
out << obj;
- if (m_storage->updateText("Logout", name.get(), out.str().c_str(), 0, ver) <= 0)
+ if (m_storage->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
m_log.warn("logout mapping record changed behind us, leaving it alone");
}
}
return (m_audiences.empty() && m_base) ? m_base->getAudiences() : m_audiences;
}
#endif
- string getNotificationURL(const HTTPRequest& request, bool front, unsigned int index) const;
+ string getNotificationURL(const char* resource, bool front, unsigned int index) const;
const set<string>& getRemoteUserAttributeIds() const {
return (m_remoteUsers.empty() && m_base) ? m_base->getRemoteUserAttributeIds() : m_remoteUsers;
#endif
-string XMLApplication::getNotificationURL(const HTTPRequest& request, bool front, unsigned int index) const
+string XMLApplication::getNotificationURL(const char* resource, bool front, unsigned int index) const
{
const vector<string>& locs = front ? m_frontLogout : m_backLogout;
if (locs.empty())
- return m_base ? m_base->getNotificationURL(request, front, index) : string();
+ return m_base ? m_base->getNotificationURL(resource, front, index) : string();
else if (index >= locs.size())
return string();
- const char* resource = request.getRequestURL();
#ifdef HAVE_STRCASECMP
if (!resource || (strncasecmp(resource,"http://",7) && strncasecmp(resource,"https://",8)))
#else
chLatin_m, chLatin_a, chLatin_p, chNull\r
};\r
\r
+const XMLCh shibspconstants::SHIB2SPNOTIFY_NS[] = // urn:mace:shibboleth:2.0:sp:notify\r
+{ chLatin_u, chLatin_r, chLatin_n, chColon, chLatin_m, chLatin_a, chLatin_c, chLatin_e, chColon,\r
+ chLatin_s, chLatin_h, chLatin_i, chLatin_b, chLatin_b, chLatin_o, chLatin_l, chLatin_e, chLatin_t, chLatin_h, chColon,\r
+ chDigit_2, chPeriod, chDigit_0, chColon, chLatin_s, chLatin_p, chColon,\r
+ chLatin_n, chLatin_o, chLatin_t, chLatin_i, chLatin_f, chLatin_y, chNull\r
+};\r
+\r
const XMLCh shibspconstants::SHIB2ATTRIBUTEFILTER_NS[] = // urn:mace:shibboleth:2.0:afp\r
{ chLatin_u, chLatin_r, chLatin_n, chColon, chLatin_m, chLatin_a, chLatin_c, chLatin_e, chColon,\r
chLatin_s, chLatin_h, chLatin_i, chLatin_b, chLatin_b, chLatin_o, chLatin_l, chLatin_e, chLatin_t, chLatin_h, chColon,\r
/** Shibboleth 2.0 attribute mapping namespace ("urn:mace:shibboleth:2.0:attribute-map") */
extern SHIBSP_API const XMLCh SHIB2ATTRIBUTEMAP_NS[];
+ /** Shibboleth 2.0 notification namespace ("urn:mace:shibboleth:2.0:sp:notify") */
+ extern SHIBSP_API const XMLCh SHIB2SPNOTIFY_NS[];
+
/** Shibboleth 2.0 attribute filter policy namespace ("urn:mace:shibboleth:2.0:afp") */
extern SHIBSP_API const XMLCh SHIB2ATTRIBUTEFILTER_NS[];