[java-oidfed-common] 01/03: Import generic security classes from java-idp-plugin-oidc-op-oidfed
Codeberg
noreply at shibboleth.net
Fri May 15 07:49:29 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-oidfed-common.
View the commit online:
https://codeberg.org/Shibboleth/java-oidfed-common/commit/c0302fcf1921b3455fcbecc867db170b3f249947
commit c0302fcf1921b3455fcbecc867db170b3f249947
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 15 09:47:15 2026 +0300
Import generic security classes from java-idp-plugin-oidc-op-oidfed
Following impl packages were imported:
- net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential
- net.shibboleth.idp.plugin.oidc.op.oidfed.security.jose.impl
- net.shibboleth.idp.plugin.oidc.op.oidfed.security.jwt.claims.impl
The packages were renamed with the following login: 'net.shibboleth.idp.plugin.oidc.op.oidfed' -> 'net.shibboleth.oidfed'
---
.../support/ClientInformationExtensionSupport.java | 95 +++++++++++++++
oidfed-common-impl/pom.xml | 10 ++
...ormationFederationEntityCredentialResolver.java | 127 +++++++++++++++++++++
...faultEntityConfigurationCredentialResolver.java | 90 +++++++++++++++
.../DefaultLocalTrustAnchorCredentialResolver.java | 109 ++++++++++++++++++
...DefaultPayloadJOSEObjectCredentialResolver.java | 84 ++++++++++++++
...yloadSignatureValidationCredentialResolver.java | 82 +++++++++++++
...aultSubordinateStatementCredentialResolver.java | 121 ++++++++++++++++++++
.../DefaultTrustMarkOwnerCredentialResolver.java | 91 +++++++++++++++
.../DefaultTrustMarkStatusCredentialResolver.java | 102 +++++++++++++++++
...ticationSignatureSigningParametersResolver.java | 117 +++++++++++++++++++
.../impl/ConstraintsSyntaxClaimsValidator.java | 100 ++++++++++++++++
.../jwt/claims/impl/CritClaimsValidator.java | 97 ++++++++++++++++
...faultMetadataPolicyOperatorsLookupStrategy.java | 103 +++++++++++++++++
.../impl/MetadataPolicyCritClaimsValidator.java | 105 +++++++++++++++++
.../impl/NonEmptyStringArrayClaimsValidator.java | 88 ++++++++++++++
.../impl/TrustMarkOwnersClaimsValidator.java | 81 +++++++++++++
.../jwt/claims/impl/TrustMarksClaimsValidator.java | 109 ++++++++++++++++++
18 files changed, 1711 insertions(+)
diff --git a/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java
new file mode 100644
index 0000000..42584b8
--- /dev/null
+++ b/oidfed-common-api/src/main/java/net/shibboleth/oidfed/support/ClientInformationExtensionSupport.java
@@ -0,0 +1,95 @@
+/*
+ * 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.oidfed.support;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * Helper methods for our client information extensions related to OpenID federation.
+ */
+public class ClientInformationExtensionSupport {
+
+ /** Identifier for validated trust anchor within client information. */
+ @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_ANCHOR = "oidfed_validated_trust_anchor";
+
+ /** Identifier for validated trust chain within client information. */
+ @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_CHAIN = "oidfed_validated_trust_chain";
+
+ /** Identifier for validated trust mark IDs within client information. */
+ @Nonnull @NotEmpty public static final String KEY_VALIDATED_TRUST_MARK_IDS = "oidfed_validated_trust_mark_ids";
+
+ /**
+ * Parse validated trust anchor from the given client information.
+ *
+ * @param clientInformation client information
+ * @return validated trust anchor
+ */
+ @Nullable
+ public static String parseValidatedTrustAnchor(@Nonnull final OIDCClientInformation clientInformation) {
+ return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_ANCHOR))
+ .filter(String.class::isInstance)
+ .map(obj -> obj.toString())
+ .orElse(null);
+ }
+
+ /**
+ * Parse validated trust chain from the given client information.
+ *
+ * @param clientInformation client information
+ * @return validated trust chain
+ */
+ @Nullable
+ public static List<String> parseValidatedTrustChain(@Nonnull final OIDCClientInformation clientInformation) {
+ return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_CHAIN))
+ .filter(List.class::isInstance)
+ .map(obj -> (List<?>) obj)
+ .map(list -> list.stream().map(String.class::cast).toList())
+ .orElse(null);
+ }
+
+ /**
+ * Parse validated trust mark IDs from the given client information..
+ *
+ * @param clientInformation client information
+ * @return validated trust mark IDs
+ */
+ @Nullable public static Map<String, List<String>> parseValidatedTrustMarkIds(
+ @Nonnull final OIDCClientInformation clientInformation) {
+ return Optional.ofNullable(clientInformation.getOIDCMetadata().getCustomField(KEY_VALIDATED_TRUST_MARK_IDS))
+ .filter(Map.class::isInstance)
+ .map(obj -> (Map<?,?>) obj)
+ .map(map -> map.entrySet().stream()
+ .filter(entry -> entry.getKey() instanceof String)
+ .filter(entry -> entry.getValue() instanceof List<?>)
+ .collect(Collectors.toMap(entry -> entry.getKey().toString(),
+ entry -> ((List<?>) entry.getValue()).stream()
+ .filter(Objects::nonNull)
+ .map(Objects::toString)
+ .toList())))
+ .orElse(null);
+ }
+
+}
diff --git a/oidfed-common-impl/pom.xml b/oidfed-common-impl/pom.xml
index 2558cd5..008ee7a 100644
--- a/oidfed-common-impl/pom.xml
+++ b/oidfed-common-impl/pom.xml
@@ -80,6 +80,16 @@
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-security-api</artifactId>
<scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-security-impl</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-xmlsec-impl</artifactId>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>${opensaml.groupId}</groupId>
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
new file mode 100644
index 0000000..0c1fcdc
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/ClientInformationFederationEntityCredentialResolver.java
@@ -0,0 +1,127 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.oidc.security.credential.JOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.ClientInformationCriterion;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
+import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A {@link JOSEObjectCredentialResolver} that resolves credentials from the entity configuration payload. The entity
+ * configuration is fetched via client custom claim
+ * {@link ClientInformationExtensionSupport#KEY_VALIDATED_TRUST_CHAIN}.
+ */
+public class ClientInformationFederationEntityCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(ClientInformationFederationEntityCredentialResolver.class);
+
+ /** Resolver for fetching federation entity credentials from entity configuration. */
+ @Nonnull private final JOSEObjectCredentialResolver entityConfigurationCredentialResolver;
+
+ /** Object mapper used for deserializing jwks from the entity configuration payload. */
+ @Nonnull private final ObjectMapper objectMapper;
+
+ /**
+ * Constructor.
+ *
+ * @param resolver The resolver for fetching federation entity credentials from entity configuration.
+ * @param mapper The object mapper used for deserializing jwks from the entity configuration payload.
+ */
+ public ClientInformationFederationEntityCredentialResolver(@Nonnull
+ @ParameterName(name="entityConfigurationCredentialResolver") final JOSEObjectCredentialResolver resolver,
+ @Nonnull @ParameterName(name="objectMapper") final ObjectMapper mapper) {
+ entityConfigurationCredentialResolver = Constraint.isNotNull(resolver,
+ "EntityConfigurationCredentialResolver cannot be null");
+ objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet)
+ throws ResolverException {
+
+ Constraint.isNotNull(criteriaSet, "CriteriaSet was null");
+
+ if (criteriaSet != null) {
+ final ClientInformationCriterion clientCrit = criteriaSet.get(ClientInformationCriterion.class);
+ if (clientCrit != null) {
+ return resolveFromMetadata(criteriaSet, clientCrit.getOidcClientInformation());
+ }
+ }
+
+ log.debug("Criteria did not contain a ClientInformationCriterion could not perform resolution");
+ return CollectionSupport.emptySet();
+ }
+
+ /**
+ * Resolve the keyset from the entity configuration payload.
+ *
+ * @param criteriaSet the criteria set
+ * @param information the RP/Client information
+ *
+ * @return a collection of credentials from the entity configuration key set (if any).
+ */
+ @Nonnull protected Iterable<Credential> resolveFromMetadata(@Nonnull final CriteriaSet criteriaSet,
+ @Nonnull final OIDCClientInformation information) throws ResolverException {
+
+ final OIDCClientMetadata metadata = information.getOIDCMetadata();
+
+ if (metadata.getCustomField(ClientInformationExtensionSupport.KEY_VALIDATED_TRUST_CHAIN)
+ instanceof List<?> list) {
+ final List<String> serialized =
+ list.stream().filter(String.class::isInstance).map(String.class::cast).toList();
+ assert serialized != null;
+ final List<EntityStatement<?>> trustChain =
+ EntityStatementHelper.deserializeTrustChain(serialized, objectMapper);
+ if (trustChain != null) {
+ final EntityStatement<?> configuration = trustChain.get(0);
+ assert configuration != null;
+ final SubjectEntityStatementCriterion configurationCriterion =
+ new SubjectEntityStatementCriterion(configuration);
+ log.debug("Returning credentials resolved via entity configuration credential resolver");
+ return entityConfigurationCredentialResolver.resolve(new CriteriaSet(configurationCriterion));
+ }
+ } else {
+ log.debug("Could not find the validated trust chain from the client metadata");
+ }
+ log.trace("Returning empty set of credentials");
+ return CollectionSupport.emptySet();
+ }
+
+}
\ No newline at end of file
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
new file mode 100644
index 0000000..8ef9825
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultEntityConfigurationCredentialResolver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity configuration) payload. The JWT is fetched
+ * via {@link SubjectEntityStatementCriterion}. If the JWT is not self-signed (i.e. it's a subordinate statement), a
+ * {@link ResolverException} is thrown.
+ */
+public class DefaultEntityConfigurationCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultEntityConfigurationCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria set supplied");
+ }
+
+ final List<Credential> result = parseJwkSet(criteriaSet).getKeys().stream()
+ .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Parses the JWKSet from the given criteria set.
+ *
+ * @param criteriaSet criteria set containing source JWT for the JWKSet
+ * @return the JWKSet parsed from the JWT payload
+ * @throws ResolverException if the JWKSet could not be parsed or found
+ */
+ @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+ final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+ if (subjectCriterion == null) {
+ log.debug("No mandatory SubjectEntityStatementCriterion criteria supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
+ }
+ final JWKSet jwks;
+ final EntityStatement<?> subjectStatement = subjectCriterion.getValue();
+ if (subjectStatement.getSubject().equals(
+ subjectStatement.getIssuer())) {
+ jwks = subjectStatement.getParsedPayload().getJwks();
+ } else {
+ throw new ResolverException(
+ "Unexpected contents in the SubjectEntityStatementCriterion: subject does not match issuer");
+ }
+
+ if (jwks == null || jwks.isEmpty()) {
+ throw new ResolverException("Could not parse mandatory jwks");
+ }
+ return jwks;
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
new file mode 100644
index 0000000..fd7108a
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultLocalTrustAnchorCredentialResolver.java
@@ -0,0 +1,109 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.local.LocalKeyContainer;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Default resolver for trusted trust anchor key resolution. A configurable {@link MetadataCache} is used for fetching
+ * the trusted/local public credentials for the entity referred via {@link SubjectEntityStatementCriterion}.
+ */
+public class DefaultLocalTrustAnchorCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultLocalTrustAnchorCredentialResolver.class);
+
+ /** Cache containing local copies of trusted trust anchor keys. */
+ @Nonnull private MetadataCache<Map<String, LocalKeyContainer>> localTrustAnchorsCache;
+
+ /**
+ * Constructor.
+ *
+ * @param cache cache containing local copies of trusted trust anchor keys
+ */
+ public DefaultLocalTrustAnchorCredentialResolver(
+ @Nonnull @ParameterName(name="cache") final MetadataCache<Map<String, LocalKeyContainer>> cache) {
+ localTrustAnchorsCache = Constraint.isNotNull(cache, "Local Trust Anchors cache cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria supplied");
+ }
+ final SubjectEntityStatementCriterion subjectCriterion = criteriaSet.get(SubjectEntityStatementCriterion.class);
+ if (subjectCriterion == null) {
+ log.debug("No SubjectEntityStatementCriterion criteria supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of SubjectEntityStatementCriterion");
+ }
+ final String entityId = subjectCriterion.getValue().getIssuer();
+ log.debug("Attempting to find trusted keys for {}", entityId);
+
+ final List<Map<String, LocalKeyContainer>> keyContainers;
+ try {
+ keyContainers = localTrustAnchorsCache.get(criteriaSet);
+ } catch (final MetadataCacheException e) {
+ throw new ResolverException("Could not resolve local trust anchor keys from the cache", e);
+ }
+ if (keyContainers.isEmpty() || !keyContainers.get(0).containsKey(entityId)) {
+ log.debug("No keys found for {}", entityId);
+ return CollectionSupport.emptyList();
+ }
+ final LocalKeyContainer keyContainer = keyContainers.get(0).get(entityId);
+ if (keyContainer == null || keyContainer.getJWKSet() == null) {
+ log.debug("No JWKSet found for {}", entityId);
+ return CollectionSupport.emptyList();
+ }
+ final JWKSet jwkSet = keyContainer.getJWKSet();
+ final List<Credential> credentials = new ArrayList<>();
+ assert jwkSet != null;
+ for (final JWK jwk : jwkSet.getKeys()) {
+ if (jwk != null) {
+ final Credential cred = buildJWKCredential(jwk, null);
+ if (cred != null) {
+ credentials.add(cred);
+ }
+ }
+ }
+ log.debug("Returning credentials {} for {}", credentials, entityId);
+ return credentials;
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
new file mode 100644
index 0000000..b6d234d
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadJOSEObjectCredentialResolver.java
@@ -0,0 +1,84 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEObject;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidc.security.jose.criterion.JOSEObjectCriterion;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link JOSEObjectCriterion}.
+ */
+public class DefaultPayloadJOSEObjectCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultPayloadJOSEObjectCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null || !criteriaSet.contains(JOSEObjectCriterion.class)) {
+ throw new ResolverException("CriteriaSet does not contain JOSEObjectCriterion");
+ }
+
+ final JOSEObjectCriterion joseObjectCriteria = criteriaSet.get(JOSEObjectCriterion.class);
+ assert joseObjectCriteria != null;
+ final JOSEObject joseObject = joseObjectCriteria.getJOSEObject();
+ if (joseObject == null) {
+ throw new ResolverException("JOSEObjectCriterion did not contain an instance of JOSEObject");
+ }
+ try {
+ final SignedJWT jwt = SignedJWT.parse(joseObject.serialize());
+ final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+ if (rawJwks == null || rawJwks.isEmpty()) {
+ log.debug("No jwks found from the payload");
+ return CollectionSupport.emptyList();
+ }
+ final JWKSet jwks = JWKSet.parse(rawJwks);
+ final List<Credential> result = jwks.getKeys().stream()
+ .filter(Objects::nonNull)
+ .filter(jwk -> jwk.getAlgorithm() != null ?
+ jwk.getAlgorithm().equals(jwt.getHeader().getAlgorithm()) : true)
+ .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ assert result != null;
+ return result;
+ } catch (final ParseException e) {
+ throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+ }
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
new file mode 100644
index 0000000..af34685
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultPayloadSignatureValidationCredentialResolver.java
@@ -0,0 +1,82 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SignatureValidationKeyContainerJwtCriterion;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (entity statement) payload. The JWT is fetched
+ * via {@link SignatureValidationKeyContainerJwtCriterion}.
+ */
+public class DefaultPayloadSignatureValidationCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(DefaultPayloadSignatureValidationCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null || !criteriaSet.contains(SignatureValidationKeyContainerJwtCriterion.class)) {
+ throw new ResolverException("CriteriaSet does not contain SignatureValidationKeyContainerJwtCriterion");
+ }
+
+ final SignatureValidationKeyContainerJwtCriterion keyContainer =
+ criteriaSet.get(SignatureValidationKeyContainerJwtCriterion.class);
+ assert keyContainer != null;
+ final SignedJWT jwt = keyContainer.getJwt();
+ if (jwt == null) {
+ throw new ResolverException(
+ "SignatureValidationKeyContainerJwtCriterion did not contain an instance of SignedJWT");
+ }
+ try {
+ final Map<String, Object> rawJwks = jwt.getJWTClaimsSet().getJSONObjectClaim("jwks");
+ if (rawJwks == null || rawJwks.isEmpty()) {
+ log.debug("No jwks found from the payload");
+ return CollectionSupport.emptyList();
+ }
+ final JWKSet jwks = JWKSet.parse(rawJwks);
+ final List<Credential> result = jwks.getKeys().stream()
+ .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ assert result != null;
+ return result;
+ } catch (final ParseException e) {
+ throw new ResolverException("Could not parse JWKSet from JOSEObject", e);
+ }
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
new file mode 100644
index 0000000..bb1ee2c
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultSubordinateStatementCredentialResolver.java
@@ -0,0 +1,121 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectStatementCriterion;
+import net.shibboleth.oidfed.metadata.payload.BaseExpirableSubjectPayload;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (issuer of a subordinate statement) payload.
+ * First, a JWT is fetched via {@link SubjectEntityStatementCriterion}. Its issuer must match with the entity
+ * statement fetched via {@link IssuerEntityStatementCriterion}. The issuer must be a self-signed statement.
+ */
+public class DefaultSubordinateStatementCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultSubordinateStatementCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria set supplied");
+ }
+
+ final List<Credential> result = parseJwkSet(criteriaSet).getKeys().stream()
+ .filter(Objects::nonNull)
+ .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Parses the JWKSet from the given criteria set.
+ *
+ * @param criteriaSet criteria set containing source JWT for the JWKSet
+ * @return the JWKSet parsed from the JWT payload
+ * @throws ResolverException if the JWKSet could not be parsed or found
+ */
+ @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+ final BaseExpirableSubjectPayload subjectPayload = getSubjectStatementPayload(criteriaSet);
+ if (subjectPayload == null) {
+ log.debug("No mandatory criteria supplied for resolving subject, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain criterion to resolve subject");
+ }
+ final JWKSet jwks;
+ if (subjectPayload.getSubject().equals(subjectPayload.getIssuer())) {
+ throw new ResolverException(
+ "Unexpected contents in the subject statement: subject matches issuer");
+ } else {
+ final IssuerEntityStatementCriterion issuerCriterion =
+ criteriaSet.get(IssuerEntityStatementCriterion.class);
+ if (issuerCriterion == null) {
+ log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+ }
+ final EntityStatement<?> issuerStatement = issuerCriterion.getValue();
+ if (!issuerStatement.getSubject().equals(subjectPayload.getIssuer())) {
+ throw new ResolverException("Credential criteria do not match for subject and issuer");
+ }
+ if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
+ throw new ResolverException("Issuer entity statement is not self signed");
+ }
+ jwks = issuerStatement.getParsedPayload().getJwks();
+ }
+
+ if (jwks == null || jwks.isEmpty()) {
+ throw new ResolverException("Could not parse mandatory jwks");
+ }
+ return jwks;
+ }
+
+ private BaseExpirableSubjectPayload getSubjectStatementPayload(
+ @Nonnull final CriteriaSet criteriaSet) {
+ final SubjectEntityStatementCriterion entityStatementCriterion =
+ criteriaSet.get(SubjectEntityStatementCriterion.class);
+ if (entityStatementCriterion == null) {
+ final SubjectStatementCriterion subjectCriterion = criteriaSet.get(SubjectStatementCriterion.class);
+ if (subjectCriterion == null) {
+ return null;
+ }
+ return subjectCriterion.getValue().getParsedPayload() instanceof BaseExpirableSubjectPayload payload
+ ? payload : null;
+ }
+ return entityStatementCriterion.getValue().getParsedPayload();
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
new file mode 100644
index 0000000..cb21704
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkOwnerCredentialResolver.java
@@ -0,0 +1,91 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.cache.SubjectEntityIDCriterion;
+import net.shibboleth.oidfed.metadata.cache.TrustMarkOwnersCriterion;
+import net.shibboleth.oidfed.metadata.payload.claim.TrustMarkOwner;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Default resolver for trust anchor owner key resolution. A {@link TrustMarkOwnersCriterion} is used for fetching the
+ * credentials for the trust mark owner fetched via {@link SubjectEntityIDCriterion}.
+ */
+public class DefaultTrustMarkOwnerCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTrustMarkOwnerCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria supplied");
+ }
+ final TrustMarkOwnersCriterion ownersCriterion = criteriaSet.get(TrustMarkOwnersCriterion.class);
+ if (ownersCriterion == null) {
+ log.debug("No TrustMarkOwnersCriterion criteria supplised, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of TrustMarkOwnersCriterion");
+ }
+ final SubjectEntityIDCriterion subjectCriterion = criteriaSet.get(SubjectEntityIDCriterion.class);
+ if (subjectCriterion == null) {
+ log.debug("No SubjectEntityIDCriterion criteria supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of SubjectEntityIDCriterion");
+ }
+ final String entityId = subjectCriterion.getValue();
+ final Map<String, TrustMarkOwner> owners = ownersCriterion.getValue();
+ if (owners.isEmpty() || owners.get(entityId) == null) {
+ log.debug("No trusted owners entry found for {}", entityId);
+ return CollectionSupport.emptyList();
+ }
+ final TrustMarkOwner ownerConfiguration = owners.get(entityId);
+ final JWKSet jwkSet = ownerConfiguration.getJwks();
+ if (jwkSet != null && !jwkSet.isEmpty()) {
+ final List<Credential> credentials = new ArrayList<>();
+ for (final JWK jwk : jwkSet.getKeys()) {
+ if (jwk != null) {
+ final Credential cred = buildJWKCredential(jwk, null);
+ if (cred != null) {
+ credentials.add(cred);
+ }
+ }
+ }
+ log.debug("Returning credentials {} for {}", credentials, entityId);
+ return credentials;
+ }
+ log.debug("Could not find jwks from {}", ownerConfiguration);
+ return CollectionSupport.emptyList();
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
new file mode 100644
index 0000000..a6509a2
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/credential/DefaultTrustMarkStatusCredentialResolver.java
@@ -0,0 +1,102 @@
+/*
+ * 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.oidfed.security.credential;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.oidfed.metadata.BasePayload;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.IssuerEntityStatementCriterion;
+import net.shibboleth.oidfed.metadata.cache.SubjectStatementCriterion;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Resolves credentials from the jwks-claim located in the signed JWT (issuer of a trust mark status) payload.
+ * First, a JWT is fetched via {@link SubjectStatementCriterion}. Its issuer must match with the entity
+ * statement fetched via {@link IssuerEntityStatementCriterion}.
+ */
+public class DefaultTrustMarkStatusCredentialResolver extends BasicJOSEObjectCredentialResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTrustMarkStatusCredentialResolver.class);
+
+ /** {@inheritDoc} */
+ @Override
+ protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+ if (criteriaSet == null) {
+ throw new ResolverException("No criteria set supplied");
+ }
+
+ final List<Credential> result = parseJwkSet(criteriaSet).getKeys().stream()
+ .filter(Objects::nonNull)
+ .map(jwk -> jwk != null ? buildJWKCredential(jwk, null) : null)
+ .filter(Objects::nonNull)
+ .map(Credential.class::cast)
+ .toList();
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Parses the JWKSet from the given criteria set.
+ *
+ * @param criteriaSet criteria set containing source JWT for the JWKSet
+ * @return the JWKSet parsed from the JWT payload
+ * @throws ResolverException if the JWKSet could not be parsed or found
+ */
+ @Nonnull protected JWKSet parseJwkSet(@Nonnull final CriteriaSet criteriaSet) throws ResolverException {
+ final BasePayload subjectPayload = Optional.ofNullable(criteriaSet.get(SubjectStatementCriterion.class))
+ .map(criterion -> criterion.getValue().getParsedPayload()).orElse(null);
+ if (subjectPayload == null) {
+ log.debug("No mandatory criteria supplied for resolving subject, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain criterion to resolve subject");
+ }
+ final IssuerEntityStatementCriterion issuerCriterion =
+ criteriaSet.get(IssuerEntityStatementCriterion.class);
+ if (issuerCriterion == null) {
+ log.debug("No mandatory IssuerEntityStatementCriterion supplied, resolver could not process");
+ throw new ResolverException(
+ "Credential criteria set did not contain an instance of IssuerEntityStatementCriterion");
+ }
+ final EntityStatement<?> issuerStatement = issuerCriterion.getValue();
+ if (!issuerStatement.getSubject().equals(subjectPayload.getIssuer())) {
+ throw new ResolverException("Credential criteria do not match for subject and issuer");
+ }
+ if (!issuerStatement.getSubject().equals(issuerStatement.getIssuer())) {
+ throw new ResolverException("Issuer entity statement is not self signed");
+ }
+ final JWKSet jwks = issuerStatement.getParsedPayload().getJwks();
+
+ if (jwks == null || jwks.isEmpty()) {
+ throw new ResolverException("Could not parse mandatory jwks");
+ }
+ return jwks;
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java
new file mode 100644
index 0000000..5785158
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jose/impl/EndpointAuthenticationSignatureSigningParametersResolver.java
@@ -0,0 +1,117 @@
+/*
+ * 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.oidfed.security.jose.impl;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.security.jose.SignatureSigningParametersResolver;
+import net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningParametersResolver;
+import net.shibboleth.oidfed.metadata.EntityStatement;
+import net.shibboleth.oidfed.metadata.cache.FederationEndpointEntityStatementCriterion;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * An implementation of an {@link SignatureSigningParametersResolver} that extends the {@link
+ * BasicSignatureSigningParametersResolver} functionality by adding a configurable lookup strategy for fetching
+ * the desired algorithm value from {@link EntityStatement}. It is expected to be found from the criteria set.
+ */
+public class EndpointAuthenticationSignatureSigningParametersResolver extends BasicSignatureSigningParametersResolver
+ implements SignatureSigningParametersResolver {
+
+ /** Class logger.*/
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(EndpointAuthenticationSignatureSigningParametersResolver.class);
+
+ /** The default algorithm value used if lookup strategy returned null. */
+ @Nullable private final String defaultAlgorithmValue;
+
+ /**
+ * Constructor.
+ *
+ * @param defaultValue the default algorithm value used if lookup strategy returned null
+ */
+ public EndpointAuthenticationSignatureSigningParametersResolver(
+ @Nullable @ParameterName(name = "defaultAlgorithmValue") final String defaultValue) {
+ defaultAlgorithmValue = defaultValue;
+ }
+
+
+ /**
+ * Get the effective list of signature algorithm URIs to consider, including application of
+ * include/exclude policy.
+ *
+ * @param criteria the input criteria being evaluated
+ * @param includeExcludePredicate the include/exclude predicate to use
+ * @return the list of effective algorithm URIs
+ */
+ @Override
+ @Nonnull protected List<String> getEffectiveSignatureAlgorithms(@Nonnull final CriteriaSet criteria,
+ @Nonnull final Predicate<String> includeExcludePredicate) {
+ final List<String> accumulator = super.getEffectiveSignatureAlgorithms(criteria, includeExcludePredicate);
+ final List<String> algorithms =
+ Optional.ofNullable(criteria.get(FederationEndpointEntityStatementCriterion.class))
+ .map(criterion -> criterion.getValue())
+ .map(statement -> statement.getParsedPayload().getMetadata())
+ .map(metadata -> metadata.getFederationEntityMetadata())
+ .map(entityMetadata -> entityMetadata.get("endpoint_auth_signing_alg_values_supported"))
+ .filter(algs -> algs instanceof List<?>)
+ .map(algs -> (List<?>) algs)
+ .map(algs -> algs.stream().filter(String.class::isInstance).map(String.class::cast).toList())
+ .orElse(null);
+ log.trace("Resolved algorithms via criteria set: {}", algorithms);
+ if (algorithms == null || algorithms.isEmpty()) {
+ if (StringSupport.trimOrNull(defaultAlgorithmValue) != null) {
+ log.debug("No algorithms resolved via criteria set, using default");
+ assert defaultAlgorithmValue != null;
+ return convertIntoListIfEnabled(defaultAlgorithmValue, accumulator);
+ }
+ log.error("No algorithms resolved via criteria set");
+ return CollectionSupport.emptyList();
+ }
+
+ final List<String> result = algorithms.stream().filter(algorithm -> accumulator.contains(algorithm)).toList();
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Returns the given algorithm in a {@link List} if it was enabled in the list of enabled algorithms. An empty
+ * list is returned if the algorithm was not enabled.
+ *
+ * @param algorithm the algorithm to be checked against the list
+ * @param enabledAlgorithms the list of enabled algorithms
+ * @return the given algorithm as list if it was enabled, or an empty list if not
+ */
+ @Nonnull protected List<String> convertIntoListIfEnabled(@Nonnull final String algorithm,
+ @Nonnull final List<String> enabledAlgorithms) {
+ if (enabledAlgorithms.contains(algorithm)) {
+ return CollectionSupport.listOf(algorithm);
+ } else {
+ log.warn("The algorithm {} is not enabled, returning empty list", algorithm);
+ return CollectionSupport.emptyList();
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java
new file mode 100644
index 0000000..f24fe88
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/ConstraintsSyntaxClaimsValidator.java
@@ -0,0 +1,100 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+
+/**
+ * A {@link ClaimsValidator} for validating the syntax for standard constraints.
+ */
+public class ConstraintsSyntaxClaimsValidator extends AbstractClaimsValidator {
+
+ /** Generic prefix to be used with the {@link JWTValidationException}. */
+ public static final String ERROR_PREFIX = "Unexpected contents for constraints: ";
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+ try {
+ final Map<String, Object> constraints = claims.getJSONObjectClaim("constraints");
+ if (constraints == null || constraints.isEmpty()) {
+ return;
+ }
+ if (constraints.containsKey("max_path_length")) {
+ if (!(constraints.get("max_path_length") instanceof Number)) {
+ throw new JWTValidationException(ERROR_PREFIX + "max_path_length is not a number");
+ }
+ }
+ if (constraints.containsKey("naming_constraints")) {
+ verifyNamingConstraints(constraints.get("naming_constraints"));
+ }
+ if (constraints.containsKey("allowed_entity_types")) {
+ if (constraints.get("allowed_entity_types") instanceof List list) {
+ for (final Object item : list) {
+ if (!(item instanceof String)) {
+ throw new JWTValidationException(ERROR_PREFIX
+ + "allowed_entity_types value is not a list of strings");
+ }
+ }
+ } else {
+ throw new JWTValidationException("allowed_entity_types is not a list");
+ }
+ }
+ } catch (final ParseException e) {
+ throw new JWTValidationException(ERROR_PREFIX + "could not parse a map", e);
+ }
+ }
+
+ /**
+ * Verifies the 'naming_constraints' syntax.
+ *
+ * @param value the naming_constraints value
+ * @throws JWTValidationException if the value syntax is invalid
+ */
+ protected void verifyNamingConstraints(@Nullable final Object value) throws JWTValidationException {
+ final String errorPrefix = ERROR_PREFIX + "naming_constraints ";
+ if (value instanceof Map<?,?> map) {
+ for (final Object key : map.keySet()) {
+ if (!(key instanceof String)) {
+ throw new JWTValidationException(errorPrefix + "key is not a string");
+ }
+ if (map.get(key) instanceof List<?> list) {
+ for (final Object item : list) {
+ if (!(item instanceof String)) {
+ throw new JWTValidationException(errorPrefix + "value is not a list of strings");
+ }
+ }
+ } else {
+ throw new JWTValidationException(errorPrefix + "value is not a list");
+ }
+ }
+ } else {
+ throw new JWTValidationException(errorPrefix + "is not a map");
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.java
new file mode 100644
index 0000000..a7b84e7
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/CritClaimsValidator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * A {@link ClaimsValidator} for validating that crit is not an empty array, does not contain standard claim names and
+ * only contain values that are configured as recognized.
+ */
+ at ThreadSafeAfterInit
+public class CritClaimsValidator extends AbstractClaimsValidator {
+
+ /** The list of standard operators that cannot be included in the metadata_policy_crit array. */
+ @Nonnull public static final List<String> STANDARD_CLAIMS = CollectionSupport.listOf(
+ "iss", "sub", "iat", "exp", "jwks", "metadata", "crit", "authority_hints", "trust_anchor_hints",
+ "trust_marks", "trust_mark_issuers", "trust_mark_owners", "constraints", "metadata_policy",
+ "metadata_policy_crit", "source_endpoint", "aud", "trust_anchor");
+
+ /** The collection of recognized claims. */
+ @Nonnull private Collection<String> recognizedClaims;
+
+ public CritClaimsValidator() {
+ recognizedClaims = CollectionSupport.emptyList();
+ }
+
+ /**
+ * Set the collection of recognized claims
+ *
+ * @param claims recognized claims
+ */
+ public void setRecognizedClaims(@Nullable final Collection<String> claims) {
+ checkSetterPreconditions();
+ final Collection<String> list =
+ Optional.ofNullable(claims).orElseGet(NonnullSupplier.of(CollectionSupport.emptyList()));
+ assert list != null;
+ recognizedClaims = list;
+ }
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+ final List<String> values;
+ try {
+ values = claims.getStringListClaim("crit");
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Could not parse crit into a list of strings");
+ }
+ if (values != null) {
+ if (values.isEmpty()) {
+ throw new JWTValidationException("Empty array is not allowed for crit");
+ }
+ for (final String value : values) {
+ if (values.indexOf(value) != values.lastIndexOf(value)) {
+ throw new JWTValidationException(
+ "Claim " + value + " is included more than once in crit");
+ }
+ if (STANDARD_CLAIMS.contains(value)) {
+ throw new JWTValidationException(
+ "Claim " + value + " is standard claim name and is not allowed in crit");
+ }
+ if (!recognizedClaims.contains(value)) {
+ throw new JWTValidationException( "Claim " + value + " is not recognized for crit");
+ }
+ }
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.java
new file mode 100644
index 0000000..f483259
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/DefaultMetadataPolicyOperatorsLookupStrategy.java
@@ -0,0 +1,103 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyOperator;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default lookup strategy to resolve list of supported metadata policy operators.
+ */
+public class DefaultMetadataPolicyOperatorsLookupStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultMetadataPolicyOperatorsLookupStrategy.class);
+
+ /** Map of supported operators by inbound message class name. */
+ @NonnullAfterInit private Map<String, List<FederationMetadataPolicyOperator>> supportedOperators;
+
+ /** List of default supported operators if inbound message class name was not mapped. */
+ @NonnullAfterInit private List<FederationMetadataPolicyOperator> defaultSupportedOperators;
+
+ /**
+ * Set the map of supported operators by inbound message class name.
+ *
+ * @param operators map of supported operators
+ */
+ public void setSupportedOperators(@Nonnull final Map<String, List<FederationMetadataPolicyOperator>> operators) {
+ checkSetterPreconditions();
+ supportedOperators = Constraint.isNotNull(operators, "Map of supported operators cannot be null");
+ }
+
+ /**
+ * Set the list of default supported operators if inbound message class name was not mapped.
+ *
+ * @param operators default list of supported operators
+ */
+ public void setDefaultSupportedOperators(@Nonnull List<FederationMetadataPolicyOperator> operators) {
+ checkSetterPreconditions();
+ defaultSupportedOperators =
+ Constraint.isNotNull(operators, "Default list of supported operators cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (supportedOperators == null) {
+ throw new ComponentInitializationException("Map of supported operators cannot be null");
+ }
+ if (defaultSupportedOperators == null) {
+ throw new ComponentInitializationException("Default list of supported operators cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public List<FederationMetadataPolicyOperator> apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ checkComponentActive();
+ if (profileRequestContext == null || profileRequestContext.getInboundMessageContext() == null
+ || profileRequestContext.ensureInboundMessageContext().getMessage() == null) {
+ log.warn("No inbound message resolved, returning default list of supported operators");
+ } else {
+ final Object message = profileRequestContext.ensureInboundMessageContext().getMessage();
+ assert message != null;
+ final String messageClassName = message.getClass().getCanonicalName();
+ if (supportedOperators.containsKey(messageClassName)) {
+ log.debug("Found message {} specific list of supported operators {}", messageClassName,
+ supportedOperators.get(messageClassName));
+ return supportedOperators.get(messageClassName);
+ }
+ log.trace("No messsage class {} specific list found, returning default list of supported operators",
+ messageClassName);
+ }
+ return defaultSupportedOperators;
+ }
+
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java
new file mode 100644
index 0000000..3fe6b59
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/MetadataPolicyCritClaimsValidator.java
@@ -0,0 +1,105 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidfed.metadata.policy.FederationMetadataPolicyOperator;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A {@link ClaimsValidator} for validating that the metadata_policy_crit is not an empty array, does not contain
+ * standard operators and all the values are recognized by the resolved list of supported metadata policy operators.
+ */
+ at ThreadSafeAfterInit
+public class MetadataPolicyCritClaimsValidator extends AbstractClaimsValidator {
+
+ /** The list of standard operators that cannot be included in the metadata_policy_crit array. */
+ public static final List<String> STANDARD_OPERATORS = CollectionSupport.listOf(
+ "value", "add", "default", "one_of", "subset_of", "superset_of", "essential");
+
+ /** The lookup strategy for the list of supported metadata policy operators . */
+ @NonnullAfterInit private
+ Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> supportedOperatorsLookupStrategy;
+
+ /**
+ * Set the lookup strategy for the list of supported metadata policy operators.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSupportedOperatorsLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<FederationMetadataPolicyOperator>> strategy) {
+ checkSetterPreconditions();
+ supportedOperatorsLookupStrategy =
+ Constraint.isNotNull(strategy, "Supported metadata policy operators lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (supportedOperatorsLookupStrategy == null) {
+ throw new ComponentInitializationException(
+ "Supported metadata policy operators lookup strategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+ final List<String> values;
+ try {
+ values = claims.getStringListClaim("metadata_policy_crit");
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Could not parse metadata_policy_crit into a list of strings");
+ }
+ final List<String> supportedValues = Optional.ofNullable(supportedOperatorsLookupStrategy.apply(context))
+ .orElse(CollectionSupport.emptyList())
+ .stream().map(operator -> operator.getOperatorName()).toList();
+ if (values != null) {
+ if (values.isEmpty()) {
+ throw new JWTValidationException("Empty array is not allowed for metadata_policy_crit");
+ }
+ for (final String value : values) {
+ if (values.indexOf(value) != values.lastIndexOf(value)) {
+ throw new JWTValidationException(
+ "Claim " + value + " is included more than once in metadata_policy_crit");
+ }
+ if (STANDARD_OPERATORS.contains(value)) {
+ throw new JWTValidationException(
+ "Claim " + value + " is standard operator and is not allowed in metadata_policy_crit");
+ }
+ if (!supportedValues.contains(value)) {
+ throw new JWTValidationException( "Claim " + value + " is not recognized in metadata_policy_crit");
+ }
+ }
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java
new file mode 100644
index 0000000..e7e3ff9
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/NonEmptyStringArrayClaimsValidator.java
@@ -0,0 +1,88 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A {@link ClaimsValidator} for validating that the configured claims are non-empty string arrays if present.
+ */
+ at ThreadSafeAfterInit
+public class NonEmptyStringArrayClaimsValidator extends AbstractClaimsValidator {
+
+ /** The names of the JWT claims that must be non-empty string arrays if they exist, empty set if none. */
+ @Nonnull @NonnullElements private Set<String> nonEmptyArrayClaims;
+
+ /**
+ * Constructor.
+ */
+ public NonEmptyStringArrayClaimsValidator() {
+ nonEmptyArrayClaims = CollectionSupport.emptySet();
+ }
+
+ /**
+ * Set the non-empty string array claims.
+ *
+ * @param claims the non-empty string array claims.
+ */
+ public void setNonEmptyArrayClaims(@Nullable final Collection<String> claims) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ if (claims !=null) {
+ nonEmptyArrayClaims = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(claims));
+ } else {
+ nonEmptyArrayClaims = CollectionSupport.emptySet();
+ }
+ }
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+
+ for (final String claim : nonEmptyArrayClaims) {
+ if (claims.getClaims().containsKey(claim)) {
+ final List<String> values;
+ try {
+ values = claims.getStringListClaim(claim);
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Could not parse " + claim + " into a list of strings");
+ }
+ if (values != null) {
+ if (values.isEmpty()) {
+ throw new JWTValidationException("Empty array is not allowed for " + claim);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java
new file mode 100644
index 0000000..51ab58c
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarkOwnersClaimsValidator.java
@@ -0,0 +1,81 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+
+/**
+ * A {@link ClaimsValidator} for validating the syntax of the optional trust_mark_owners claim. The value must be a
+ * String-keyed map of maps with mandatory sub and jwks claims.
+ */
+ at ThreadSafeAfterInit
+public class TrustMarkOwnersClaimsValidator extends AbstractClaimsValidator {
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+ try {
+ final Map<String,Object> trustMarkOwners = claims.getJSONObjectClaim("trust_mark_owners");
+ if (trustMarkOwners != null) {
+ for (final Object key : trustMarkOwners.keySet()) {
+ if (key instanceof String string) {
+ final Object raw = trustMarkOwners.get(string);
+ if (raw instanceof Map<?, ?> map) {
+ final Map<String, Object> trustMarkOwner = map.keySet().stream()
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .filter(k -> map.get(k) != null)
+ .collect(Collectors.toMap(k -> k, k -> map.get(k)));
+ if (trustMarkOwner.get("sub") instanceof String subString) {
+ if (subString.isEmpty()) {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+ + "Subject is empty");
+ }
+ } else {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+ + "Subject is not a string");
+ }
+ if (trustMarkOwner.get("jwks") == null) {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+ + "Value for 'jwks' is missing");
+ }
+ } else {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+ + "Value is not a map");
+ }
+ } else {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners item: "
+ + "Key is not a string");
+ }
+ }
+ }
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Unexpected contents for trust_mark_owners: could not parse a map", e);
+ }
+ }
+}
diff --git a/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java
new file mode 100644
index 0000000..89f1251
--- /dev/null
+++ b/oidfed-common-impl/src/main/java/net/shibboleth/oidfed/security/jwt/claims/impl/TrustMarksClaimsValidator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.oidfed.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+
+/**
+ * A {@link ClaimsValidator} for validating the syntax of the optional trust_marks claim. Each item ie the array must
+ * contain a match between the trust_mark_type claim and the corresponding claim inside the trust_mark JWT payload.
+ */
+ at ThreadSafeAfterInit
+public class TrustMarksClaimsValidator extends AbstractClaimsValidator {
+
+ /** {@inheritDoc} */
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nonnull final ProfileRequestContext context) throws JWTValidationException {
+ try {
+ final List<Object> trustMarks = claims.getListClaim("trust_marks");
+ if (trustMarks != null) {
+ for (final Object raw : trustMarks) {
+ if (raw instanceof Map<?, ?> map) {
+ final Map<String, String> trustMark = map.keySet().stream()
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .filter(key -> map.get(key) instanceof String)
+ .collect(Collectors.toMap(key -> key, key -> (String) map.get(key)));
+ if (trustMark.isEmpty()) {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "Could not parse a map of Strings");
+ }
+ validateContent(trustMark);
+ } else {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "Could not parse a map of strings");
+ }
+ }
+ }
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Unexpected contents for trust_marks: could not parse an array", e);
+ }
+ }
+
+ /**
+ * Verifies that the given trust mark item meets the syntax requirements: the trust_mark content must be a signed
+ * JWT with a matching value for the trust_mark_type claim.
+ *
+ * @param trustMark trust mark JSON object as a map
+ * @throws JWTValidationException if the syntax validation fails
+ */
+ protected void validateContent(@Nonnull final Map<String, String> trustMark) throws JWTValidationException {
+ final String trustMarkType = trustMark.get("trust_mark_type");
+ if (trustMarkType == null) {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "trust_mark_type is null");
+ }
+ final SignedJWT trustMarkJwt = Optional.ofNullable(trustMark.get("trust_mark"))
+ .map(value -> {
+ try {
+ return SignedJWT.parse(value);
+ } catch (final ParseException e) {
+ return null;
+ }
+ })
+ .orElse(null);
+ if (trustMarkJwt == null) {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "trust_mark cannot be parsed into JWT");
+ }
+ try {
+ final String trustMarkTypeJwt = trustMarkJwt.getJWTClaimsSet().getStringClaim("trust_mark_type");
+ if (!trustMarkType.equals(trustMarkTypeJwt)) {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "trust_mark_type " + trustMarkType + " does not match the JWT claim " + trustMarkTypeJwt);
+ }
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Unexpected contents for trust_marks array item: "
+ + "Could not parse trust_mark_type from JWT for trust_mark_type " + trustMarkType, e);
+ }
+
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list