[java-oidc-common] branch main updated: JCOMOIDC-129 - Add profile option for reverting 'aud' claims in JWT authentication assertions to tokenEndpointURL

Henri Mikkonen henri.mikkonen at iki.fi
Fri Jun 6 11:12:06 UTC 2025


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

hjmikkon 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=191947e8ff2b54f03774163820be75102ab2f934

The following commit(s) were added to refs/heads/main by this push:
     new 191947e  JCOMOIDC-129 - Add profile option for reverting 'aud' claims in JWT authentication assertions to tokenEndpointURL
191947e is described below

commit 191947e8ff2b54f03774163820be75102ab2f934
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Jun 6 14:11:36 2025 +0300

    JCOMOIDC-129 - Add profile option for reverting 'aud' claims in JWT authentication assertions to tokenEndpointURL
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-129
    
    - New claim validator JWTAuthenticationAudienceClaimsValidator exploits the new profile configuration option 'useTargetedEndpointAsJWTAudience'
    - Deprecated AuthenticationAudienceClaimsValidator
---
 .../AuthenticationAudienceClaimsValidator.java     |   6 ++
 ... JWTAuthenticationAudienceClaimsValidator.java} |  59 ++++++++----
 .../AuthenticationAudienceClaimsValidatorTest.java |   1 +
 ...AuthenticationAudienceClaimsValidatorTest.java} | 107 ++++++++++++++++++++-
 4 files changed, 151 insertions(+), 22 deletions(-)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java
index 25d4b53..01e1ba6 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java
@@ -32,6 +32,8 @@ import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.DeprecationSupport;
+import net.shibboleth.shared.primitive.DeprecationSupport.ObjectType;
 import net.shibboleth.shared.primitive.StringSupport;
 
 /**
@@ -45,8 +47,10 @@ import net.shibboleth.shared.primitive.StringSupport;
  * to return the endpoint URL used in the HTTP request containing the JWT authentication.
  * 
  * @since 2.2.0
+ * @deprecated
  */
 @ThreadSafeAfterInit
+ at Deprecated(since = "3.3.0", forRemoval=true)
 public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidator{
     
     /** Lookup function for the responder identifier. */
@@ -115,6 +119,8 @@ public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidat
      */
     @Override @Nonnull @NotEmpty protected Set<String> resolveAcceptedAudiences(@Nonnull final JWTClaimsSet claims,
             @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+        DeprecationSupport.warn(ObjectType.CLASS, getClass().getName(), null,
+                "Use JWTAuthenticationAudienceClaimsValidator");
         final String endpointAudience = super.resolveAcceptedAudiences(claims, context).iterator().next();
         final Set<String> result = new HashSet<>();
         final String responderId = responderIdLookupStrategy.apply(context);
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidator.java
similarity index 71%
copy from oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java
copy to oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidator.java
index 25d4b53..7db8867 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidator.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidator.java
@@ -18,6 +18,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -27,6 +28,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import com.nimbusds.jwt.JWTClaimsSet;
 
 import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.UseTargetedEndpointAsJWTAudienceLookupFunction;
 import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
@@ -42,16 +44,20 @@ import net.shibboleth.shared.primitive.StringSupport;
  * desired to be accepted in some other endpoints.
  * 
  * The {@link AudienceClaimsValidator#resolveAcceptedAudiences(JWTClaimsSet, ProfileRequestContext)} is expected
- * to return the endpoint URL used in the HTTP request containing the JWT authentication.
+ * to return the endpoint URL used in the HTTP request containing the JWT authentication. The allowance of targeted
+ * endpoint as audience can be configured via {@link #allowTargetedEndpointAsJWTAudienceCondition}.
  * 
- * @since 2.2.0
+ * @since 3.3.0
  */
 @ThreadSafeAfterInit
-public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidator{
+public class JWTAuthenticationAudienceClaimsValidator extends AudienceClaimsValidator{
     
     /** Lookup function for the responder identifier. */
     @Nonnull private Function<ProfileRequestContext, String> responderIdLookupStrategy;
-    
+
+    /** Condition to allow targeted endpoint as audience in addition to the responder identifier. */
+    @Nonnull private Predicate<ProfileRequestContext> allowTargetedEndpointAsJWTAudienceCondition;
+
     /** The substrings to replace from resolved endpoint with one configured at {@link #endpointReplacement}. */
     @Nonnull private List<String> endpointTargets;
 
@@ -59,9 +65,10 @@ public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidat
     @Nullable private String endpointReplacement;
     
     /** Constructor. */
-    public AuthenticationAudienceClaimsValidator() {
+    public JWTAuthenticationAudienceClaimsValidator() {
         super();
         responderIdLookupStrategy = new IssuerLookupFunction();
+        allowTargetedEndpointAsJWTAudienceCondition = new UseTargetedEndpointAsJWTAudienceLookupFunction();
         endpointTargets = CollectionSupport.emptyList();
         endpointReplacement = null;
     }
@@ -72,20 +79,32 @@ public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidat
      * @param strategy What to set.
      */
     public void setResponderIdLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
-        ifInitializedThrowUnmodifiabledComponentException();
-        ifDestroyedThrowDestroyedComponentException();
-
+        checkSetterPreconditions();
+        
         responderIdLookupStrategy = Constraint.isNotNull(strategy, "The responder ID lookup strategy cannot be null");
     }
 
     /**
+     * Set the condition to allow targeted endpoint as audience in addition to the responder identifier.
+     *
+     * @param condition What to set.
+     */
+    public void setAllowTargetedEndpointAsJWTAudienceCondition(
+            @Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+
+        allowTargetedEndpointAsJWTAudienceCondition = Constraint.isNotNull(condition,
+                "The allow targeted endpoint condition cannot be null");
+    }
+
+    /**
+
      * Set the substrings to replace from resolved endpoint with one configured at {@link #endpointReplacement}.
      * 
      * @param paths What to set.
      */
     public void setEndpointTargets(@Nonnull final List<String> paths) {
-        ifInitializedThrowUnmodifiabledComponentException();
-        ifDestroyedThrowDestroyedComponentException();
+        checkSetterPreconditions();
 
         endpointTargets = Constraint.isNotNull(paths, "The endpoint paths to replace cannot be null");
     }
@@ -96,8 +115,7 @@ public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidat
      * @param path What to set.
      */
     public void setEndpointReplacement(@Nullable final String path) {
-        ifInitializedThrowUnmodifiabledComponentException();
-        ifDestroyedThrowDestroyedComponentException();
+        checkSetterPreconditions();
 
         endpointReplacement = path;
     }
@@ -115,19 +133,24 @@ public class AuthenticationAudienceClaimsValidator extends AudienceClaimsValidat
      */
     @Override @Nonnull @NotEmpty protected Set<String> resolveAcceptedAudiences(@Nonnull final JWTClaimsSet claims,
             @Nonnull final ProfileRequestContext context) throws JWTValidationException {
-        final String endpointAudience = super.resolveAcceptedAudiences(claims, context).iterator().next();
         final Set<String> result = new HashSet<>();
         final String responderId = responderIdLookupStrategy.apply(context);
         if (StringSupport.trimOrNull(responderId) != null) {
             result.add(responderId);
         }
-        result.add(endpointAudience);
-        for (final String path : endpointTargets) {
-            if (endpointAudience.contains(path) && endpointReplacement != null) {
-                result.add(endpointAudience.replace(path, endpointReplacement));
+        if (allowTargetedEndpointAsJWTAudienceCondition.test(context)) {
+            final String endpointAudience = super.resolveAcceptedAudiences(claims, context).iterator().next();
+            result.add(endpointAudience);
+            for (final String path : endpointTargets) {
+                if (endpointAudience.contains(path) && endpointReplacement != null) {
+                    result.add(endpointAudience.replace(path, endpointReplacement));
+                }
             }
         }
-        return result;
+        if (result.isEmpty()) {
+            throw new JWTValidationException("Could not resolve any accepted audiences");
+        }
+        return CollectionSupport.copyToSet(result);
     }
 
 }
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java
index db68f48..8375b4a 100644
--- a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java
@@ -30,6 +30,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 
 /** Test for the {@link AuthenticationAudienceClaimsValidator}. */
 @SuppressWarnings(value={"javadoc","null"})
+ at Deprecated(since = "3.3.0", forRemoval=true)
 public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsValidatorTest {
     
     /** The validator to test.*/
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidatorTest.java
similarity index 64%
copy from oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java
copy to oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidatorTest.java
index db68f48..80529b0 100644
--- a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/AuthenticationAudienceClaimsValidatorTest.java
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTAuthenticationAudienceClaimsValidatorTest.java
@@ -27,21 +27,23 @@ import com.nimbusds.jwt.JWTClaimsSet;
 
 import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.PredicateSupport;
 
-/** Test for the {@link AuthenticationAudienceClaimsValidator}. */
+/** Test for the {@link JWTAuthenticationAudienceClaimsValidator}. */
 @SuppressWarnings(value={"javadoc","null"})
-public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsValidatorTest {
+public class JWTAuthenticationAudienceClaimsValidatorTest extends AbstractClaimsValidatorTest {
     
     /** The validator to test.*/
-    private AuthenticationAudienceClaimsValidator validator;
+    private JWTAuthenticationAudienceClaimsValidator validator;
 
+    @SuppressWarnings("unchecked")
     private final Function<ProfileRequestContext, String> responderIdLookup = Mockito.mock(Function.class);
     
     @Override
     @BeforeMethod
     public void setup() throws ComponentInitializationException {
         super.setup();
-        validator = new AuthenticationAudienceClaimsValidator();
+        validator = new JWTAuthenticationAudienceClaimsValidator();
         validator.setId("test-validator");
     }
 
@@ -63,6 +65,21 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_EndpointAsOnlyAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = 
+                new JWTClaimsSet.Builder().audience("https://localhost/idp/profile/oauth2/one").build();
+        validator.setExtraAudienceValidation(false);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -77,6 +94,22 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_EndpointAsOneAudience_noAdditionalAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet =
+                new JWTClaimsSet.Builder().audience(List.of("https://localhost/idp/profile/oauth2/one", "another"))
+                .build();
+        validator.setExtraAudienceValidation(false);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -106,6 +139,23 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
         validator.setAdditionalAudiencesLookupStrategy((prc, claims) -> Set.of("another"));
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_EndpointAsOneAudience_withTrustedAdditionalAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet =
+                new JWTClaimsSet.Builder().audience(List.of("https://localhost/idp/profile/oauth2/one", "another"))
+                .build();
+        validator.setExtraAudienceValidation(true);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAdditionalAudiencesLookupStrategy((prc, claims) -> Set.of("another"));
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -119,6 +169,21 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_ReplacedEndpointAsOnlyAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet = 
+                new JWTClaimsSet.Builder().audience("https://localhost/idp/profile/oidc/token").build();
+        validator.setExtraAudienceValidation(false);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -133,6 +198,22 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_ReplacedEndpointAsOneAudience_noAdditionalAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet =
+                new JWTClaimsSet.Builder().audience(List.of("https://localhost/idp/profile/oidc/token", "another"))
+                .build();
+        validator.setExtraAudienceValidation(false);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -162,6 +243,23 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
         validator.setEndpointReplacement("/profile/oidc/token");
         validator.setAdditionalAudiencesLookupStrategy((prc, claims) -> Set.of("another"));
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysTrue());
+        validator.initialize();
+        validator.validate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidTest_ReplacedEndpointAsOneAudience_withTrustedAdditionalAudience_endpointIgnored()
+            throws JWTValidationException, ComponentInitializationException {
+        final JWTClaimsSet claimsSet =
+                new JWTClaimsSet.Builder().audience(List.of("https://localhost/idp/profile/oidc/token", "another"))
+                .build();
+        validator.setExtraAudienceValidation(true);
+        validator.setAudienceLookupStrategy((prc,claims) -> "https://localhost/idp/profile/oauth2/one");
+        validator.setEndpointTargets(List.of("/profile/oauth2/one", "/profile/oauth2/two"));
+        validator.setEndpointReplacement("/profile/oidc/token");
+        validator.setAdditionalAudiencesLookupStrategy((prc, claims) -> Set.of("another"));
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         validator.initialize();
         validator.validate(claimsSet, prc);
     }
@@ -177,6 +275,7 @@ public class AuthenticationAudienceClaimsValidatorTest extends AbstractClaimsVal
         validator.setEndpointReplacement("/profile/oidc/token");
         Mockito.when(responderIdLookup.apply(Mockito.any())).thenReturn("https://op.example.org");
         validator.setResponderIdLookupStrategy(responderIdLookup);
+        validator.setAllowTargetedEndpointAsJWTAudienceCondition(PredicateSupport.alwaysFalse());
         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