[java-oidc-common] branch main updated: Improve auth_time validation

Phil Smart philip.smart at jisc.ac.uk
Fri Apr 21 11:23:27 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=a2b84d431c52c73528b0192221eb50d23f476b3c

The following commit(s) were added to refs/heads/main by this push:
     new a2b84d4  Improve auth_time validation
a2b84d4 is described below

commit a2b84d431c52c73528b0192221eb50d23f476b3c
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Apr 21 12:23:25 2023 +0100

    Improve auth_time validation
    
    If max_age=0 there is no need to use a 'window', just check the
    auth_time is after the authentication request time i.e. authentication
    was performed
---
 .../impl/AuthenticationTimeClaimsValidator.java    | 108 +++++++++++++++++----
 .../AuthenticationTimeClaimsValidatorTest.java     |  31 ++++++
 .../profile/core/OIDCAuthenticationRequest.java    |  26 +++++
 3 files changed, 145 insertions(+), 20 deletions(-)

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
index af5cd92..18db2db 100644
--- 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
@@ -42,7 +42,14 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 
 /**
- * Verifies the auth_time (when the End-User authentication took place) is within a valid expiration window.
+ * 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.</p>
+ * <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>
  */
@@ -58,6 +65,12 @@ public class AuthenticationTimeClaimsValidator extends AbstractClaimsValidator {
      */
     @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). 
@@ -74,6 +87,7 @@ public class AuthenticationTimeClaimsValidator extends AbstractClaimsValidator {
     public AuthenticationTimeClaimsValidator() {
         authnLifetimeLookupStrategy = prc ->  Duration.ofSeconds(60);
         clockSkew = Duration.ofSeconds(60);
+        authnRequestTimeLookupStrategy = prc -> Instant.now().minus(clockSkew);
         requested = Predicates.alwaysTrue();
     }
     
@@ -87,6 +101,22 @@ public class AuthenticationTimeClaimsValidator extends AbstractClaimsValidator {
         
         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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        authnRequestTimeLookupStrategy = Constraint.isNotNull(strategy,
+                "authnRequestTimeLookupStrategy can not be null");
+    }
 
     
     /**
@@ -142,31 +172,69 @@ public class AuthenticationTimeClaimsValidator extends AbstractClaimsValidator {
         } 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");
                 }
-                final Instant authTime = authTimeDate.toInstant();
-                final Instant now = Instant.now();
-                final Instant latestValid = now.plus(clockSkew);
-                final Instant expiration = authTime.plus(clockSkew).plus(authnLifetimeLookupStrategy.apply(context));
-                
-                // 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");
+                if (authnLifetime == null) {
+                    throw new JWTValidationException("No authentication lifetime set");
                 }
-
-                // 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");
+                
+                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.
                 }
-                //is OK.
                 
             } catch (final ParseException e) {
                 throw new JWTValidationException("Autentication forced, but no authentication time found in token",e);
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
index fa9ed04..7c32ae9 100644
--- 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
@@ -155,6 +155,37 @@ public class AuthenticationTimeClaimsValidatorTest extends AbstractClaimsValidat
         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.
      * 
diff --git a/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/core/OIDCAuthenticationRequest.java b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/core/OIDCAuthenticationRequest.java
index 745f433..2b8341b 100644
--- a/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/core/OIDCAuthenticationRequest.java
+++ b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/core/OIDCAuthenticationRequest.java
@@ -19,6 +19,7 @@ package net.shibboleth.oidc.profile.core;
 
 import java.net.URI;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 
@@ -96,6 +97,9 @@ public class OIDCAuthenticationRequest extends OAuthAuthorizationRequest {
     /** The nonce. */
     @Nullable private Nonce nonce;
     
+    /** The time at which the RP made the authentication request to the OP.*/
+    @Nullable private Instant authnRequestTime;
+    
     /**
      * 
      * Constructor.
@@ -330,6 +334,28 @@ public class OIDCAuthenticationRequest extends OAuthAuthorizationRequest {
    public void setNonce(@Nullable final Nonce theNonce) {
        nonce = theNonce;
    }
+   
+   /**
+    * Set the time at which this RP sent this authentication request to the OP.
+    * 
+    * @param time the time the request was made
+    * 
+    * @since 2.2.0
+    */
+   public void setAuthnRequestTime(@Nullable final Instant time) {
+       authnRequestTime = time;
+   }
+   
+   /**
+    * Get the time at which this RP sent this authentication request to the OP.
+    * 
+    * @return the time the request was made
+    * 
+    * @since 2.2.0
+    */
+   @Nullable public Instant getAuthnRequestTime() {
+    return authnRequestTime;
+}
 
     //TODO others relating to sections 5.2, 5.5, 6, and 7.2.1
     

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


More information about the commits mailing list