From c8bb7d6ef7633f5464b317e67a074506dddbb18b Mon Sep 17 00:00:00 2001 From: cantor Date: Sat, 17 Nov 2007 05:56:45 +0000 Subject: [PATCH] Add authnskew property for ForceAuthn enforcement. git-svn-id: https://svn.middleware.georgetown.edu/cpp-sp/trunk@2628 cb58f699-b61c-0410-a6fe-9272a202ed29 --- adfs/adfs.cpp | 10 +++++++++- schemas/shibboleth-2.0-native-sp-config.xsd | 3 ++- shibsp/handler/impl/SAML1Consumer.cpp | 27 ++++++++++++++++++++++----- shibsp/handler/impl/SAML2Consumer.cpp | 25 ++++++++++++++++--------- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/adfs/adfs.cpp b/adfs/adfs.cpp index c06a203..8a37559 100644 --- a/adfs/adfs.cpp +++ b/adfs/adfs.cpp @@ -571,11 +571,20 @@ string ADFSConsumer::implementProtocol( throw FatalProfileException("Assertion did not contain time conditions."); else if (token->getAuthenticationStatements().empty()) throw FatalProfileException("Assertion did not contain an authentication statement."); + // With ADFS, we only have one token, but we need to put it in a vector. vector tokens(1,token); const saml1::AuthenticationStatement* ssoStatement=token->getAuthenticationStatements().front(); + // authnskew allows rejection of SSO if AuthnInstant is too old. + const PropertySet* sessionProps = application.getPropertySet("Sessions"); + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("authnskew") : pair(false,0); + + if (authnskew.first && authnskew.second && + ssoStatement->getAuthenticationInstant() && (now - ssoStatement->getAuthenticationInstantEpoch() > authnskew.second)) + throw FatalProfileException("The gap between now and the time you logged into your identity provider exceeds the limit."); + // Address checking. saml1::SubjectLocality* locality = ssoStatement->getSubjectLocality(); if (locality && locality->getIPAddress()) { @@ -590,7 +599,6 @@ string ADFSConsumer::implementProtocol( // Now we have to extract the authentication details for attribute and session setup. // Session expiration for ADFS is purely SP-driven, and the method is mapped to a ctx class. - const PropertySet* sessionProps = application.getPropertySet("Sessions"); pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); if (!lifetime.first || lifetime.second == 0) lifetime.second = 28800; diff --git a/schemas/shibboleth-2.0-native-sp-config.xsd b/schemas/shibboleth-2.0-native-sp-config.xsd index 0aeb718..55c3d71 100644 --- a/schemas/shibboleth-2.0-native-sp-config.xsd +++ b/schemas/shibboleth-2.0-native-sp-config.xsd @@ -515,10 +515,11 @@ - + + diff --git a/shibsp/handler/impl/SAML1Consumer.cpp b/shibsp/handler/impl/SAML1Consumer.cpp index 71a53cb..7ad7355 100644 --- a/shibsp/handler/impl/SAML1Consumer.cpp +++ b/shibsp/handler/impl/SAML1Consumer.cpp @@ -152,6 +152,13 @@ string SAML1Consumer::implementProtocol( // With this flag on, we ignore any unsigned assertions. pair flag = settings->getBool("signedAssertions"); + // authnskew allows rejection of SSO if AuthnInstant is too old. + const PropertySet* sessionProps = application.getPropertySet("Sessions"); + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("authnskew") : pair(false,0); + + // Saves off error messages potentially helpful for users. + string contextualError; + for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { // Skip unsigned assertion? if (!(*a)->getSignature() && flag.first && flag.second) { @@ -188,8 +195,16 @@ string SAML1Consumer::implementProtocol( tokens.push_back(*a); // Save off the first valid SSO statement. - if (!ssoStatement && !(*a)->getAuthenticationStatements().empty()) - ssoStatement = (*a)->getAuthenticationStatements().front(); + const vector& statements = const_cast(*a)->getAuthenticationStatements(); + for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { + if (authnskew.first && authnskew.second && + (*s)->getAuthenticationInstant() && (now - (*s)->getAuthenticationInstantEpoch() > authnskew.second)) + contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit."; + else if (!ssoStatement) { + ssoStatement = *s; + break; + } + } } catch (exception& ex) { m_log.warn("detected a problem with assertion: %s", ex.what()); @@ -197,8 +212,11 @@ string SAML1Consumer::implementProtocol( } } - if (!ssoStatement) - throw FatalProfileException("A valid authentication statement was not found in the incoming message."); + if (!ssoStatement) { + if (contextualError.empty()) + throw FatalProfileException("A valid authentication statement was not found in the incoming message."); + throw FatalProfileException(contextualError.c_str()); + } // Address checking. SubjectLocality* locality = ssoStatement->getSubjectLocality(); @@ -214,7 +232,6 @@ string SAML1Consumer::implementProtocol( // Now we have to extract the authentication details for attribute and session setup. // Session expiration for SAML 1.x is purely SP-driven, and the method is mapped to a ctx class. - const PropertySet* sessionProps = application.getPropertySet("Sessions"); pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); if (!lifetime.first || lifetime.second == 0) lifetime.second = 28800; diff --git a/shibsp/handler/impl/SAML2Consumer.cpp b/shibsp/handler/impl/SAML2Consumer.cpp index 83f8ea9..bde48ed 100644 --- a/shibsp/handler/impl/SAML2Consumer.cpp +++ b/shibsp/handler/impl/SAML2Consumer.cpp @@ -138,8 +138,12 @@ string SAML2Consumer::implementProtocol( // With this flag on, we ignore any unsigned assertions. pair flag = settings->getBool("signedAssertions"); - // Saves off IP-mismatch error message because it's potentially helpful for users. - string addressMismatch; + // authnskew allows rejection of SSO if AuthnInstant is too old. + const PropertySet* sessionProps = application.getPropertySet("Sessions"); + pair authnskew = sessionProps ? sessionProps->getUnsignedInt("authnskew") : pair(false,0); + + // Saves off error messages potentially helpful for users. + string contextualError; for (vector::const_iterator a = assertions.begin(); a!=assertions.end(); ++a) { // Skip unsigned assertion? @@ -179,7 +183,7 @@ string SAML2Consumer::implementProtocol( catch (exception& ex) { // We save off the message if there's no SSO statement yet. if (!ssoStatement) - addressMismatch = ex.what(); + contextualError = ex.what(); throw; } @@ -189,7 +193,9 @@ string SAML2Consumer::implementProtocol( // Save off the first valid SSO statement, but favor the "soonest" session expiration. const vector& statements = const_cast(*a)->getAuthnStatements(); for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { - if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) + if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second)) + contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit."; + else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) ssoStatement = *s; } @@ -269,7 +275,7 @@ string SAML2Consumer::implementProtocol( catch (exception& ex) { // We save off the message if there's no SSO statement yet. if (!ssoStatement) - addressMismatch = ex.what(); + contextualError = ex.what(); throw; } @@ -279,7 +285,9 @@ string SAML2Consumer::implementProtocol( // Save off the first valid SSO statement, but favor the "soonest" session expiration. const vector& statements = const_cast(decrypted)->getAuthnStatements(); for (vector::const_iterator s = statements.begin(); s!=statements.end(); ++s) { - if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) + if (authnskew.first && authnskew.second && (*s)->getAuthnInstant() && (now - (*s)->getAuthnInstantEpoch() > authnskew.second)) + contextualError = "The gap between now and the time you logged into your identity provider exceeds the limit."; + else if (!ssoStatement || (*s)->getSessionNotOnOrAfterEpoch() < ssoStatement->getSessionNotOnOrAfterEpoch()) ssoStatement = *s; } @@ -295,9 +303,9 @@ string SAML2Consumer::implementProtocol( if (!ssoStatement) { for_each(ownedtokens.begin(), ownedtokens.end(), xmltooling::cleanup()); - if (addressMismatch.empty()) + if (contextualError.empty()) throw FatalProfileException("A valid authentication statement was not found in the incoming message."); - throw FatalProfileException(addressMismatch.c_str()); + throw FatalProfileException(contextualError.c_str()); } // May need to decrypt NameID. @@ -337,7 +345,6 @@ string SAML2Consumer::implementProtocol( // Session expiration for SAML 2.0 is jointly IdP- and SP-driven. time_t sessionExp = ssoStatement->getSessionNotOnOrAfter() ? ssoStatement->getSessionNotOnOrAfterEpoch() : 0; - const PropertySet* sessionProps = application.getPropertySet("Sessions"); pair lifetime = sessionProps ? sessionProps->getUnsignedInt("lifetime") : pair(true,28800); if (!lifetime.first || lifetime.second == 0) lifetime.second = 28800; -- 2.1.4