1c7af1399f641cc90b0eb30a99d65b717698ea64
[shibboleth/sp.git] / shibsp / handler / impl / SAML2Consumer.cpp
1 /*
2  *  Copyright 2001-2009 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * SAML2Consumer.cpp
19  *
20  * SAML 2.0 assertion consumer service
21  */
22
23 #include "internal.h"
24 #include "handler/AssertionConsumerService.h"
25
26 #ifndef SHIBSP_LITE
27 # include "Application.h"
28 # include "ServiceProvider.h"
29 # include "SessionCache.h"
30 # include "attribute/resolver/ResolutionContext.h"
31 # include <saml/exceptions.h>
32 # include <saml/SAMLConfig.h>
33 # include <saml/binding/SecurityPolicyRule.h>
34 # include <saml/saml2/core/Protocols.h>
35 # include <saml/saml2/metadata/Metadata.h>
36 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
37 # include <saml/saml2/profile/SAML2AssertionPolicy.h>
38 # include <xmltooling/XMLToolingConfig.h>
39 # include <xmltooling/io/HTTPRequest.h>
40 # include <xmltooling/util/DateTime.h>
41 using namespace opensaml::saml2;
42 using namespace opensaml::saml2p;
43 using namespace opensaml::saml2md;
44 using namespace opensaml;
45 # ifndef min
46 #  define min(a,b)            (((a) < (b)) ? (a) : (b))
47 # endif
48 #endif
49
50 using namespace shibsp;
51 using namespace xmltooling;
52 using namespace std;
53
54 namespace shibsp {
55
56 #if defined (_MSC_VER)
57     #pragma warning( push )
58     #pragma warning( disable : 4250 )
59 #endif
60
61     class SHIBSP_DLLLOCAL SAML2Consumer : public AssertionConsumerService
62     {
63     public:
64         SAML2Consumer(const DOMElement* e, const char* appId)
65             : AssertionConsumerService(e, appId, Category::getInstance(SHIBSP_LOGCAT".SSO.SAML2")) {
66 #ifndef SHIBSP_LITE
67             m_ssoRule = NULL;
68             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
69                 m_ssoRule = SAMLConfig::getConfig().SecurityPolicyRuleManager.newPlugin(BEARER_POLICY_RULE, e);
70 #endif
71         }
72         virtual ~SAML2Consumer() {
73 #ifndef SHIBSP_LITE
74             delete m_ssoRule;
75 #endif
76         }
77
78 #ifndef SHIBSP_LITE
79         void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
80             AssertionConsumerService::generateMetadata(role, handlerURL);
81             role.addSupport(samlconstants::SAML20P_NS);
82         }
83
84     private:
85         void implementProtocol(
86             const Application& application,
87             const HTTPRequest& httpRequest,
88             HTTPResponse& httpResponse,
89             SecurityPolicy& policy,
90             const PropertySet* settings,
91             const XMLObject& xmlObject
92             ) const;
93
94         SecurityPolicyRule* m_ssoRule;
95 #endif
96     };
97
98 #if defined (_MSC_VER)
99     #pragma warning( pop )
100 #endif
101
102     Handler* SHIBSP_DLLLOCAL SAML2ConsumerFactory(const pair<const DOMElement*,const char*>& p)
103     {
104         return new SAML2Consumer(p.first, p.second);
105     }
106
107 #ifndef SHIBSP_LITE
108     class SHIBSP_DLLLOCAL _rulenamed : std::unary_function<const SecurityPolicyRule*,bool>
109     {
110     public:
111         _rulenamed(const char* name) : m_name(name) {}
112         bool operator()(const SecurityPolicyRule* rule) const {
113             return rule ? !strcmp(m_name, rule->getType()) : false;
114         }
115     private:
116         const char* m_name;
117     };
118 #endif
119 };
120
121 #ifndef SHIBSP_LITE
122
123 void SAML2Consumer::implementProtocol(
124     const Application& application,
125     const HTTPRequest& httpRequest,
126     HTTPResponse& httpResponse,
127     SecurityPolicy& policy,
128     const PropertySet* settings,
129     const XMLObject& xmlObject
130     ) const
131 {
132     // Implementation of SAML 2.0 SSO profile(s).
133     m_log.debug("processing message against SAML 2.0 SSO profile");
134
135     // Remember whether we already established trust.
136     // None of the SAML 2 bindings require security at the protocol layer.
137     bool alreadySecured = policy.isAuthenticated();
138
139     // Check for errors...this will throw if it's not a successful message.
140     checkError(&xmlObject, policy.getIssuerMetadata());
141
142     const Response* response = dynamic_cast<const Response*>(&xmlObject);
143     if (!response)
144         throw FatalProfileException("Incoming message was not a samlp:Response.");
145
146     const vector<saml2::Assertion*>& assertions = response->getAssertions();
147     const vector<saml2::EncryptedAssertion*>& encassertions = response->getEncryptedAssertions();
148     if (assertions.empty() && encassertions.empty())
149         throw FatalProfileException("Incoming message contained no SAML assertions.");
150
151     // Maintain list of "legit" tokens to feed to SP subsystems.
152     const Subject* ssoSubject=NULL;
153     const AuthnStatement* ssoStatement=NULL;
154     vector<const opensaml::Assertion*> tokens;
155
156     // Also track "bad" tokens that we'll cache but not use.
157     // This is necessary because there may be valid tokens not aimed at us.
158     vector<const opensaml::Assertion*> badtokens;
159
160     // And also track "owned" tokens that we decrypt here.
161     vector<saml2::Assertion*> ownedtokens;
162
163     // With this flag on, we ignore any unsigned assertions.
164     const EntityDescriptor* entity = NULL;
165     pair<bool,bool> flag = make_pair(false,false);
166     if (alreadySecured && policy.getIssuerMetadata()) {
167         entity = dynamic_cast<const EntityDescriptor*>(policy.getIssuerMetadata()->getParent());
168         flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions");
169     }
170
171     // authnskew allows rejection of SSO if AuthnInstant is too old.
172     const PropertySet* sessionProps = application.getPropertySet("Sessions");
173     pair<bool,unsigned int> authnskew = sessionProps ? sessionProps->getUnsignedInt("maxTimeSinceAuthn") : pair<bool,unsigned int>(false,0);
174
175     // Saves off error messages potentially helpful for users.
176     string contextualError;
177
178     // Ensure the Bearer rule is in the policy set.
179     if (find_if(policy.getRules(), _rulenamed(BEARER_POLICY_RULE)) == NULL)
180         policy.getRules().push_back(m_ssoRule);
181
182     // Populate recipient as audience.
183     policy.getAudiences().push_back(application.getRelyingParty(entity)->getXMLString("entityID").second);
184
185     time_t now = time(NULL);
186     for (vector<saml2::Assertion*>::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) {
187         try {
188             // Skip unsigned assertion?
189             if (!(*a)->getSignature() && flag.first && flag.second)
190                 throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy.");
191
192             // We clear the security flag, so we can tell whether the token was secured on its own.
193             policy.setAuthenticated(false);
194             policy.reset(true);
195
196             // Extract message bits and re-verify Issuer information.
197             extractMessageDetails(*(*a), samlconstants::SAML20P_NS, policy);
198
199             // Run the policy over the assertion. Handles replay, freshness, and
200             // signature verification, assuming the relevant rules are configured,
201             // along with condition and profile enforcement.
202             policy.evaluate(*(*a), &httpRequest);
203
204             // If no security is in place now, we kick it.
205             if (!alreadySecured && !policy.isAuthenticated())
206                 throw SecurityPolicyException("Unable to establish security of incoming assertion.");
207
208             // If we hadn't established Issuer yet, redo the signedAssertions check.
209             if (!entity && policy.getIssuerMetadata()) {
210                 entity = dynamic_cast<const EntityDescriptor*>(policy.getIssuerMetadata()->getParent());
211                 flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions");
212                 if (!(*a)->getSignature() && flag.first && flag.second)
213                     throw SecurityPolicyException("The incoming assertion was unsigned, violating local security policy.");
214             }
215
216             // Address checking.
217             SubjectConfirmationData* subcondata = dynamic_cast<SubjectConfirmationData*>(
218                 dynamic_cast<SAML2AssertionPolicy&>(policy).getSubjectConfirmation()->getSubjectConfirmationData()
219                 );
220             if (subcondata && subcondata->getAddress()) {
221                 auto_ptr_char boundip(subcondata->getAddress());
222                 checkAddress(application, httpRequest, boundip.get());
223             }
224
225             // Track it as a valid token.
226             tokens.push_back(*a);
227
228             // Save off the first valid SSO statement, but favor the "soonest" session expiration.
229             const vector<AuthnStatement*>& statements = const_cast<const saml2::Assertion*>(*a)->getAuthnStatements();
230             for (vector<AuthnStatement*>::const_iterator s = statements.begin(); s!=statements.end(); ++s) {
231                 if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second))
232                     contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit.";
233                 else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch())
234                     ssoStatement = *s;
235             }
236
237             // Save off the first valid Subject, but favor an unencrypted NameID over anything else.
238             if (!ssoSubject || (!ssoSubject->getNameID() && (*a)->getSubject()->getNameID()))
239                 ssoSubject = (*a)->getSubject();
240         }
241         catch (exception& ex) {
242             m_log.warn("detected a problem with assertion: %s", ex.what());
243             if (!ssoStatement)
244                 contextualError = ex.what();
245             badtokens.push_back(*a);
246         }
247     }
248
249     // In case we need decryption...
250     CredentialResolver* cr=application.getCredentialResolver();
251     if (!cr && !encassertions.empty())
252         m_log.warn("found encrypted assertions, but no CredentialResolver was available");
253
254     for (vector<saml2::EncryptedAssertion*>::const_iterator ea = encassertions.begin(); cr && ea!=encassertions.end(); ++ea) {
255         // Attempt to decrypt it.
256         saml2::Assertion* decrypted=NULL;
257         try {
258             Locker credlocker(cr);
259             auto_ptr<MetadataCredentialCriteria> mcc(
260                 policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL
261                 );
262             auto_ptr<XMLObject> wrapper((*ea)->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get()));
263             decrypted = dynamic_cast<saml2::Assertion*>(wrapper.get());
264             if (decrypted) {
265                 wrapper.release();
266                 ownedtokens.push_back(decrypted);
267                 if (m_log.isDebugEnabled())
268                     m_log.debugStream() << "decrypted Assertion: " << *decrypted << logging::eol;
269             }
270         }
271         catch (exception& ex) {
272             m_log.error(ex.what());
273         }
274         if (!decrypted)
275             continue;
276
277         try {
278             // We clear the security flag, so we can tell whether the token was secured on its own.
279             policy.setAuthenticated(false);
280             policy.reset(true);
281
282             // Extract message bits and re-verify Issuer information.
283             extractMessageDetails(*decrypted, samlconstants::SAML20P_NS, policy);
284
285             // Run the policy over the assertion. Handles replay, freshness, and
286             // signature verification, assuming the relevant rules are configured,
287             // along with condition and profile enforcement.
288             // We have to marshall the object first to ensure signatures can be checked.
289             if (!decrypted->getDOM())
290                 decrypted->marshall();
291             policy.evaluate(*decrypted, &httpRequest);
292
293             // If no security is in place now, we kick it.
294             if (!alreadySecured && !policy.isAuthenticated())
295                 throw SecurityPolicyException("Unable to establish security of incoming assertion.");
296
297             // If we hadn't established Issuer yet, redo the signedAssertions check.
298             if (!entity && policy.getIssuerMetadata()) {
299                 entity = dynamic_cast<const EntityDescriptor*>(policy.getIssuerMetadata()->getParent());
300                 flag = application.getRelyingParty(entity)->getBool("requireSignedAssertions");
301                 if (!decrypted->getSignature() && flag.first && flag.second)
302                     throw SecurityPolicyException("The decrypted assertion was unsigned, violating local security policy.");
303             }
304
305             // Address checking.
306             SubjectConfirmationData* subcondata = dynamic_cast<SubjectConfirmationData*>(
307                 dynamic_cast<SAML2AssertionPolicy&>(policy).getSubjectConfirmation()->getSubjectConfirmationData()
308                 );
309             if (subcondata && subcondata->getAddress()) {
310                 auto_ptr_char boundip(subcondata->getAddress());
311                 checkAddress(application, httpRequest, boundip.get());
312             }
313
314             // Track it as a valid token.
315             tokens.push_back(decrypted);
316
317             // Save off the first valid SSO statement, but favor the "soonest" session expiration.
318             const vector<AuthnStatement*>& statements = const_cast<const saml2::Assertion*>(decrypted)->getAuthnStatements();
319             for (vector<AuthnStatement*>::const_iterator s = statements.begin(); s!=statements.end(); ++s) {
320                 if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second))
321                     contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit.";
322                 else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch())
323                     ssoStatement = *s;
324             }
325
326             // Save off the first valid Subject, but favor an unencrypted NameID over anything else.
327             if (!ssoSubject || (!ssoSubject->getNameID() && decrypted->getSubject()->getNameID()))
328                 ssoSubject = decrypted->getSubject();
329         }
330         catch (exception& ex) {
331             m_log.warn("detected a problem with assertion: %s", ex.what());
332             if (!ssoStatement)
333                 contextualError = ex.what();
334             badtokens.push_back(decrypted);
335         }
336     }
337
338     if (!ssoStatement) {
339         for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup<saml2::Assertion>());
340         if (contextualError.empty())
341             throw FatalProfileException("A valid authentication statement was not found in the incoming message.");
342         throw FatalProfileException(contextualError.c_str());
343     }
344
345     // May need to decrypt NameID.
346     bool ownedName = false;
347     NameID* ssoName = ssoSubject->getNameID();
348     if (!ssoName) {
349         EncryptedID* encname = ssoSubject->getEncryptedID();
350         if (encname) {
351             if (!cr)
352                 m_log.warn("found encrypted NameID, but no decryption credential was available");
353             else {
354                 Locker credlocker(cr);
355                 auto_ptr<MetadataCredentialCriteria> mcc(
356                     policy.getIssuerMetadata() ? new MetadataCredentialCriteria(*policy.getIssuerMetadata()) : NULL
357                     );
358                 try {
359                     auto_ptr<XMLObject> decryptedID(encname->decrypt(*cr,application.getRelyingParty(entity)->getXMLString("entityID").second,mcc.get()));
360                     ssoName = dynamic_cast<NameID*>(decryptedID.get());
361                     if (ssoName) {
362                         ownedName = true;
363                         decryptedID.release();
364                         if (m_log.isDebugEnabled())
365                             m_log.debugStream() << "decrypted NameID: " << *ssoName << logging::eol;
366                     }
367                 }
368                 catch (exception& ex) {
369                     m_log.error(ex.what());
370                 }
371             }
372         }
373     }
374
375     m_log.debug("SSO profile processing completed successfully");
376
377     // We've successfully "accepted" at least one SSO token, along with any additional valid tokens.
378     // To complete processing, we need to extract and resolve attributes and then create the session.
379
380     // Now we have to extract the authentication details for session setup.
381
382     // Session expiration for SAML 2.0 is jointly IdP- and SP-driven.
383     time_t sessionExp = ssoStatement->getSessionNotOnOrAfter() ?
384         (ssoStatement->getSessionNotOnOrAfterEpoch() + XMLToolingConfig::getConfig().clock_skew_secs) : 0;
385     pair<bool,unsigned int> lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair<bool,unsigned int>(true,28800);
386     if (!lifetime.first || lifetime.second == 0)
387         lifetime.second = 28800;
388     if (sessionExp == 0)
389         sessionExp = now + lifetime.second;     // IdP says nothing, calulate based on SP.
390     else
391         sessionExp = min(sessionExp, now + lifetime.second);    // Use the lowest.
392
393     const AuthnContext* authnContext = ssoStatement->getAuthnContext();
394
395     try {
396         // The context will handle deleting attributes and new tokens.
397         auto_ptr<ResolutionContext> ctx(
398             resolveAttributes(
399                 application,
400                 policy.getIssuerMetadata(),
401                 samlconstants::SAML20P_NS,
402                 NULL,
403                 ssoName,
404                 (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL,
405                 (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL,
406                 &tokens
407                 )
408             );
409
410         if (ctx.get()) {
411             // Copy over any new tokens, but leave them in the context for cleanup.
412             tokens.insert(tokens.end(), ctx->getResolvedAssertions().begin(), ctx->getResolvedAssertions().end());
413         }
414
415         // Now merge in bad tokens for caching.
416         tokens.insert(tokens.end(), badtokens.begin(), badtokens.end());
417
418         application.getServiceProvider().getSessionCache()->insert(
419             application,
420             httpRequest,
421             httpResponse,
422             sessionExp,
423             entity,
424             samlconstants::SAML20P_NS,
425             ssoName,
426             ssoStatement->getAuthnInstant() ? ssoStatement->getAuthnInstant()->getRawData() : NULL,
427             ssoStatement->getSessionIndex(),
428             (authnContext && authnContext->getAuthnContextClassRef()) ? authnContext->getAuthnContextClassRef()->getReference() : NULL,
429             (authnContext && authnContext->getAuthnContextDeclRef()) ? authnContext->getAuthnContextDeclRef()->getReference() : NULL,
430             &tokens,
431             ctx.get() ? &ctx->getResolvedAttributes() : NULL
432             );
433
434         if (ownedName)
435             delete ssoName;
436         for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup<saml2::Assertion>());
437     }
438     catch (exception&) {
439         if (ownedName)
440             delete ssoName;
441         for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup<saml2::Assertion>());
442         throw;
443     }
444 }
445
446 #endif