[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