[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