[java-identity-provider] 07/11: IDP-2039 - Add audit logging to login flows

Scott Cantor cantor.2 at osu.edu
Tue Jan 3 22:01:47 UTC 2023


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=cc440445daaa1a16ca848527e6b95d8e889696ec

commit cc440445daaa1a16ca848527e6b95d8e889696ec
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Dec 7 09:43:17 2022 -0500

    IDP-2039 - Add audit logging to login flows
    
    https://shibboleth.atlassian.net/browse/IDP-2039
    
    Add auditing to Duo flow and add a default message mapping.
---
 .../net/shibboleth/idp/authn/AuthnAuditFields.java | 21 +++++++++++
 .../idp/authn/duo/impl/ValidateDuoAuthAPI.java     | 32 ++++++++++++++++-
 .../idp/authn/duo/impl/ValidateDuoWebResponse.java | 39 +++++++++++++++-----
 .../shibboleth/idp/flows/authn/duo-authn-beans.xml | 42 ++++++++++++++++++++--
 4 files changed, 123 insertions(+), 11 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthnAuditFields.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthnAuditFields.java
index 097e18709..79a543a8f 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthnAuditFields.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AuthnAuditFields.java
@@ -73,6 +73,27 @@ public final class AuthnAuditFields {
      */
     @Nonnull @NotEmpty public static final String X509_ISSUER = "X509I";
 
+    /**
+     * Duo client/integration key/ID.
+     * 
+     * @since 4.3.0
+     */
+    @Nonnull @NotEmpty public static final String DUO_CLIENT_ID = "DuoCID";
+
+    /**
+     * Duo device ID.
+     * 
+     * @since 4.3.0
+     */
+    @Nonnull @NotEmpty public static final String DUO_DEVICE_ID = "DuoDID";
+
+    /**
+     * Duo factor.
+     * 
+     * @since 4.3.0
+     */
+    @Nonnull @NotEmpty public static final String DUO_FACTOR = "DuoF";
+
     /** Constructor. */
     private AuthnAuditFields() {
 
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java
index f233858b7..edc9f6869 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java
@@ -18,6 +18,8 @@
 package net.shibboleth.idp.authn.duo.impl;
 
 import java.security.Principal;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -33,6 +35,7 @@ import org.slf4j.LoggerFactory;
 import com.duosecurity.duoweb.DuoWebException;
 
 import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthnAuditFields;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
@@ -40,6 +43,8 @@ import net.shibboleth.idp.authn.duo.DuoAuthAPI;
 import net.shibboleth.idp.authn.duo.DuoIntegration;
 import net.shibboleth.idp.authn.duo.DuoPrincipal;
 import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
+import net.shibboleth.idp.authn.impl.AbstractAuditingValidationAction;
+import net.shibboleth.idp.profile.IdPAuditFields;
 import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -70,7 +75,7 @@ import net.shibboleth.shared.logic.FunctionSupport;
  *       {@link AbstractValidationAction#handleError(ProfileRequestContext, AuthenticationContext, String, String)}
  *       method is called.
  */
-public class ValidateDuoAuthAPI extends AbstractValidationAction {
+public class ValidateDuoAuthAPI extends AbstractAuditingValidationAction {
 
     /** Default prefix for metrics. */
     @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.duo";
@@ -319,4 +324,29 @@ public class ValidateDuoAuthAPI extends AbstractValidationAction {
         profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(username);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Map<String,String> getAuditFields(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final Map<String,String> fields = new HashMap<>();
+        
+        if (username != null) {
+            fields.put(IdPAuditFields.USERNAME, username);
+        }
+        
+        if (duoIntegration != null) {
+            fields.put(AuthnAuditFields.DUO_CLIENT_ID, duoIntegration.getIntegrationKey());
+        }
+        
+        if (duoContext != null) {
+            if (duoContext.getDeviceID() != null) {
+                fields.put(AuthnAuditFields.DUO_DEVICE_ID, duoContext.getDeviceID());
+            }
+            if (duoContext.getFactor() != null) {
+                fields.put(AuthnAuditFields.DUO_FACTOR, duoContext.getFactor());
+            }
+        }
+        
+        return Map.copyOf(fields);
+    }
+    
 }
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
index e8d013833..adf961c1e 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -36,12 +38,14 @@ import org.slf4j.LoggerFactory;
 import com.duosecurity.duoweb.DuoWebException;
 
 import jakarta.servlet.ServletRequest;
-import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthnAuditFields;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
 import net.shibboleth.idp.authn.duo.DuoIntegration;
 import net.shibboleth.idp.authn.duo.DuoPrincipal;
+import net.shibboleth.idp.authn.impl.AbstractAuditingValidationAction;
+import net.shibboleth.idp.profile.IdPAuditFields;
 import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
@@ -53,7 +57,7 @@ import net.shibboleth.shared.logic.FunctionSupport;
  * 
  * <p>The username to cross-check comes from a lookup strategy, by default a {@link CanonicalUsernameLookupStrategy}
  * that returns a username produced by an earlier authentication flow, and on success the same name is populated into
- * a {@link SubjectCanonicalizationContext} as a pre-established result for the login flow.
+ * a {@link SubjectCanonicalizationContext} as a pre-established result for the login flow.</p>
  *  
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
@@ -63,7 +67,7 @@ import net.shibboleth.shared.logic.FunctionSupport;
  * 
  * @since 3.3.0
  */
-public class ValidateDuoWebResponse extends AbstractValidationAction {
+public class ValidateDuoWebResponse extends AbstractAuditingValidationAction {
 
     /** Signed response parameter name. */
     @Nonnull @NotEmpty public static final String RESPONSE_PARAM = "sig_response";
@@ -147,21 +151,24 @@ public class ValidateDuoWebResponse extends AbstractValidationAction {
         username = usernameLookupStrategy.apply(profileRequestContext);
         if (username == null) {
             log.warn("{} No principal name available to cross-check Duo result", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            handleError(profileRequestContext, authenticationContext, AuthnEventIds.NO_CREDENTIALS,
+                    AuthnEventIds.NO_CREDENTIALS);
             return false;
         }
 
         final ServletRequest servletRequest = getHttpServletRequest();
         if (servletRequest == null) {
             log.error("{} No ServletRequest available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            handleError(profileRequestContext, authenticationContext, AuthnEventIds.NO_CREDENTIALS,
+                    AuthnEventIds.NO_CREDENTIALS);
             return false;
         }
         
         signedResponse = servletRequest.getParameter(RESPONSE_PARAM);
         if (signedResponse == null || signedResponse.isEmpty()) {
             log.warn("{} No signed Duo response in the request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            handleError(profileRequestContext, authenticationContext, AuthnEventIds.NO_CREDENTIALS,
+                    AuthnEventIds.NO_CREDENTIALS);
             recordFailure(profileRequestContext);
             return false;
         }
@@ -216,7 +223,23 @@ public class ValidateDuoWebResponse extends AbstractValidationAction {
         // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
         profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(username);
     }
-    
-    
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Map<String,String> getAuditFields(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (username != null) {
+            if (duoIntegration != null) {
+                return Map.of(AuthnAuditFields.DUO_CLIENT_ID, duoIntegration.getIntegrationKey(),
+                        IdPAuditFields.USERNAME, username);
+            } else {
+                return Collections.singletonMap(IdPAuditFields.USERNAME, username);
+            }
+        } else if (duoIntegration != null) {
+            return Collections.singletonMap(AuthnAuditFields.DUO_CLIENT_ID, duoIntegration.getIntegrationKey());
+        }
+        
+        return super.getAuditFields(profileRequestContext);
+    }
     
 }
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/duo-authn-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/duo-authn-beans.xml
index a1bc2ecde..b103877e8 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/duo-authn-beans.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/duo-authn-beans.xml
@@ -44,6 +44,22 @@
                 <value>Your two-factor account is disabled.</value>
             </list>
         </entry>
+        <entry key="RequestUnsupported">
+            <list>
+                <value>RequestUnsupported</value>
+            </list>
+        </entry>
+        <entry key="NoCredentials">
+            <list>
+                <value>NoCredentials</value>
+                <value>Invalid request parameters (no capable device)</value>
+            </list>
+        </entry>
+        <entry key="InvalidCredentials">
+            <list>
+                <value>InvalidCredentials</value>
+            </list>
+        </entry>
     </util:map>
 
     <!-- Can override one or more of the beans above. -->
@@ -57,7 +73,9 @@
         p:usernameLookupStrategy-ref="shibboleth.authn.Duo.UsernameLookupStrategy"
         p:duoIntegrationLookupStrategy-ref="shibboleth.authn.Duo.DuoIntegrationStrategy"
         p:addDefaultPrincipals="#{getObject('shibboleth.authn.Duo.addDefaultPrincipals') ?: %{idp.authn.Duo.addDefaultPrincipals:true}}"
-        p:resultCachingPredicate="#{getObject('shibboleth.authn.Duo.resultCachingPredicate')}" />
+        p:resultCachingPredicate="#{getObject('shibboleth.authn.Duo.resultCachingPredicate')}"
+        p:populateAuditContextAction="#{%{idp.authn.Duo.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('shibboleth.authn.Duo.PopulateAuditContext') : null}"
+        p:writeAuditLogAction="#{%{idp.authn.Duo.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('WriteAuditLog') : null}" />
 
     <bean id="ExtractDuoAuthenticationFromHeaders" scope="prototype"
         class="net.shibboleth.idp.authn.duo.impl.ExtractDuoAuthenticationFromHeaders"
@@ -89,6 +107,26 @@
         p:preauthAuthenticator-ref="DuoPreauthAuthenticator"
         p:authAuthenticator-ref="DuoAuthAuthenticator"
         p:classifiedMessages="#{getObject('shibboleth.authn.Duo.ClassifiedMessageMap')}"
-        p:resultCachingPredicate="#{getObject('shibboleth.authn.Duo.resultCachingPredicate')}" />
+        p:resultCachingPredicate="#{getObject('shibboleth.authn.Duo.resultCachingPredicate')}"
+        p:populateAuditContextAction="#{%{idp.authn.Duo.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('shibboleth.authn.Duo.PopulateAuditContext') : null}"
+        p:writeAuditLogAction="#{%{idp.authn.Duo.audit.enabled:%{idp.authn.audit.enabled:false}} ? getObject('WriteAuditLog') : null}" />
+
+    <!-- Audit logging beans. -->
+
+    <util:map id="shibboleth.authn.AuditFormattingMap">
+        <entry key="#{'%{idp.authn.Duo.audit.category:Shibboleth-Audit.Duo}'.trim()}"
+            value="#{'%{idp.authn.Duo.audit.format:%a|%T|%SP|%s|%AF|%u|%AR|%DuoCID|%DuoF|%DuoDID|%UA}'.trim()}" />
+    </util:map>
+
+    <bean id="shibboleth.authn.Duo.DefaultAuditExtractors" parent="shibboleth.authn.DefaultAuditExtractors" lazy-init="true"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+             <map merge="true" />
+        </property>
+    </bean>
+    
+    <bean id="shibboleth.authn.Duo.PopulateAuditContext" parent="shibboleth.authn.AbstractPopulateAuditContext" lazy-init="true"
+        p:fieldExtractors="#{getObject('shibboleth.authn.Duo.AuditExtractors') ?: getObject('shibboleth.authn.Duo.DefaultAuditExtractors')}"
+        p:clearAuditContext="true" />
 
 </beans>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list