[java-oidc-common] branch main updated: Revert "Removed the deprecated AuthenticationTimeClaimsValidator class + references."

Phil Smart philip.smart at jisc.ac.uk
Fri Sep 8 11:14:04 UTC 2023


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

philsmart pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=5d6d4ce67479fcf4c2906f63b4194c7a6b8ff88f

The following commit(s) were added to refs/heads/main by this push:
     new 5d6d4ce  Revert "Removed the deprecated AuthenticationTimeClaimsValidator class + references."
5d6d4ce is described below

commit 5d6d4ce67479fcf4c2906f63b4194c7a6b8ff88f
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Sep 8 12:13:15 2023 +0100

    Revert "Removed the deprecated AuthenticationTimeClaimsValidator class + references."
    
    This reverts commit 088a7de402d76fbed8bebe6fd65a36bca13a843e.
---
 .../impl/AuthenticationTimeClaimsValidator.java    | 244 +++++++++++++++++++++
 .../AuthenticationTimeClaimsValidatorTest.java     | 202 +++++++++++++++++
 .../impl/ChainingJWTClaimsValidatorTest.java       |  27 +++
 3 files changed, 473 insertions(+)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidator.java
new file mode 100644
index 0000000..d78e843
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidator.java
@@ -0,0 +1,244 @@
+/*
+ * 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.oidc.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+/**
+ * Verifies the auth_time (when the End-User authentication took place):
+ * <ol>
+ * <li>If the authnLifetimeLookup returns 0 seconds (e.g. max_age=0), assume the 'forced authentication' semantic, and 
+ * check the auth_time is after the authentication request time.</li>
+ * <li>Or, if the authnLifetimeLookup returns a value >0, check the authentication occurred within a valid expiration
+ * window.</li>
+ * </ol>
+ * 
+ * <p>A predicate determines if the auth_time was requested e.g. was explicitly requested, or the max_age
+ * claim was set. Defaults to true.</p>
+ */
+ at ThreadSafeAfterInit
+public class AuthenticationTimeClaimsValidator extends AbstractClaimsValidator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AuthenticationTimeClaimsValidator.class);
+    
+    /** 
+     * Lookup strategy to find the amount of time for which a token is valid 
+     * after if it was first issued. (Default value: 60 seconds) 
+     */
+    @Nonnull private Function<ProfileRequestContext, Duration> authnLifetimeLookupStrategy;
+    
+    /** 
+     * Lookup strategy to find the time at which the authentication request was made.
+     * Defaults to now minus the clockskew.
+     */
+    @Nonnull private Function<ProfileRequestContext, Instant> authnRequestTimeLookupStrategy;
+    
+    /** 
+     * Positive clock skew adjustment to consider when checking auth_time is not in the future 
+     * or has expired. (Default value: 60 seconds). 
+     */
+    @Nonnull private Duration clockSkew;
+    
+    /**
+     *  Has the auth_time been requested, either explicitly or from the max_age parameter?
+     *  Defaults to true. 
+     */ 
+    @Nonnull private Predicate<ProfileRequestContext> requested;
+    
+    /** Constructor.*/
+    public AuthenticationTimeClaimsValidator() {
+        authnLifetimeLookupStrategy = prc ->  Duration.ofSeconds(60);
+        clockSkew = Duration.ofSeconds(60);
+        authnRequestTimeLookupStrategy = prc -> Instant.now().minus(clockSkew);
+        requested = Predicates.alwaysTrue();
+    }
+    
+    /**
+     * Set the clock skew.
+     * 
+     * @param skew clock skew to set
+     */
+    public void setClockSkew(@Nonnull final Duration skew) {  
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy to find out when the authentication request (if any) was made.
+     * 
+     * @param strategy the strategy
+     * 
+     * @since 2.2.0
+     */
+    public void setAuthnRequestTimeLookupStrategy(
+            final Function<ProfileRequestContext, Instant> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        ifDestroyedThrowDestroyedComponentException();
+
+        authnRequestTimeLookupStrategy = Constraint.isNotNull(strategy,
+                "authnRequestTimeLookupStrategy can not be null");
+    }
+
+    
+    /**
+     * Has the auth_time been request e.g. explicitly, or by using the max_age parameter.
+     * @deprecated use the activation condition in the base class instead.
+     * 
+     * @param isRequested has auth_time been requested.
+     */
+    @Deprecated(forRemoval = true, since = "2.2.0")
+    public void setRequested(final Predicate<ProfileRequestContext> isRequested) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        requested = isRequested;
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid from when the original authentication took place.
+     * 
+     * @param lifetime amount of time for which a token is valid
+     */
+    public void setAuthnLifetime(@Nonnull final Duration lifetime) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
+        Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
+        
+        authnLifetimeLookupStrategy = FunctionSupport.constant(lifetime);
+    }
+    
+    /**
+     * Set the lookup strategy used to locate the amount of time for which a token is valid from when the original 
+     * authentication took place.
+     * 
+     * @param strategy the strategy
+     * 
+     * @since 2.2.0
+     */
+    public void setAuthnLifetimeLookupStrategy(@Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        ifDestroyedThrowDestroyedComponentException();
+
+        authnLifetimeLookupStrategy = Constraint.isNotNull(strategy,
+                "AuthnLifetime Lookup Strategy can not be null");
+    }
+
+ // Checkstyle: CyclomaticComplexity|MethodLength OFF
+    @Override
+    public void doValidate(@Nonnull final JWTClaimsSet claimsSet, @Nonnull final ProfileRequestContext context) 
+            throws JWTValidationException {
+       
+        if (!requested.test(context)) {
+            //not requested, so nothing to check.
+            return;
+        } else {
+            
+            try { 
+                final Duration authnLifetime = authnLifetimeLookupStrategy.apply(context);
+                final Date authTimeDate = claimsSet.getDateClaim(IDTokenClaims.AUTHENTICATION_TIME.getClaimName());
+                
+                if (authTimeDate == null) {
+                    throw new JWTValidationException("No authentication time found in token");
+                }
+                if (authnLifetime == null) {
+                    throw new JWTValidationException("No authentication lifetime set");
+                }
+                
+                if (authnLifetime.equals(Duration.ofSeconds(0))) {
+                    // Now assume forced authentication semantics, the authentication must have happened after the 
+                    // authentication request was made to the OP
+                    if (authnRequestTimeLookupStrategy == null) {
+                        log.warn("Maximum authentication age of 0 seconds requested, but no "
+                                + "authentication request time lookup strategy set, can not check for a fresh "
+                                + "authentication");
+                        throw new JWTValidationException("Maximum authentication age of 0 seconds requested, but no "
+                                + "authentication request time lookup strategy set, can not check for a fresh "
+                                + "authentication");
+                    }
+                    final Instant authnRequestTime = authnRequestTimeLookupStrategy.apply(context);
+                    
+                    if (authnRequestTime == null) {
+                        log.warn("Maximum authentication age of 0 seconds requested, but no "
+                                + "authentication request time could be found, can not check for a fresh "
+                                + "authentication");
+                        throw new JWTValidationException("Maximum authentication age of 0 seconds requested, but no "
+                                + "authentication request time could be found, can not check for a fresh "
+                                + "authentication");
+                    }
+                    
+                    // Check authTime is after the time which the authentication request was made
+                    if (authTimeDate.toInstant().isBefore(authnRequestTime)) {
+                        log.warn("JWT token authentication time is not valid. Authentication is not fresh but max_age=0"
+                                + " was requested, re-authentication did not occur");
+                        throw new JWTValidationException("JWT token authentication time is not valid. Authentication "
+                                + "is not fresh but max_age=0 was requested, re-authentication did not occur");
+                    }
+                    
+                } else {             
+                    final Instant authTime = authTimeDate.toInstant();
+                    final Instant now = Instant.now();
+                    final Instant latestValid = now.plus(clockSkew);
+                    final Instant expiration = authTime.plus(clockSkew).plus(authnLifetime);
+                    
+                    // Check time of authentication wasn't in the future
+                    if (authTime.isAfter(latestValid)) {
+                        log.warn("Authentication is not yet valid: auth_time was {}, latest valid is: {}",
+                                authTime, latestValid);
+                        throw new JWTValidationException("JWT token authentication time is not yet valid");
+                    }
+    
+                    // Check time of authentication has not expired
+                    if (expiration.isBefore(now)) {
+                        log.warn(
+                                "Authentication has expired: auth_time was '{}', "
+                                + "expired at: '{}', current time: '{}'",
+                                authTime, expiration, now);
+                        throw new JWTValidationException("JWT token authentication time has expired");
+                    }
+                    //is OK.
+                }
+                
+            } catch (final ParseException e) {
+                throw new JWTValidationException("Autentication forced, but no authentication time found in token",e);
+            }
+        }
+        
+    }
+
+ // Checkstyle: CyclomaticComplexity|MethodLength ON
+}
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidatorTest.java
new file mode 100644
index 0000000..5dbe72a
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationTimeClaimsValidatorTest.java
@@ -0,0 +1,202 @@
+/* 
+ * 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.oidc.security.jwt.claims.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/** Test for the {@link AuthenticationTimeClaimsValidator}. */
+public class AuthenticationTimeClaimsValidatorTest extends AbstractClaimsValidatorTest {
+    
+    /** The validator to test.*/
+    @Nonnull private AuthenticationTimeClaimsValidator validator;
+
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws ComponentInitializationException {
+        super.setup();
+        validator = new AuthenticationTimeClaimsValidator();
+    }
+    
+    @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);   
+    }
+    
+    @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);
+    }
+    
+    /**
+     * Would fail, but the claim was not requested, so it is not checked.
+     * 
+     * @throws JWTValidationException on error.
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public void doValidateAuthTimeNotRequested() throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().build();
+        validator.setId("test-validator");   
+        validator.setRequested(prc -> false);
+        validator.setAuthnLifetime(Duration.ofMinutes(1));
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+    
+}
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/ChainingJWTClaimsValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/ChainingJWTClaimsValidatorTest.java
index 16b1690..7e1f340 100644
--- a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/ChainingJWTClaimsValidatorTest.java
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/ChainingJWTClaimsValidatorTest.java
@@ -86,6 +86,14 @@ public class ChainingJWTClaimsValidatorTest extends AbstractClaimsValidatorTest{
         usernameExactV.initialize();
         validators.add(usernameExactV);
         
+        //auth time
+        final AuthenticationTimeClaimsValidator authTimeV = new AuthenticationTimeClaimsValidator();
+        authTimeV.setId("auth-time-validator");
+        authTimeV.setAuthnLifetime(Duration.ofMinutes(1));
+        authTimeV.setRequested(prc -> true);
+        authTimeV.initialize();
+        validators.add(authTimeV);
+        
         //nonce
         final ExactMatchClaimsValidator nonceV = new ExactMatchClaimsValidator();
         nonceV.setId("nonce-validator");
@@ -310,4 +318,23 @@ public class ChainingJWTClaimsValidatorTest extends AbstractClaimsValidatorTest{
         validation.validate(claimsSet, prc);        
     }
     
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void validationFailedAuthTimeTooFarInPast() throws ComponentInitializationException, JWTValidationException {
+        
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .issuer("issuer")
+                .subject("jdoe")
+                .expirationTime(Date.from(Instant.now().plus(Duration.ofMinutes(10))))
+                .audience("audience")
+                .notBeforeTime(Date.from(Instant.now().minus(Duration.ofMinutes(1))))
+                .issueTime(Date.from(Instant.now()))
+                .claim(IDTokenClaims.AUTHENTICATION_TIME.getClaimName(), 
+                        Instant.now().minus(Duration.ofSeconds(300)).getEpochSecond())
+                .claim("username", "jdoe")
+                .claim(IDTokenClaims.NONCE.getClaimName(), "nonce")
+                .build();        
+
+        validation.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