[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