[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