[java-idp-plugin-duo] branch main updated: JDUO-104 - Add authn_time result to AuthResult’s timestamp

Codeberg noreply at shibboleth.net
Thu Aug 27 15:16:57 UTC 2026


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

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

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-duo/commit/441412f4642beb51669aeb5bcb60bd58accbde67

The following commit(s) were added to refs/heads/main by this push:
     new 441412f4 JDUO-104 - Add authn_time result to AuthResult’s timestamp
441412f4 is described below

commit 441412f4642beb51669aeb5bcb60bd58accbde67
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Aug 27 16:16:48 2026 +0100

    JDUO-104 - Add authn_time result to AuthResult’s timestamp
    
     - Reset the authentication instant to one in the result if present in
    the Duo Token.
    
    https://shibboleth.atlassian.net/browse/JDUO-104
---
 .../impl/ValidateDuoTokenAuthenticationResult.java | 36 +++++++++++++++++++---
 .../ValidateDuoTokenAuthenticationResultTest.java  | 28 +++++++++++++++++
 2 files changed, 60 insertions(+), 4 deletions(-)

diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
index 4890178c..91ba244a 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
@@ -16,7 +16,9 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
 
 import java.security.Principal;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.Collection;
+import java.util.Date;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -45,6 +47,7 @@ import net.shibboleth.idp.plugin.authn.duo.DuoException;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
 import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.oidc.security.jwt.claims.impl.IDTokenClaims;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -220,7 +223,7 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
                             AuthnEventIds.INVALID_CREDENTIALS);
                     recordFailure(profileRequestContext);
                     return;
-                }
+                }              
                 
                 log.info("{} Duo 2FA authentication succeeded for '{}', using second-factor '{}'",
                         getLogPrefix(),duoContext.getUsername(), factorUsed != null ? factorUsed : "unspecified");
@@ -228,6 +231,15 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
                 //the cleanup hook which removes the Duo context and prevents useful operation
                 //of the contextToPrincipalMappingStrategy.
                 buildAuthenticationResult(profileRequestContext, authenticationContext);
+                
+                // Reset the authentication time to the one in the result
+                final Instant authTime = extractAuthenticationTime();
+                if (authTime != null) {
+                	log.trace("{} Resetting authentication time to Duo value: {}", getLogPrefix(), authTime);
+                	final AuthenticationResult ar = authenticationContext.getAuthenticationResult();
+                	ar.setAuthenticationInstant(authTime);
+                }
+                
                 recordSuccess(profileRequestContext);
                 return;
             } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(authResultStatus)) {
@@ -254,6 +266,22 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
       
     }
     
+    /**
+     * Extract the auth_time claim from the ID Token if present. Return {@code null} if not present or there is an
+     * error in obtaining the value.
+     * 
+     * @return the authentication time, or {@code null} if the claim is absent or cannot be parsed
+     */
+    @Nullable private Instant extractAuthenticationTime() {
+    	try {
+			final Date authTimeDate = claimsSet.getDateClaim(IDTokenClaims.AUTHENTICATION_TIME.getClaimName());			
+			return authTimeDate != null ? authTimeDate.toInstant() : null;
+		} catch (final ParseException e) {
+			log.trace("{} Failed to extract auth_time claim", getLogPrefix(), e);
+			return null;
+		}
+    }
+    
     /**
      * Extract the second-factor used for authentication as taken from the auth_context and other
      * details to be stored in the Duo context.
@@ -266,14 +294,14 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
             
             if (authnContextClaimObj != null) {         
                 final Object factorClaimObj = authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_AUTH_FACTOR_JSON_OBJECT);
-                if (factorClaimObj instanceof String factor) {
+                if (factorClaimObj instanceof final String factor) {
                     duoContext.setFactorUsed(factor);
                 }
                 
                 final Object deviceClaimObj = authnContextClaimObj.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_JSON_OBJECT);
-                if (deviceClaimObj instanceof Map<?,?> device) {
+                if (deviceClaimObj instanceof final Map<?,?> device) {
                     final Object keyObj = device.get(DuoOIDCAuthAPI.DUO_AUTH_DEVICE_KEY_JSON_OBJECT);
-                    if (keyObj instanceof String key) {
+                    if (keyObj instanceof final String key) {
                         duoContext.setDeviceKey(key);
                     }
                 }
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
index d584436c..5a6d4285 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
@@ -80,6 +81,33 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
         ActionTestingSupport.assertProceedEvent(event);
     }
     
+    /**
+     * Test successful execution checking the authentication instant comes from the authn_time of the Duo response.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public void testExecuteSuccessWithAuthnTimeSet() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        final Instant authnTime = Instant.now();
+        dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(),authnTime, "api.duosecurity.com", "duo_push"));
+        dc.setUsername("jdoe");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        //check the correct authentication_time has been populated.
+        final var authnResult = ac.getAuthenticationResult();
+        assert authnResult != null;
+        final Instant authenticationInstant = authnResult.getAuthenticationInstant();
+        // Compare seconds only, due to the conversation into a JWT and back.
+        assertEquals(authenticationInstant.getEpochSecond(), authnTime.getEpochSecond());
+    }
+    
     /**
      * Test successful execution with factor enforcement.
      * 

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


More information about the commits mailing list