[java-idp-plugin-duo] branch main updated: JDUO-61 - Log transactional information ahead of redirect to Duo

Phil Smart philip.smart at jisc.ac.uk
Tue Nov 15 10:03:52 UTC 2022


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

philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=5928a6cd2cd2449b43c9d3a21bb060a285430339

The following commit(s) were added to refs/heads/main by this push:
     new 5928a6c  JDUO-61 - Log transactional information ahead of redirect to Duo
5928a6c is described below

commit 5928a6cd2cd2449b43c9d3a21bb060a285430339
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Nov 15 10:03:44 2022 +0000

    JDUO-61 - Log transactional information ahead of redirect to Duo
    
     - Add INFO level logging of username and state to controller
     - Add pluggable audit logging to the Duo2FA request
    
    https://shibboleth.atlassian.net/browse/JDUO-61
---
 .../idp/plugin/authn/duo/DuoOIDCAuthAPI.java       | 15 ++++
 .../authn/duo/audit/AbstractDuoAuditExtractor.java | 68 +++++++++++++++
 .../AbstractDuoTokenClaimsAuditExtractor.java      | 96 ++++++++++++++++++++++
 .../idp/plugin/authn/duo/audit/AuditFields.java    | 62 ++++++++++++++
 idp-duo-api/src/test/resources/logback-test.xml    |  7 +-
 .../audit/impl/DuoAuthnDeviceIdAuditExtractor.java | 63 ++++++++++++++
 .../impl/DuoAuthnDeviceNameAuditExtractor.java     | 63 ++++++++++++++
 .../duo/audit/impl/DuoFactorAuditExtractor.java    | 56 +++++++++++++
 .../duo/audit/impl/DuoReasonAuditExtractor.java    | 56 +++++++++++++
 .../audit/impl/DuoRequestStateAuditExtractor.java  | 32 ++++++++
 .../audit/impl/DuoResponseStateAuditExtractor.java | 34 ++++++++
 .../audit/impl/DuoTransactionIdAuditExtractor.java | 56 +++++++++++++
 .../duo/audit/impl/DuoUsernameAuditExtractor.java  | 30 +++++++
 .../duo/audit/impl/FixedStringAuditExtractor.java  | 49 +++++++++++
 .../authn/duo/impl/DuoOIDCAuthnController.java     | 22 ++---
 .../idp/plugin/authn/duo/impl/DuoSupport.java      |  2 +-
 .../duo/impl/PopulateDuoAuthenticationContext.java | 10 ++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     | 89 ++++++++++++++++++++
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   | 33 ++++++++
 .../flows/authn/DuoOIDC/duo-oidc-authn-flow.xml    |  8 +-
 .../authn/duo/impl/DuoOIDCAuthnControllerTest.java |  5 +-
 .../duo/nimbus/conf/authn/duo-oidc.properties      |  5 +-
 .../authn/duo/sdk/conf/authn/duo-oidc.properties   |  5 +-
 23 files changed, 844 insertions(+), 22 deletions(-)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
index 00e82c6..7fc335e 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
@@ -55,6 +55,21 @@ public final class DuoOIDCAuthAPI {
     /** The name of the JSON authentication context object.*/
     @Nonnull @NotEmpty public static final String DUO_AUTH_CONTEXT_JSON_OBJECT = "auth_context";
     
+    /** The name of the JSON authentication device object.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_DEVICE_JSON_OBJECT = "auth_device";
+    
+    /** The name of the JSON transaction id object.*/
+    @Nonnull @NotEmpty public static final String DUO_TXID_JSON_OBJECT = "txid";
+    
+    /** The name of the JSON authenticating device friendly name object.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_DEVICE_NAME_JSON_OBJECT = "name";
+    
+    /** The name of the JSON authenticating device key object.*/
+    @Nonnull @NotEmpty public static final String DUO_AUTH_DEVICE_KEY_JSON_OBJECT = "key";
+    
+    /** The name of the JSON reason object.*/
+    @Nonnull @NotEmpty public static final String DUO_REASON_JSON_OBJECT = "reason";
+    
     /** The name of the JSON authentication factor object.*/
     @Nonnull @NotEmpty public static final String DUO_AUTH_FACTOR_JSON_OBJECT = "factor";
     
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoAuditExtractor.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoAuditExtractor.java
new file mode 100644
index 0000000..495ae7d
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoAuditExtractor.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+
+/** 
+ * An abstract audit extractor function to extract values directly from the Duo context.
+ * 
+ * @param <T> the claim type to extract
+ */
+ at ThreadSafe
+public abstract class AbstractDuoAuditExtractor<T> implements Function<ProfileRequestContext, T> {
+    
+    
+    @Override
+    @Nullable public T apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        
+        if (profileRequestContext == null) {
+            return null;
+        }
+        final AuthenticationContext authnContext = 
+                profileRequestContext.getSubcontext(AuthenticationContext.class);
+        if (authnContext == null) {
+            return null;
+        }
+        final DuoOIDCAuthenticationContext duoContext = authnContext.getSubcontext(DuoOIDCAuthenticationContext.class);
+        if (duoContext == null) {
+            return null;
+        }        
+        return doLookup(duoContext);
+    }
+    
+    /**
+     * Implemented to perform the actual lookup.
+     * 
+     * @param context the Duo OIDC context
+     * 
+     * @return lookup value or {@code null} if not found
+     */
+    @Nullable protected abstract T doLookup(@Nonnull final DuoOIDCAuthenticationContext context);
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoTokenClaimsAuditExtractor.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoTokenClaimsAuditExtractor.java
new file mode 100644
index 0000000..3f8eae4
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AbstractDuoTokenClaimsAuditExtractor.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit;
+
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+
+/** 
+ * An extension of the {@link AbstractDuoAuditExtractor} that allows extraction of claims inside the 
+ * Duo authentication token.
+ * 
+ * @param <T> the claim type to extract
+ */
+ at ThreadSafe
+public abstract class AbstractDuoTokenClaimsAuditExtractor<T> extends AbstractDuoAuditExtractor<T> {
+    
+    
+    @Override
+    @Nullable public T apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        
+        if (profileRequestContext == null) {
+            return null;
+        }
+        final AuthenticationContext authnContext = 
+                profileRequestContext.getSubcontext(AuthenticationContext.class);
+        if (authnContext == null) {
+            return null;
+        }
+        final DuoOIDCAuthenticationContext duoContext = authnContext.getSubcontext(DuoOIDCAuthenticationContext.class);
+        if (duoContext == null || duoContext.getAuthToken() == null) {
+            return null;
+        }
+        JWTClaimsSet claimsSet = null;
+        try {
+            claimsSet = duoContext.getAuthToken().getJWTClaimsSet();
+            if (claimsSet == null) {
+                return null;
+            }
+        } catch (final ParseException e) {
+            return null;
+        }
+        return doLookup(claimsSet, duoContext);
+    }
+    
+    @Override
+    @Nullable protected T doLookup(@Nonnull final DuoOIDCAuthenticationContext duoContext) {
+        JWTClaimsSet claimsSet = null;
+        try {
+            claimsSet = duoContext.getAuthToken().getJWTClaimsSet();
+            if (claimsSet == null) {
+                return null;
+            }
+        } catch (final ParseException e) {
+            return null;
+        }
+        return doLookup(claimsSet, duoContext);
+    }
+    
+    /**
+     * Implemented to perform the actual lookup.
+     * 
+     * @param claimsSet the claims set of the Duo authentication token
+     * @param context the Duo OIDC context
+     * 
+     * @return lookup value or {@code null} if not found
+     */
+    @Nullable protected abstract T doLookup(
+            final @Nonnull JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context);
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AuditFields.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AuditFields.java
new file mode 100644
index 0000000..37b295b
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/AuditFields.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.profile.context.AuditContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/** Constants to use for Duo audit logging fields stored in an {@link AuditContext}. */
+public final class AuditFields {
+    
+    /**
+     * Constructor.
+     */
+    private AuditFields() {
+        // no op
+    }
+    
+    /** Duo Transaction log discriminator. */
+    @Nonnull @NotEmpty public static final String DISCRIMINATOR = "DuoDISC";
+    
+    /** Duo Transaction ID. */
+    @Nonnull @NotEmpty public static final String TXID = "DuoTXID";
+    
+    /** Duo Device ID (auth_device:key). */
+    @Nonnull @NotEmpty public static final String DEVICE_ID = "DuoDID";
+    
+    /** Duo Device friendly name (auth_device:name). */
+    @Nonnull @NotEmpty public static final String DEVICE_NAME = "DuoDN";
+    
+    /** Duo reason. */
+    @Nonnull @NotEmpty public static final String REASON = "DuoR";
+    
+    /** Duo factor used for 2FA. */
+    @Nonnull @NotEmpty public static final String FACTOR = "DuoF";
+    
+    /** Duo username used for 2FA. */
+    @Nonnull @NotEmpty public static final String DUO_USER = "DuoU";
+    
+    /** The response state value received from the Duo 2FA authz endpoint. */
+    @Nonnull @NotEmpty public static final String RESP_STATE = "DuoRespS";
+    
+    /** The request state value sent to the Duo 2FA authz endpoint. */
+    @Nonnull @NotEmpty public static final String REQ_STATE = "DuoReqS";
+
+}
diff --git a/idp-duo-api/src/test/resources/logback-test.xml b/idp-duo-api/src/test/resources/logback-test.xml
index 6ddfb8a..608430e 100644
--- a/idp-duo-api/src/test/resources/logback-test.xml
+++ b/idp-duo-api/src/test/resources/logback-test.xml
@@ -14,13 +14,10 @@
         <appender-ref ref="STDOUT" />
     </logger>
     
-     <logger name="net.shibboleth.idp.plugin.authn.duo" level="TRACE" additivity="false">
+     <logger name="net.shibboleth.idp.plugin" level="TRACE" additivity="false">
         <appender-ref ref="STDOUT" />
     </logger>
-    <logger name="net.shibboleth.idp.ext.authn.duo.impl.Saml2FlowTest" level="DEBUG" additivity="false">
-        <appender-ref ref="STDOUT" />
-    </logger>
-    
+
     <logger name="org.springframework.webflow" level="TRACE" additivity="false">
         <appender-ref ref="STDOUT" />
     </logger>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceIdAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceIdAuditExtractor.java
new file mode 100644
index 0000000..5310c54
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceIdAuditExtractor.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoTokenClaimsAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo 2FA device ID (Key) from the auth_device inside the Duo token response.*/
+public class DuoAuthnDeviceIdAuditExtractor extends AbstractDuoTokenClaimsAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context) {
+        String value = null;
+        try {
+            final Map<String, Object> authnContextClaimObj = 
+                    claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_CONTEXT_JSON_OBJECT);
+            
+            if (authnContextClaimObj != null) {      
+                
+                final Object authDeviceObj = 
+                        authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_JSON_OBJECT);
+                if (authDeviceObj instanceof Map) {
+                    final Map<?, ?> mapValue = (Map<?, ?>)authDeviceObj;
+                    
+                    final Object valueObj = 
+                            mapValue.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_KEY_JSON_OBJECT);
+                    if (valueObj instanceof String) {
+                        value = (String)valueObj;
+                    }
+                }   
+            }
+        } catch (final ParseException e) {
+            // Do nothing, just return null
+        }
+        return value;
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceNameAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceNameAuditExtractor.java
new file mode 100644
index 0000000..683b444
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoAuthnDeviceNameAuditExtractor.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoTokenClaimsAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo 2FA device name from the auth_device in the Duo token response.*/
+public class DuoAuthnDeviceNameAuditExtractor extends AbstractDuoTokenClaimsAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context) {
+        String value = null;
+        try {
+            final Map<String, Object> authnContextClaimObj = 
+                    claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_CONTEXT_JSON_OBJECT);
+            
+            if (authnContextClaimObj != null) {      
+                
+                final Object authDeviceObj = 
+                        authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_JSON_OBJECT);
+                if (authDeviceObj instanceof Map) {
+                    final Map<?, ?> mapValue = (Map<?, ?>)authDeviceObj;
+                    
+                    final Object valueObj = 
+                            mapValue.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_NAME_JSON_OBJECT);
+                    if (valueObj instanceof String) {
+                        value = (String)valueObj;
+                    }
+                } 
+            }
+        } catch (final ParseException e) {
+            // Do nothing, just return null
+        }
+        return value;
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoFactorAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoFactorAuditExtractor.java
new file mode 100644
index 0000000..fe4a035
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoFactorAuditExtractor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoTokenClaimsAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo authentication factor from the Duo token response.*/
+public class DuoFactorAuditExtractor extends AbstractDuoTokenClaimsAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context) {
+        String value = null;
+        try {
+            final Map<String, Object> authnContextClaimObj = 
+                    claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_CONTEXT_JSON_OBJECT);
+            
+            if (authnContextClaimObj != null) {         
+                final Object valueObj = 
+                        authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_AUTH_FACTOR_JSON_OBJECT);
+                if (valueObj instanceof String) {
+                    value = (String)valueObj;
+                }
+            }
+        } catch (final ParseException e) {
+            // Do nothing, just return null
+        }
+        return value;
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoReasonAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoReasonAuditExtractor.java
new file mode 100644
index 0000000..3795478
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoReasonAuditExtractor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoTokenClaimsAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo authentication reason from the Duo token response.*/
+public class DuoReasonAuditExtractor extends AbstractDuoTokenClaimsAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context) {
+        String value = null;
+        try {
+            final Map<String, Object> authnContextClaimObj = 
+                    claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_CONTEXT_JSON_OBJECT);
+            
+            if (authnContextClaimObj != null) {         
+                final Object valueObj = 
+                        authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_REASON_JSON_OBJECT);
+                if (valueObj instanceof String) {
+                    value = (String)valueObj;
+                }
+            }
+        } catch (final ParseException e) {
+            // Do nothing, just return null
+        }
+        return value;
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoRequestStateAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoRequestStateAuditExtractor.java
new file mode 100644
index 0000000..a7c5df1
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoRequestStateAuditExtractor.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo request state OAuth 2.0 parameter from the Duo context.*/
+public class DuoRequestStateAuditExtractor extends AbstractDuoAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(@Nonnull final DuoOIDCAuthenticationContext context) {       
+        return context.getRequestState();
+    }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoResponseStateAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoResponseStateAuditExtractor.java
new file mode 100644
index 0000000..0fed05c
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoResponseStateAuditExtractor.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo response state OAuth 2.0 parameter from the Duo context.*/
+public class DuoResponseStateAuditExtractor extends AbstractDuoAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(@Nonnull final DuoOIDCAuthenticationContext context) {       
+        return context.getResponseState();
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoTransactionIdAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoTransactionIdAuditExtractor.java
new file mode 100644
index 0000000..9d70085
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoTransactionIdAuditExtractor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoTokenClaimsAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo transaction ID from the Duo token response.*/
+public class DuoTransactionIdAuditExtractor extends AbstractDuoTokenClaimsAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(
+            @Nonnull final JWTClaimsSet claimsSet, @Nonnull final DuoOIDCAuthenticationContext context) {
+        String txid = null;
+        try {
+            final Map<String, Object> authnContextClaimObj = 
+                    claimsSet.getJSONObjectClaim(DuoOIDCAuthAPI.DUO_AUTH_CONTEXT_JSON_OBJECT);
+            
+            if (authnContextClaimObj != null) {         
+                final Object txidObj = 
+                        authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_TXID_JSON_OBJECT);
+                if (txidObj instanceof String) {
+                    txid = (String)txidObj;
+                }
+            }
+        } catch (final ParseException e) {
+            // Do nothing, just return null
+        }
+        return txid;
+    }
+
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoUsernameAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoUsernameAuditExtractor.java
new file mode 100644
index 0000000..97fe80f
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/DuoUsernameAuditExtractor.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import net.shibboleth.idp.plugin.authn.duo.audit.AbstractDuoAuditExtractor;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/** An audit extractor to extract the Duo username from the Duo context.*/
+public class DuoUsernameAuditExtractor extends AbstractDuoAuditExtractor<String> {
+
+    @Override
+    protected String doLookup(final DuoOIDCAuthenticationContext context) {       
+        return context.getUsername();
+    }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/FixedStringAuditExtractor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/FixedStringAuditExtractor.java
new file mode 100644
index 0000000..d0488da
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/FixedStringAuditExtractor.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** A simple audit extractor that allows a fixed string to be logged.*/
+public class FixedStringAuditExtractor implements Function<ProfileRequestContext, String> {
+    
+    /** The fixed string to log.*/
+    @Nonnull private final String value;
+    
+    /**
+     * Constructor.
+     *
+     * @param stringToLog the fixed string to log
+     */
+    public FixedStringAuditExtractor(@Nonnull @NotEmpty @ParameterName(name = "value") final String stringToLog) {
+        value = Constraint.isNotEmpty(stringToLog, "The fixed string to log can not be empty");
+    }
+
+    @Override
+    public String apply(final ProfileRequestContext input) {
+        return value;
+    }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
index 9f193f0..c27f4af 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
@@ -40,8 +40,8 @@ import net.shibboleth.idp.authn.ExternalAuthenticationException;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shibboleth.idp.plugin.authn.duo.DuoException;
-import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
@@ -133,20 +133,17 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
             
         }
 
-        log.trace("Starting Duo 2FA for integration client '{}' and user '{}'",
-                duoContext.getIntegration() != null ? duoContext.getIntegration().getClientId(): 
-                    "none", duoContext.getUsername());
         try {
             final DuoOIDCClient client = duoContext.getClient();
             if (client == null) {
                 throw new DuoClientException("Duo client is null, has the context been created correctly?");
             }            
-            //generate state, stash in the context for checking on return.
-            //TODO: could use a type of replay cache and storage service?
-            final String nonce = DuoSupport.generateNonce(32);
-            final String state = DuoSupport.generateState(nonce, key);
-            //store only the nonce component as the request state. The key component is only used to resume the flow.
-            duoContext.setRequestState(nonce);
+            
+            final String state = DuoSupport.generateState(duoContext.getRequestState(), key);            
+            
+            log.info("Starting Duo 2FA for client '{}', user '{}', and unique request state '{}'",
+                    duoContext.getIntegration() != null ? duoContext.getIntegration().getClientId(): 
+                        "none", duoContext.getUsername(), duoContext.getRequestState());
             
             String authURL;
             //if an OIDC id_token nonce is supported, add it to the authz request and context
@@ -213,6 +210,11 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         }
         duoContext.setAuthorizationCode(code);
         duoContext.setResponseState(nonce);
+        
+        log.info("Finished Duo 2FA for client '{}', user '{}', and unique request state '{}'",
+                duoContext.getIntegration() != null ? duoContext.getIntegration().getClientId(): 
+                    "none", duoContext.getUsername(), nonce);
+        
         ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
         
     }
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
index f97b0c3..3402970 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -74,7 +74,7 @@ public final class DuoSupport {
      * @return the combined state component.
      */
     @Nonnull static String generateState(@Nonnull final String nonce, @Nonnull final String key) {
-        Constraint.isNotNull(nonce, "NonceHex key can not be null");
+        Constraint.isNotNull(nonce, "Nonce Hex key can not be null");
         Constraint.isNotNull(key, "Webflow execution key can not be null");
         
         final String keyHex = Hex.encodeHexString(key.getBytes());
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
index 9851e75..569a6b1 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
@@ -51,10 +51,12 @@ import net.shibboleth.utilities.java.support.logic.FunctionSupport;
  * An action to create (or lookup) and populate the {@link DuoOIDCAuthenticationContext} 
  * with the username, chosen {@link DuoOIDCIntegration}, and {@link DuoOIDCClient} appropriate for this request. 
  * 
- * <p>Also determines the usable redirect_uri, either from one registered, or computed from the
+ * <p>Determines the usable redirect_uri, either from one registered, or computed from the
  * HTTP request. Is set once, before the client is constructed, for every client. If however, the client supports 
  * dynamic use of the redirect_uri, it is also set into the context for use downstream.</p>
  * 
+ * <p>Adds the nonce part of the state parameter for matching on callback from the 2FA check.</p>
+ * 
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
  * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
@@ -205,6 +207,12 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
         }
         context.setUsername(username);
         
+        // Generate state, stash in the context for checking on return.
+        final String nonce = DuoSupport.generateNonce(32);
+        // Store only the nonce component as the request state. The SWF key is added by the controller
+        // And included in the authorization request to Duo.
+        context.setRequestState(nonce);
+        
         try {
             computeAndStoreRedirectURIIfSupported(duoIntegration, request, context);     
             
diff --git a/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 285d8e0..807d5f6 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -34,5 +34,94 @@
     <!-- Controller implementation -->
     <bean id="shibboleth.DuoOIDCAuthnController"
         class="net.shibboleth.idp.plugin.authn.duo.impl.DuoOIDCAuthnController" />
+        
+       
+    <!-- Default audit format and extractors --> 
+    <util:map id="shibboleth.authn.DuoOIDC.DefaultAuditFormattingMap">
+        <entry key="Shibboleth-Audit" value="%a|%ST|%T|%DuoDISC|%DuoU|%DuoReqS|%DuoRespS|%DuoTXID|%DuoDID|%DuoDN|%DuoR|%DuoF" />
+    </util:map>
+    
+    
+    <bean id="shibboleth.authn.DuoOIDC.DefaultPreDuoPopulateAuditExtractors" lazy-init="true"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+             <map>   
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.DISCRIMINATOR"/>
+                    </key>
+                     <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.FixedStringAuditExtractor"
+                        c:value="Duo2FA"/>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.DUO_USER"/>
+                    </key>
+                     <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoUsernameAuditExtractor"/>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.REQ_STATE"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoRequestStateAuditExtractor"/>
+                </entry>
+             </map>
+        </property>
+    </bean>
+    
+    <!-- Included so the state can be stashed inside the audit context before it is validated and removed -->
+    <bean id="shibboleth.authn.DuoOIDC.DefaultPreStateValidationPopulateAuditExtractors" lazy-init="true"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+             <map>   
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.RESP_STATE"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoResponseStateAuditExtractor"/>
+                </entry>
+             </map>
+        </property>
+    </bean>
+       
+        
+    <bean id="shibboleth.authn.DuoOIDC.DefaultPostDuoPopulateAuditExtractors" lazy-init="true"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map>               
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.TXID"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoTransactionIdAuditExtractor"/>
+                </entry> 
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.DEVICE_ID"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoAuthnDeviceIdAuditExtractor"/>
+                </entry> 
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.DEVICE_NAME"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoAuthnDeviceNameAuditExtractor"/>
+                </entry> 
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.REASON"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoReasonAuditExtractor"/>
+                </entry> 
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.plugin.authn.duo.audit.AuditFields.FACTOR"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.plugin.authn.duo.audit.impl.DuoFactorAuditExtractor"/>
+                </entry> 
+                                  
+            </map>
+        </property>
+    </bean>
 
 </beans>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index 102b10f..fc970c2 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -123,6 +123,10 @@
         class="net.shibboleth.idp.plugin.authn.duo.impl.HealthCheckDuoOIDCAuthAPI"
         p:activationCondition="%{idp.duo.oidc.healthcheck.enabled:true}">
     </bean>
+    
+    <bean id="PreDuoPopulateAuditContext" parent="shibboleth.authn.DuoOIDC.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.authn.DuoOIDC.PreDuoPopulateAuditExtractors') ?: getObject('shibboleth.authn.DuoOIDC.DefaultPreDuoPopulateAuditExtractors')}" />
+    
         
     <bean id="ValidateExternalAuthenticationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateExternalAuthenticationContext" />
@@ -231,6 +235,21 @@
         
     <bean id="shibboleth.authn.DuoOIDC.DefaultCleanupHook" 
         class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationResult.DuoOIDCCleanupHook" />
+        
+    
+    <bean id="PreStateValidationPopulateAuditContext" parent="shibboleth.authn.DuoOIDC.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.authn.DuoOIDC.PreStateValidationPopulateAuditExtractors') ?: getObject('shibboleth.authn.DuoOIDC.DefaultPreStateValidationPopulateAuditExtractors')}" />
+    
+        
+    <bean id="PostDuoPopulateAuditContext" parent="shibboleth.authn.DuoOIDC.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.authn.DuoOIDC.PostDuoPopulateAuditExtractors') ?: getObject('shibboleth.authn.DuoOIDC.DefaultPostDuoPopulateAuditExtractors')}" />
+    
+    <bean id="WriteAuditLog" class="net.shibboleth.idp.profile.audit.impl.WriteAuditLog" scope="prototype"
+        p:formattingMap="#{getObject('shibboleth.authn.DuoOIDC.AuditFormattingMap') ?: getObject('shibboleth.authn.DuoOIDC.DefaultAuditFormattingMap')}"
+        p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+        p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest" 
+        p:activationCondition="%{idp.duo.oidc.writeaudit:false}"/>
 
     <bean id="ValidateDuoTokenAuthenticationResult" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationResult"        
@@ -241,4 +260,18 @@
         p:addDefaultPrincipals="#{getObject('shibboleth.authn.DuoOIDC.ContextToPrincipalMappingStrategy') == null}"/>
 
 
+    <!-- Audit System Setup Specific to Duo -->
+    
+    <bean id="shibboleth.authn.DuoOIDC.AbstractPopulateAuditContext" abstract="true"
+        class="net.shibboleth.idp.profile.audit.impl.PopulateAuditContext" scope="prototype"
+        p:formattingMapParser-ref="shibboleth.authn.DuoOIDC.AuditFormattingMapParser"
+        p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+        p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+        p:fieldReplacements="#{getObject('shibboleth.AuditFieldReplacementMap')}" />
+        
+    <bean id="shibboleth.authn.DuoOIDC.AuditFormattingMapParser" scope="prototype"
+        class="net.shibboleth.idp.profile.audit.impl.PopulateAuditContext.FormattingMapParser"
+        c:_0="#{getObject('shibboleth.authn.DuoOIDC.AuditFormattingMap') ?: getObject('shibboleth.authn.DuoOIDC.DefaultAuditFormattingMap')}" />
+
+    
 </beans>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
index ca568ce..d83170c 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
@@ -26,6 +26,8 @@
     <action-state id="CheckDuoOIDCAuthAPI">
         <evaluate expression="PopulateDuoAuthenticationContext" />
         <evaluate expression="HealthCheckDuoOIDCAuthAPI" />
+        <evaluate expression="PreDuoPopulateAuditContext" />
+        <evaluate expression="WriteAuditLog"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="Duo2FAAuthorizationRequest" />
     </action-state>
@@ -44,6 +46,8 @@
     <!-- match the response state to the request state, fail if error  -->
     <action-state id="ValidateDuoResponse">
         <evaluate expression="ValidateExternalAuthenticationContext"/> 
+        <!-- Populate the audit context with the state before it is internall removed -->
+        <evaluate expression="PreStateValidationPopulateAuditContext" />
         <evaluate expression="ValidateDuoResponseState"/>  
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ExchangeCodeForDuoToken" />
@@ -54,8 +58,10 @@
         <!-- validate the token signature and claims -->
         <evaluate expression="ValidateTokenSignature"/>
         <evaluate expression="ValidateTokenClaims"/>
+        <evaluate expression="PostDuoPopulateAuditContext" />
+        <evaluate expression="WriteAuditLog" /> 
         <!-- final validation of the response status to build an authn result -->
-        <evaluate expression="ValidateDuoTokenAuthenticationResult"/>       
+        <evaluate expression="ValidateDuoTokenAuthenticationResult"/>        
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="proceed" />
     </action-state>    
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
index 669df02..43ebad8 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
@@ -85,7 +85,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
     @Nonnull private final String KEY = "e1s1";
     
     /** Duo OIDC nonce component of the state parameter.*/
-    @Nonnull private String NONCE = "8821febf7b3237fc3974f76a75f37e8c";
+    @Nonnull private final String NONCE = "8821febf7b3237fc3974f76a75f37e8c";
     
     /** Duo OIDC state parameter.*/
     @NonnullAfterInit private String state;
@@ -284,7 +284,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
         final FlowExecutionRepository mockFlowExecutionRepo = Mockito.mock(FlowExecutionRepository.class);
         final FlowExecution mockFlowExecution = Mockito.mock(FlowExecution.class);
 
-        final MutableAttributeMap<Object> map = new LocalAttributeMap<Object>();
+        final MutableAttributeMap<Object> map = new LocalAttributeMap<>();
         map.put(ProfileRequestContext.BINDING_KEY, buildProfileRequestContext());
 
         Mockito.when(mockFlowExecutor.getExecutionRepository()).thenReturn(mockFlowExecutionRepo);
@@ -322,6 +322,7 @@ public class DuoOIDCAuthnControllerTest extends AbstractTestNGSpringContextTests
         dc.setUsername("jdoe");
         dc.setIntegration(integ);
         dc.setClient(new MockDuoOIDCClient_OK(integ));
+        dc.setRequestState(NONCE);
 
         final AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
         afd.setId("authn/DuoOIDC");
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
index 436aee4..1533dd5 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
@@ -60,4 +60,7 @@ idp.duo.oidc.redirectURL = https://<hostname>:<port>/idp/profile/Authn/Duo/2FA/d
 #idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token 
 #idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
 # Applies only to forced authentication
-#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
+
+## Write audit entries before the Duo redirect and after response validation
+#idp.duo.oidc.writeaudit = false
\ No newline at end of file
diff --git a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
index f0abb74..6d787d7 100644
--- a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
+++ b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
@@ -48,4 +48,7 @@ idp.duo.oidc.redirectURL = https://<hostname>:<port>/idp/profile/Authn/Duo/2FA/d
 #idp.duo.oidc.jwt.verifier.issuerPath = /oauth/v1/token 
 #idp.duo.oidc.jwt.verifier.preferredUsername = preferred_username
 # Applies only to forced authentication
-#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
\ No newline at end of file
+#idp.duo.oidc.jwt.verifier.authLifetime = PT60S
+
+## Write audit entries before the Duo redirect and after response validation
+#idp.duo.oidc.writeaudit = false
\ No newline at end of file

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


More information about the commits mailing list