X-Git-Url: http://www.project-moonshot.org/gitweb/?a=blobdiff_plain;f=shibsp%2Fattribute%2Fresolver%2Fimpl%2FQueryAttributeResolver.cpp;h=85a1d881526c1f550e9fdcd935ce0b2525c79a1e;hb=a0af8ed86ab481af4605daf5cf837fdaa4544bdd;hp=e796c53ad539c916c12b6106b19aa3a50f719c4b;hpb=486ea6f0c638815c3a7556cbb12156e2f4b1943b;p=shibboleth%2Fsp.git diff --git a/shibsp/attribute/resolver/impl/QueryAttributeResolver.cpp b/shibsp/attribute/resolver/impl/QueryAttributeResolver.cpp index e796c53..85a1d88 100644 --- a/shibsp/attribute/resolver/impl/QueryAttributeResolver.cpp +++ b/shibsp/attribute/resolver/impl/QueryAttributeResolver.cpp @@ -1,6 +1,6 @@ /* - * Copyright 2001-2007 Internet2 - * + * Copyright 2001-2010 Internet2 + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,7 +16,7 @@ /** * QueryAttributeResolver.cpp - * + * * AttributeResolver based on SAML queries. */ @@ -24,7 +24,7 @@ #include "Application.h" #include "ServiceProvider.h" #include "SessionCache.h" -#include "attribute/Attribute.h" +#include "attribute/SimpleAttribute.h" #include "attribute/filtering/AttributeFilter.h" #include "attribute/filtering/BasicFilteringContext.h" #include "attribute/resolver/AttributeExtractor.h" @@ -32,20 +32,22 @@ #include "attribute/resolver/ResolutionContext.h" #include "binding/SOAPClient.h" #include "metadata/MetadataProviderCriteria.h" +#include "security/SecurityPolicy.h" +#include "security/SecurityPolicyProvider.h" #include "util/SPConstants.h" #include -#include #include #include #include -#include #include #include #include +#include #include -#include +#include #include +#include #include #include @@ -65,22 +67,21 @@ namespace shibsp { { public: QueryContext(const Application& application, const Session& session) - : m_query(true), m_app(application), m_session(&session), m_metadata(NULL), m_entity(NULL), m_nameid(NULL) { + : m_query(true), m_app(application), m_session(&session), m_metadata(nullptr), m_entity(nullptr), m_nameid(nullptr) { m_protocol = XMLString::transcode(session.getProtocol()); m_class = XMLString::transcode(session.getAuthnContextClassRef()); m_decl = XMLString::transcode(session.getAuthnContextDeclRef()); } - + QueryContext( const Application& application, const EntityDescriptor* issuer, const XMLCh* protocol, - const NameID* nameid=NULL, - const XMLCh* authncontext_class=NULL, - const XMLCh* authncontext_decl=NULL, - const vector* tokens=NULL, - const vector* attributes=NULL - ) : m_query(true), m_app(application), m_session(NULL), m_metadata(NULL), m_entity(issuer), + const NameID* nameid=nullptr, + const XMLCh* authncontext_class=nullptr, + const XMLCh* authncontext_decl=nullptr, + const vector* tokens=nullptr + ) : m_query(true), m_app(application), m_session(nullptr), m_metadata(nullptr), m_entity(issuer), m_protocol(protocol), m_nameid(nameid), m_class(authncontext_class), m_decl(authncontext_decl) { if (tokens) { @@ -98,7 +99,7 @@ namespace shibsp { } } } - + ~QueryContext() { if (m_session) { XMLString::release((XMLCh**)&m_protocol); @@ -110,7 +111,7 @@ namespace shibsp { for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup()); for_each(m_assertions.begin(), m_assertions.end(), xmltooling::cleanup()); } - + bool doQuery() const { return m_query; } @@ -128,7 +129,7 @@ namespace shibsp { return m_entity = m_metadata->getEntityDescriptor(MetadataProviderCriteria(m_app, m_session->getEntityID())).first; } } - return NULL; + return nullptr; } const XMLCh* getProtocol() const { return m_protocol; @@ -165,7 +166,7 @@ namespace shibsp { vector m_attributes; vector m_assertions; }; - + class SHIBSP_DLLLOCAL QueryResolver : public AttributeResolver { public: @@ -177,18 +178,18 @@ namespace shibsp { Lockable* lock() {return this;} void unlock() {} - + ResolutionContext* createResolutionContext( const Application& application, const EntityDescriptor* issuer, const XMLCh* protocol, - const NameID* nameid=NULL, - const XMLCh* authncontext_class=NULL, - const XMLCh* authncontext_decl=NULL, - const vector* tokens=NULL, - const vector* attributes=NULL + const NameID* nameid=nullptr, + const XMLCh* authncontext_class=nullptr, + const XMLCh* authncontext_decl=nullptr, + const vector* tokens=nullptr, + const vector* attributes=nullptr ) const { - return new QueryContext(application,issuer,protocol,nameid,authncontext_class,authncontext_decl,tokens,attributes); + return new QueryContext(application,issuer,protocol,nameid,authncontext_class,authncontext_decl,tokens); } ResolutionContext* createResolutionContext(const Application& application, const Session& session) const { @@ -202,31 +203,40 @@ namespace shibsp { } private: - bool SAML1Query(QueryContext& ctx) const; - bool SAML2Query(QueryContext& ctx) const; + void SAML1Query(QueryContext& ctx) const; + void SAML2Query(QueryContext& ctx) const; Category& m_log; + string m_policyId; + bool m_subjectMatch; vector m_SAML1Designators; vector m_SAML2Designators; + vector m_exceptionId; }; AttributeResolver* SHIBSP_DLLLOCAL QueryResolverFactory(const DOMElement* const & e) { return new QueryResolver(e); } - + + static const XMLCh exceptionId[] = UNICODE_LITERAL_11(e,x,c,e,p,t,i,o,n,I,d); + static const XMLCh policyId[] = UNICODE_LITERAL_8(p,o,l,i,c,y,I,d); + static const XMLCh subjectMatch[] = UNICODE_LITERAL_12(s,u,b,j,e,c,t,M,a,t,c,h); }; -QueryResolver::QueryResolver(const DOMElement* e) : m_log(Category::getInstance(SHIBSP_LOGCAT".AttributeResolver")) +QueryResolver::QueryResolver(const DOMElement* e) + : m_log(Category::getInstance(SHIBSP_LOGCAT".AttributeResolver.Query")), + m_policyId(XMLHelper::getAttrString(e, nullptr, policyId)), + m_subjectMatch(XMLHelper::getAttrBool(e, false, subjectMatch)) { #ifdef _DEBUG xmltooling::NDC ndc("QueryResolver"); #endif - + DOMElement* child = XMLHelper::getFirstChildElement(e); while (child) { try { - if (XMLHelper::isNodeNamed(e, samlconstants::SAML20_NS, saml2::Attribute::LOCAL_NAME)) { + if (XMLHelper::isNodeNamed(child, samlconstants::SAML20_NS, saml2::Attribute::LOCAL_NAME)) { auto_ptr obj(saml2::AttributeBuilder::buildOneFromElement(child)); saml2::Attribute* down = dynamic_cast(obj.get()); if (down) { @@ -234,7 +244,7 @@ QueryResolver::QueryResolver(const DOMElement* e) : m_log(Category::getInstance( obj.release(); } } - else if (XMLHelper::isNodeNamed(e, samlconstants::SAML1P_NS, AttributeDesignator::LOCAL_NAME)) { + else if (XMLHelper::isNodeNamed(child, samlconstants::SAML1_NS, AttributeDesignator::LOCAL_NAME)) { auto_ptr obj(AttributeDesignatorBuilder::buildOneFromElement(child)); AttributeDesignator* down = dynamic_cast(obj.get()); if (down) { @@ -248,9 +258,13 @@ QueryResolver::QueryResolver(const DOMElement* e) : m_log(Category::getInstance( } child = XMLHelper::getNextSiblingElement(child); } + + string exid(XMLHelper::getAttrString(e, nullptr, exceptionId)); + if (!exid.empty()) + m_exceptionId.push_back(exid); } -bool QueryResolver::SAML1Query(QueryContext& ctx) const +void QueryResolver::SAML1Query(QueryContext& ctx) const { #ifdef _DEBUG xmltooling::NDC ndc("query"); @@ -261,20 +275,28 @@ bool QueryResolver::SAML1Query(QueryContext& ctx) const find_if(ctx.getEntityDescriptor()->getAttributeAuthorityDescriptors(), isValidForProtocol(ctx.getProtocol())); if (!AA) { m_log.warn("no SAML 1.%d AttributeAuthority role found in metadata", version); - return false; + return; } const Application& application = ctx.getApplication(); const PropertySet* relyingParty = application.getRelyingParty(ctx.getEntityDescriptor()); - shibsp::SecurityPolicy policy(application); + + // Locate policy key. + const char* policyId = m_policyId.empty() ? application.getString("policyId").second : m_policyId.c_str(); + + // Set up policy and SOAP client. + auto_ptr policy( + application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, nullptr, policyId) + ); + policy->getAudiences().push_back(relyingParty->getXMLString("entityID").second); MetadataCredentialCriteria mcc(*AA); - shibsp::SOAPClient soaper(policy); + shibsp::SOAPClient soaper(*policy.get()); auto_ptr_XMLCh binding(samlconstants::SAML1_BINDING_SOAP); - saml1p::Response* response=NULL; + saml1p::Response* response=nullptr; const vector& endpoints=AA->getAttributeServices(); for (vector::const_iterator ep=endpoints.begin(); !response && ep!=endpoints.end(); ++ep) { - if (!XMLString::equals((*ep)->getBinding(),binding.get())) + if (!XMLString::equals((*ep)->getBinding(),binding.get()) || !(*ep)->getLocation()) continue; auto_ptr_char loc((*ep)->getLocation()); try { @@ -305,23 +327,24 @@ bool QueryResolver::SAML1Query(QueryContext& ctx) const if (!response) { m_log.error("unable to obtain a SAML response from attribute authority"); - return false; + throw BindingException("Unable to obtain a SAML response from attribute authority."); } - else if (!response->getStatus() || !response->getStatus()->getStatusCode() || response->getStatus()->getStatusCode()->getValue()==NULL || + else if (!response->getStatus() || !response->getStatus()->getStatusCode() || response->getStatus()->getStatusCode()->getValue()==nullptr || *(response->getStatus()->getStatusCode()->getValue()) != saml1p::StatusCode::SUCCESS) { delete response; m_log.error("attribute authority returned a SAML error"); - return true; + throw FatalProfileException("Attribute authority returned a SAML error."); } const vector& assertions = const_cast(response)->getAssertions(); if (assertions.empty()) { delete response; m_log.warn("response from attribute authority was empty"); - return true; + return; } - else if (assertions.size()>1) + else if (assertions.size()>1) { m_log.warn("simple resolver only supports one assertion in the query response"); + } auto_ptr wrapper(response); saml1::Assertion* newtoken = assertions.front(); @@ -329,33 +352,29 @@ bool QueryResolver::SAML1Query(QueryContext& ctx) const pair signedAssertions = relyingParty->getBool("requireSignedAssertions"); if (!newtoken->getSignature() && signedAssertions.first && signedAssertions.second) { m_log.error("assertion unsigned, rejecting it based on signedAssertions policy"); - return true; + throw SecurityPolicyException("Rejected unsigned assertion based on local policy."); } try { // We're going to insist that the assertion issuer is the same as the peer. // Reset the policy's message bits and extract them from the assertion. - policy.reset(true); - policy.setMessageID(newtoken->getAssertionID()); - policy.setIssueInstant(newtoken->getIssueInstantEpoch()); - policy.setIssuer(newtoken->getIssuer()); - policy.evaluate(*newtoken); + policy->reset(true); + policy->setMessageID(newtoken->getAssertionID()); + policy->setIssueInstant(newtoken->getIssueInstantEpoch()); + policy->setIssuer(newtoken->getIssuer()); + policy->evaluate(*newtoken); // Now we can check the security status of the policy. - if (!policy.isAuthenticated()) + if (!policy->isAuthenticated()) throw SecurityPolicyException("Security of SAML 1.x query result not established."); - - // Lastly, check it over. - saml1::AssertionValidator tokval(relyingParty->getXMLString("entityID").second, application.getAudiences(), time(NULL)); - tokval.validateAssertion(*newtoken); } catch (exception& ex) { - m_log.error("assertion failed policy/validation: %s", ex.what()); - return true; + m_log.error("assertion failed policy validation: %s", ex.what()); + throw; } newtoken->detach(); - wrapper.release(); + wrapper.release(); // detach blows away the Response ctx.getResolvedAssertions().push_back(newtoken); // Finally, extract and filter the result. @@ -363,7 +382,24 @@ bool QueryResolver::SAML1Query(QueryContext& ctx) const AttributeExtractor* extractor = application.getAttributeExtractor(); if (extractor) { Locker extlocker(extractor); - extractor->extractAttributes(application, AA, *newtoken, ctx.getResolvedAttributes()); + const vector& statements = const_cast(newtoken)->getAttributeStatements(); + for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { + if (m_subjectMatch) { + // Check for subject match. + const NameIdentifier* respName = (*s)->getSubject() ? (*s)->getSubject()->getNameIdentifier() : nullptr; + if (!respName || !XMLString::equals(respName->getName(), ctx.getNameID()->getName()) || + !XMLString::equals(respName->getFormat(), ctx.getNameID()->getFormat()) || + !XMLString::equals(respName->getNameQualifier(), ctx.getNameID()->getNameQualifier())) { + if (respName) + m_log.warnStream() << "ignoring AttributeStatement without strongly matching NameIdentifier in Subject: " << + *respName << logging::eol; + else + m_log.warn("ignoring AttributeStatement without NameIdentifier in Subject"); + continue; + } + } + extractor->extractAttributes(application, AA, *(*s), ctx.getResolvedAttributes()); + } } AttributeFilter* filter = application.getAttributeFilter(); @@ -377,12 +413,11 @@ bool QueryResolver::SAML1Query(QueryContext& ctx) const m_log.error("caught exception extracting/filtering attributes from query result: %s", ex.what()); for_each(ctx.getResolvedAttributes().begin(), ctx.getResolvedAttributes().end(), xmltooling::cleanup()); ctx.getResolvedAttributes().clear(); + throw; } - - return true; } -bool QueryResolver::SAML2Query(QueryContext& ctx) const +void QueryResolver::SAML2Query(QueryContext& ctx) const { #ifdef _DEBUG xmltooling::NDC ndc("query"); @@ -392,23 +427,30 @@ bool QueryResolver::SAML2Query(QueryContext& ctx) const find_if(ctx.getEntityDescriptor()->getAttributeAuthorityDescriptors(), isValidForProtocol(samlconstants::SAML20P_NS)); if (!AA) { m_log.warn("no SAML 2 AttributeAuthority role found in metadata"); - return false; + return; } const Application& application = ctx.getApplication(); - shibsp::SecurityPolicy policy(application); - MetadataCredentialCriteria mcc(*AA); - shibsp::SOAPClient soaper(policy); - const PropertySet* relyingParty = application.getRelyingParty(ctx.getEntityDescriptor()); pair signedAssertions = relyingParty->getBool("requireSignedAssertions"); pair encryption = relyingParty->getString("encryption"); + // Locate policy key. + const char* policyId = m_policyId.empty() ? application.getString("policyId").second : m_policyId.c_str(); + + // Set up policy and SOAP client. + auto_ptr policy( + application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, nullptr, policyId) + ); + policy->getAudiences().push_back(relyingParty->getXMLString("entityID").second); + MetadataCredentialCriteria mcc(*AA); + shibsp::SOAPClient soaper(*policy.get()); + auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP); - saml2p::StatusResponseType* srt=NULL; + saml2p::StatusResponseType* srt=nullptr; const vector& endpoints=AA->getAttributeServices(); for (vector::const_iterator ep=endpoints.begin(); !srt && ep!=endpoints.end(); ++ep) { - if (!XMLString::equals((*ep)->getBinding(),binding.get())) + if (!XMLString::equals((*ep)->getBinding(),binding.get()) || !(*ep)->getLocation()) continue; auto_ptr_char loc((*ep)->getLocation()); try { @@ -417,7 +459,6 @@ bool QueryResolver::SAML2Query(QueryContext& ctx) const // Encrypt the NameID? if (encryption.first && (!strcmp(encryption.second, "true") || !strcmp(encryption.second, "back"))) { auto_ptr encrypted(EncryptedIDBuilder::buildEncryptedID()); - MetadataCredentialCriteria mcc(*AA); encrypted->encrypt( *ctx.getNameID(), *(application.getMetadataProvider()), @@ -451,62 +492,138 @@ bool QueryResolver::SAML2Query(QueryContext& ctx) const if (!srt) { m_log.error("unable to obtain a SAML response from attribute authority"); - return false; + throw BindingException("Unable to obtain a SAML response from attribute authority."); } + + auto_ptr wrapper(srt); + saml2p::Response* response = dynamic_cast(srt); if (!response) { - delete srt; m_log.error("message was not a samlp:Response"); - return true; + throw FatalProfileException("Attribute authority returned an unrecognized message."); } else if (!response->getStatus() || !response->getStatus()->getStatusCode() || !XMLString::equals(response->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) { - delete srt; m_log.error("attribute authority returned a SAML error"); - return true; + throw FatalProfileException("Attribute authority returned a SAML error."); } + saml2::Assertion* newtoken = nullptr; const vector& assertions = const_cast(response)->getAssertions(); if (assertions.empty()) { - delete srt; - m_log.warn("response from attribute authority was empty"); - return true; - } - else if (assertions.size()>1) - m_log.warn("simple resolver only supports one assertion in the query response"); + // Check for encryption. + const vector& encassertions = const_cast(response)->getEncryptedAssertions(); + if (encassertions.empty()) { + m_log.warn("response from attribute authority was empty"); + return; + } + else if (encassertions.size() > 1) { + m_log.warn("simple resolver only supports one assertion in the query response"); + } - auto_ptr wrapper(srt); - saml2::Assertion* newtoken = assertions.front(); + CredentialResolver* cr=application.getCredentialResolver(); + if (!cr) { + m_log.warn("found encrypted assertion, but no CredentialResolver was available"); + throw FatalProfileException("Assertion was encrypted, but no decryption credentials are available."); + } + + // Attempt to decrypt it. + try { + Locker credlocker(cr); + auto_ptr tokenwrapper(encassertions.front()->decrypt(*cr, relyingParty->getXMLString("entityID").second, &mcc)); + newtoken = dynamic_cast(tokenwrapper.get()); + if (newtoken) { + tokenwrapper.release(); + if (m_log.isDebugEnabled()) + m_log.debugStream() << "decrypted Assertion: " << *newtoken << logging::eol; + // Free the Response now, so we know this is a stand-alone token later. + delete wrapper.release(); + } + } + catch (exception& ex) { + m_log.error(ex.what()); + throw; + } + } + else { + if (assertions.size() > 1) + m_log.warn("simple resolver only supports one assertion in the query response"); + newtoken = assertions.front(); + } if (!newtoken->getSignature() && signedAssertions.first && signedAssertions.second) { m_log.error("assertion unsigned, rejecting it based on signedAssertions policy"); - return true; + if (!wrapper.get()) + delete newtoken; + throw SecurityPolicyException("Rejected unsigned assertion based on local policy."); } try { // We're going to insist that the assertion issuer is the same as the peer. // Reset the policy's message bits and extract them from the assertion. - policy.reset(true); - policy.setMessageID(newtoken->getID()); - policy.setIssueInstant(newtoken->getIssueInstantEpoch()); - policy.setIssuer(newtoken->getIssuer()); - policy.evaluate(*newtoken); + policy->reset(true); + policy->setMessageID(newtoken->getID()); + policy->setIssueInstant(newtoken->getIssueInstantEpoch()); + policy->setIssuer(newtoken->getIssuer()); + policy->evaluate(*newtoken); // Now we can check the security status of the policy. - if (!policy.isAuthenticated()) + if (!policy->isAuthenticated()) throw SecurityPolicyException("Security of SAML 2.0 query result not established."); - // Lastly, check it over. - saml2::AssertionValidator tokval(relyingParty->getXMLString("entityID").second, application.getAudiences(), time(NULL)); - tokval.validateAssertion(*newtoken); + if (m_subjectMatch) { + // Check for subject match. + bool ownedName = false; + NameID* respName = newtoken->getSubject() ? newtoken->getSubject()->getNameID() : nullptr; + if (!respName) { + // Check for encryption. + EncryptedID* encname = newtoken->getSubject() ? newtoken->getSubject()->getEncryptedID() : nullptr; + if (encname) { + CredentialResolver* cr=application.getCredentialResolver(); + if (!cr) + m_log.warn("found EncryptedID, but no CredentialResolver was available"); + else { + Locker credlocker(cr); + auto_ptr decryptedID(encname->decrypt(*cr, relyingParty->getXMLString("entityID").second, &mcc)); + respName = dynamic_cast(decryptedID.get()); + if (respName) { + ownedName = true; + decryptedID.release(); + if (m_log.isDebugEnabled()) + m_log.debugStream() << "decrypted NameID: " << *respName << logging::eol; + } + } + } + } + + auto_ptr nameIDwrapper(ownedName ? respName : nullptr); + + if (!respName || !XMLString::equals(respName->getName(), ctx.getNameID()->getName()) || + !XMLString::equals(respName->getFormat(), ctx.getNameID()->getFormat()) || + !XMLString::equals(respName->getNameQualifier(), ctx.getNameID()->getNameQualifier()) || + !XMLString::equals(respName->getSPNameQualifier(), ctx.getNameID()->getSPNameQualifier())) { + if (respName) + m_log.warnStream() << "ignoring Assertion without strongly matching NameID in Subject: " << + *respName << logging::eol; + else + m_log.warn("ignoring Assertion without NameID in Subject"); + if (!wrapper.get()) + delete newtoken; + return; + } + } } catch (exception& ex) { - m_log.error("assertion failed policy/validation: %s", ex.what()); - return true; + m_log.error("assertion failed policy validation: %s", ex.what()); + if (!wrapper.get()) + delete newtoken; + throw; } - newtoken->detach(); - wrapper.release(); + if (wrapper.get()) { + newtoken->detach(); + wrapper.release(); // detach blows away the Response + } ctx.getResolvedAssertions().push_back(newtoken); // Finally, extract and filter the result. @@ -528,9 +645,8 @@ bool QueryResolver::SAML2Query(QueryContext& ctx) const m_log.error("caught exception extracting/filtering attributes from query result: %s", ex.what()); for_each(ctx.getResolvedAttributes().begin(), ctx.getResolvedAttributes().end(), xmltooling::cleanup()); ctx.getResolvedAttributes().clear(); + throw; } - - return true; } void QueryResolver::resolveAttributes(ResolutionContext& ctx) const @@ -545,19 +661,31 @@ void QueryResolver::resolveAttributes(ResolutionContext& ctx) const return; } - if (qctx.getNameID() && qctx.getEntityDescriptor()) { - if (XMLString::equals(qctx.getProtocol(), samlconstants::SAML20P_NS)) { - m_log.debug("attempting SAML 2.0 attribute query"); - SAML2Query(qctx); + try { + if (qctx.getNameID() && qctx.getEntityDescriptor()) { + if (XMLString::equals(qctx.getProtocol(), samlconstants::SAML20P_NS)) { + m_log.debug("attempting SAML 2.0 attribute query"); + SAML2Query(qctx); + } + else if (XMLString::equals(qctx.getProtocol(), samlconstants::SAML11_PROTOCOL_ENUM) || + XMLString::equals(qctx.getProtocol(), samlconstants::SAML10_PROTOCOL_ENUM)) { + m_log.debug("attempting SAML 1.x attribute query"); + SAML1Query(qctx); + } + else { + m_log.info("SSO protocol does not allow for attribute query"); + } + } + else { + m_log.warn("can't attempt attribute query, either no NameID or no metadata to use"); } - else if (XMLString::equals(qctx.getProtocol(), samlconstants::SAML11_PROTOCOL_ENUM) || - XMLString::equals(qctx.getProtocol(), samlconstants::SAML10_PROTOCOL_ENUM)) { - m_log.debug("attempting SAML 1.x attribute query"); - SAML1Query(qctx); + } + catch (exception& ex) { + // Already logged. + if (!m_exceptionId.empty()) { + SimpleAttribute* attr = new SimpleAttribute(m_exceptionId); + attr->getValues().push_back(XMLToolingConfig::getConfig().getURLEncoder()->encode(ex.what())); + qctx.getResolvedAttributes().push_back(attr); } - else - m_log.warn("SSO protocol does not allow for attribute query"); } - else - m_log.warn("can't attempt attribute query, either no NameID or no metadata to use"); }