[java-idp-plugin-duo] 01/02: Move test from oidc-common into Duo and add further unit tests

Codeberg noreply at shibboleth.net
Wed Sep 23 14:54:32 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/a0f3192e1f5aa2867674a81f439a43429cacd3fc

commit a0f3192e1f5aa2867674a81f439a43429cacd3fc
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Sep 23 15:29:48 2026 +0100

    Move test from oidc-common into Duo and add further unit tests
    
     - DuoAuthenticationTimeClaimsValidator is a variant of the
    AuthenticationTimeClaimsValidator in commons, as the semantics have
    changed a bit. So I moved in the tests for
    AuthenticationTimeClaimsValidator from commons and modified them
    slightly.
---
 .../authn/duo/AuthenticationRequestOptions.java    |   2 +-
 ...uthenticationRequestTimeLookupStrategyTest.java |  77 +++++++
 .../DuoAuthenticationTimeClaimsValidatorTest.java  | 224 +++++++++++++++++++++
 3 files changed, 302 insertions(+), 1 deletion(-)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestOptions.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestOptions.java
index 5b50edf9..a2fde851 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestOptions.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestOptions.java
@@ -49,7 +49,7 @@ public class AuthenticationRequestOptions {
 	 *
 	 * @return this options
 	 */
-	public AuthenticationRequestOptions setUsername(@Nullable final String value) {
+	@Nonnull public AuthenticationRequestOptions setUsername(@Nullable final String value) {
 		username = value;
 		return this;
 	}
diff --git a/idp-duo-api/src/test/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestTimeLookupStrategyTest.java b/idp-duo-api/src/test/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestTimeLookupStrategyTest.java
new file mode 100644
index 00000000..f746a4a8
--- /dev/null
+++ b/idp-duo-api/src/test/java/net/shibboleth/idp/plugin/authn/duo/AuthenticationRequestTimeLookupStrategyTest.java
@@ -0,0 +1,77 @@
+
+package net.shibboleth.idp.plugin.authn.duo;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+
+/**
+ * Tests for AuthenticationRequestTimeLookupStrategy.
+ */
+public class AuthenticationRequestTimeLookupStrategyTest {
+	
+	private ProfileRequestContext prc;
+
+	private AuthenticationContext authnContext;
+	
+	private DuoOIDCAuthenticationContext duoCtx;
+	
+	private DefaultDuoOIDCIntegration integ;
+
+	@BeforeMethod
+	public void setup() {
+		prc = new ProfileRequestContext();
+		authnContext = new AuthenticationContext();
+		duoCtx = authnContext.ensureSubcontext(DuoOIDCAuthenticationContext.class);
+		prc.addSubcontext(authnContext);
+		integ = new DefaultDuoOIDCIntegration();
+		integ.setAPIHost("host.com");
+        integ.setAuthorizeEndpoint("https://host.com/authz");
+        integ.setTokenEndpoint("https://host.com/token");
+        integ.setHealthCheckEndpoint("https://host.com/health");
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
+        integ.setRegisteredRedirectURI("http://localhost/");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+		duoCtx.setIntegration(integ);
+	}
+	
+	@Test
+	public void testRequestTimeWithClockSkew() {
+		final Duration skew = Duration.ofSeconds(30);
+		final Instant requestTime = Instant.now();
+		duoCtx.setAuthnRequestTime(requestTime);
+		final AuthenticationRequestTimeLookupStrategy strategy = new AuthenticationRequestTimeLookupStrategy(skew);
+		Assert.assertEquals(strategy.apply(prc), requestTime.minus(skew));
+	}
+	
+	@Test
+	public void testRequestTimeWithNoClockSkew() {
+		final Duration skew = Duration.ofSeconds(0);
+		final Instant requestTime = Instant.now();
+		duoCtx.setAuthnRequestTime(requestTime);
+		final AuthenticationRequestTimeLookupStrategy strategy = new AuthenticationRequestTimeLookupStrategy(skew);
+		Assert.assertEquals(strategy.apply(prc), requestTime);
+	}
+	
+	@Test
+	public void testNoAuthenticationRequestTimeSet_UseDefault() {		
+		final Duration skew = Duration.ofSeconds(30);
+		final AuthenticationRequestTimeLookupStrategy strategy = new AuthenticationRequestTimeLookupStrategy(skew);
+		
+		final Instant before = Instant.now().minus(skew);
+		final Instant requestTime = strategy.apply(prc);
+		final Instant after = Instant.now().minus(skew);
+
+		Assert.assertFalse(requestTime.isBefore(before));
+		Assert.assertFalse(requestTime.isAfter(after));
+	}
+	
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthenticationTimeClaimsValidatorTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthenticationTimeClaimsValidatorTest.java
new file mode 100644
index 00000000..f7700104
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthenticationTimeClaimsValidatorTest.java
@@ -0,0 +1,224 @@
+/* 
+ * Licensed 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.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.security.jwt.claims.impl.AuthenticationTimeClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.IDTokenClaims;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/** Test for the {@link AuthenticationTimeClaimsValidator}. */
+ at SuppressWarnings(value={"javadoc","null"})
+public class DuoAuthenticationTimeClaimsValidatorTest {
+    
+    /** The validator to test.*/
+    private DuoAuthenticationTimeClaimsValidator validator;
+    
+    /** The request context.*/
+    protected RequestContext src;
+
+    /** The profile request context.*/
+    protected ProfileRequestContext prc;
+    
+    /** The authentication context to add to the profile request context.*/
+    protected  AuthenticationContext ac;
+    
+    @BeforeMethod
+    public void setup() throws ComponentInitializationException {
+        validator = new DuoAuthenticationTimeClaimsValidator();
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
+        ac = new AuthenticationContext();
+        
+        prc.addSubcontext(ac);
+    }
+    
+    @Test
+    public void doValidateTest() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), Instant.now().getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.setClockSkew(Duration.ofSeconds(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);   
+    }
+    
+    /** 
+     * Check that a null outcome from the authn lifetime strategy disables the check. If it does not an exception
+     * would be thrown.
+     * 
+     * @throws JWTValidationException on error
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void doValidateTest_AuthLifetimeDisablesTest() 
+    		throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), Instant.now().minus(Duration.ofHours(1))
+                .getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetimeLookupStrategy(prc -> null);
+        validator.setClockSkew(Duration.ofSeconds(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);   
+    }
+    
+    @Test
+    public void doValidateTest_UseAuthnStrategy() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), Instant.now().getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetimeLookupStrategy(prc -> Duration.ofMinutes(1));
+        validator.setClockSkew(Duration.ofSeconds(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);   
+    }
+    
+    @Test
+    public void doValidateTest_DefaultAuthnTime() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), Instant.now().getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setClockSkew(Duration.ofSeconds(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);   
+    }
+    
+    @Test
+    public void doValidateTestWithinLifetime() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().minus(Duration.ofSeconds(30)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.setClockSkew(Duration.ofSeconds(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test
+    public void doValidateNoClaimButNotActive() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().build();
+        validator.setActivationCondition((prc,claims) -> false);
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doRejectedTestCanNotParseDate() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(),"not-a-date").build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doRejectedTest() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().minus(Duration.ofMinutes(10)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.setClockSkew(Duration.ofMinutes(0));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doRejectedInTheFuture() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().plus(Duration.ofSeconds(30)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        //set clockskew to 10 seconds, and JWT is 30 seconds in the future, so should throw
+        validator.setClockSkew(Duration.ofSeconds(10));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test
+    public void doRejectedInTheFutureButInsideSkew() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().plus(Duration.ofSeconds(30)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        //set clockskew to 60 seconds, and JWT is 30 seconds in the future, so should be fine
+        validator.setClockSkew(Duration.ofSeconds(60));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doRejectedNoClaim() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().build();
+        validator.setId("test-validator");   
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doRejectedForceAuthenticationRequestButOldAuthnTime() 
+            throws JWTValidationException, ComponentInitializationException {
+        // Authentication occurs 10 second before the authn request was sent
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().minus(Duration.ofSeconds(10)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        // returning 0 seconds 'signals' forced authentication
+        validator.setAuthnLifetime(Duration.ofSeconds(0));
+        validator.setAuthnRequestTimeLookupStrategy(prc -> Instant.now());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    
+    @Test
+    public void doValidForceAuthenticationRequest() 
+            throws JWTValidationException, ComponentInitializationException {
+        // Authentication occurs 10 second after the authn request was sent
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().claim(
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                Instant.now().plus(Duration.ofSeconds(10)).getEpochSecond()).build();
+        validator.setId("test-validator");   
+        // returning 0 seconds 'signals' forced authentication
+        validator.setAuthnLifetime(Duration.ofSeconds(0));
+        validator.setAuthnRequestTimeLookupStrategy(prc -> Instant.now());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+    
+}

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


More information about the commits mailing list