[java-oidc-common] 10/20: Move in Token request encoders and response decoders and other functions
Codeberg
noreply at shibboleth.net
Tue Feb 17 20:14:45 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/JCOMOIDC-139
in repository java-oidc-common.
View the commit online:
https://codeberg.org/Shibboleth/java-oidc-common/commit/27cb0538d5e4b46fab6a7d7bbb36a4946097d78a
commit 27cb0538d5e4b46fab6a7d7bbb36a4946097d78a
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Nov 7 14:33:25 2025 +0000
Move in Token request encoders and response decoders and other functions
- Move in token request encoders and response decoders
- Move over more lookup functions
- Move over some flow actions common with the RP Proxy
---
...tAuthenticationConfigurationLookupFunction.java | 191 ++++++++++++++++++++
...viderMetadataStringListValueLookupFunction.java | 83 +++++++++
oidc-common-profile-impl/pom.xml | 21 ++-
.../impl/AbstractJSONResponseDecoderFunction.java | 70 ++++++++
.../decoding/impl/AccessTokenResponseDecoder.java | 103 +++++++++++
.../impl/AbstractRequestEncoderFunction.java | 195 +++++++++++++++++++++
.../encoding/impl/AuthCodeTokenRequestEncoder.java | 113 ++++++++++++
...nitializeOAuth2ClientAuthenticationContext.java | 94 ++++++++++
8 files changed, 869 insertions(+), 1 deletion(-)
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jose/impl/ClientAuthenticationConfigurationLookupFunction.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jose/impl/ClientAuthenticationConfigurationLookupFunction.java
new file mode 100644
index 00000000..4151023b
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jose/impl/ClientAuthenticationConfigurationLookupFunction.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.oidc.security.jose.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.opensaml.security.config.SecurityConfiguration;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+
+import net.shibboleth.oidc.profile.config.JSONSecurityConfiguration;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A function that returns a {@link SignatureSigningConfiguration} list for signing client authentication JWTs.
+ *
+ * <p>The configuration list is taken from the active security configuration, but the algorithms are filtered to only
+ * allow those that are compatible with the client authentication type chosen. For example, the HMAC family
+ * of algorithms if the client_secret_jwt method is used.</p>
+ */
+public class ClientAuthenticationConfigurationLookupFunction
+ implements ContextDataLookupFunction<MessageContext, List<SignatureSigningConfiguration>> {
+
+ /** Lookup function for parent ProfileRequestContext. */
+ @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP =
+ new ParentProfileRequestContextLookup<>();
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ClientAuthenticationConfigurationLookupFunction.class);
+
+ /**
+ * Strategy used to locate the {@link RelyingPartyContext} associated with a
+ * given {@link ProfileRequestContext}.
+ */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+
+ /** Constructor. */
+ public ClientAuthenticationConfigurationLookupFunction() {
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ }
+
+ /**
+ * Set the strategy used to locate the {@link RelyingPartyContext} associated
+ * with a given {@link ProfileRequestContext}.
+ *
+ * @param strategy
+ * lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+ relyingPartyContextLookupStrategy = Constraint.isNotNull(strategy,
+ "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+ @Override
+ @Nonnull @NonnullElements @NotLive @Unmodifiable
+ public List<SignatureSigningConfiguration> apply(@Nullable final MessageContext input) {
+
+ final List<SignatureSigningConfiguration> configs = new ArrayList<>();
+ String tokenEndpointAuthMethod = null;
+ final RelyingPartyContext rpc = relyingPartyContextLookupStrategy.apply(PRC_LOOKUP.apply(input));
+
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc != null && pc.getSecurityConfiguration(PRC_LOOKUP.apply(input)) instanceof final JSONSecurityConfiguration jsonSecConfig
+ && jsonSecConfig.getJwtSignatureSigningConfiguration() != null) {
+ configs.add(jsonSecConfig.getJwtSignatureSigningConfiguration());
+ }
+ if (pc instanceof final OIDCAuthenticationRelyingPartyProfileConfiguration oidcRpConfig) {
+ tokenEndpointAuthMethod = oidcRpConfig.getTokenEndpointAuthMethod(PRC_LOOKUP.apply(input));
+ }
+ // Check for a per-profile default (relying party independent) config.
+ final RelyingPartyConfiguration rpConfig = rpc.getConfiguration();
+ if (rpConfig != null) {
+ final SecurityConfiguration defaultConfig = rpConfig.getSecurityConfiguration(PRC_LOOKUP.apply(input));
+ if (defaultConfig instanceof final JSONSecurityConfiguration jsonSecConfig
+ && jsonSecConfig.getJwtSignatureSigningConfiguration() != null) {
+ configs.add(jsonSecConfig.getJwtSignatureSigningConfiguration());
+ }
+ }
+ }
+
+ if (tokenEndpointAuthMethod == null) {
+ log.trace("Token endpoint client authentication method can not be found");
+ return CollectionSupport.emptyList();
+ }
+
+ final ClientAuthenticationMethod method = new ClientAuthenticationMethod(tokenEndpointAuthMethod);
+ final List<SignatureSigningConfiguration> configsFiltered = new ArrayList<>();
+
+ // Filter algorithms based on the type of JWT client authentication method chosen.
+ if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+ for (final SignatureSigningConfiguration config : configs) {
+ final List<String> filteredForMethodAlgs = filterAlgorithmsAgainstFamily(JWSAlgorithm.Family.HMAC_SHA,
+ config.getSignatureAlgorithms());
+ configsFiltered.add(createSignatureSigningConfiguration(config, filteredForMethodAlgs));
+ }
+ } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+ for (final SignatureSigningConfiguration config : configs) {
+ final List<String> filteredForMethodAlgs = filterAlgorithmsAgainstFamily(JWSAlgorithm.Family.SIGNATURE,
+ config.getSignatureAlgorithms());
+ configsFiltered.add(createSignatureSigningConfiguration(config, filteredForMethodAlgs));
+ }
+ }
+
+ return Collections.unmodifiableList(configsFiltered);
+ }
+
+ /**
+ * Filter out any algorithms in the list that are not from the given algorithm family.
+ *
+ * @param algFamily the algorithm family to filter on
+ * @param algorithms the algorithms to filter
+ *
+ * @return a filtered list of algorithms
+ */
+ @Nonnull @NonnullElements @NotLive @Unmodifiable private List<String> filterAlgorithmsAgainstFamily(
+ @Nonnull final JWSAlgorithm.Family algFamily, @Nullable final List<String> algorithms) {
+
+ if (algorithms == null) {
+ return CollectionSupport.emptyList();
+ }
+
+ final List<String> filtered = algorithms.stream()
+ .filter(Objects::nonNull).filter(Predicate.not(String::isEmpty)).map(JWSAlgorithm::parse)
+ .filter(algFamily::contains).map(Algorithm::getName).toList();
+ return Collections.unmodifiableList(filtered);
+ }
+
+ /**
+ * Create a copy of the signature signing configuration given, but replace the algorithms with those input.
+ *
+ * @param signingConfig the signature signing configuration to copy
+ * @param algorithms the algorithms to add into the copied configuration
+ *
+ * @return a copied signature signing configuration with the algorithms input
+ */
+ @Nonnull private BasicSignatureSigningConfiguration createSignatureSigningConfiguration(
+ @Nonnull final SignatureSigningConfiguration signingConfig, @Nonnull final List<String> algorithms) {
+ final BasicSignatureSigningConfiguration config = new BasicSignatureSigningConfiguration();
+ config.setExcludedAlgorithms(signingConfig.getExcludedAlgorithms());
+ config.setIncludedAlgorithms(signingConfig.getIncludedAlgorithms());
+ config.setIncludeExcludePrecedence(signingConfig.getIncludeExcludePrecedence());
+ config.setSigningCredentials(signingConfig.getSigningCredentials());
+ config.setIncludeMerge(signingConfig.isIncludeMerge());
+ config.setExcludeMerge(signingConfig.isExcludeMerge());
+ config.setSignatureAlgorithms(algorithms);
+ return config;
+ }
+
+}
diff --git a/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/navigate/ProviderMetadataStringListValueLookupFunction.java b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/navigate/ProviderMetadataStringListValueLookupFunction.java
new file mode 100644
index 00000000..2eb078a1
--- /dev/null
+++ b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/navigate/ProviderMetadataStringListValueLookupFunction.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.profile.config.navigate;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Fetches the value for the configured key as List of {@link String}s. May be {@code null} if the value is not found
+ * or the given {@link OIDCProviderMetadata} is null.
+ */
+public class ProviderMetadataStringListValueLookupFunction implements Function<OIDCProviderMetadata, List<String>> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProviderMetadataStringListValueLookupFunction.class);
+
+ /** The key for which to fetch the value for. */
+ @Nonnull private final String keyName;
+
+ /**
+ * Constructor.
+ *
+ * @param name The key for which to fetch the value for.
+ */
+ public ProviderMetadataStringListValueLookupFunction(@ParameterName(name = "keyName") @Nonnull final String name) {
+ keyName = Constraint.isNotEmpty(name, "The key name cannot be empty");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public List<String> apply(@Nullable final OIDCProviderMetadata metadata) {
+ if (metadata == null) {
+ log.trace("No provider metadata available");
+ return null;
+ }
+ final Object value = metadata.toJSONObject().get(keyName);
+ if (value == null) {
+ log.trace("No value found for the key {}", keyName);
+ return null;
+ }
+ if (value instanceof final String strValue) {
+ return List.of(strValue);
+ }
+ if (value instanceof List) {
+ final List<?> valueAsList = (List<?>)value;
+ return Collections.unmodifiableList(valueAsList.stream()
+ .filter(Objects::nonNull)
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .filter(Predicate.not(String::isEmpty))
+ .toList());
+ }
+
+ return null;
+ }
+
+}
diff --git a/oidc-common-profile-impl/pom.xml b/oidc-common-profile-impl/pom.xml
index f597369e..92443963 100644
--- a/oidc-common-profile-impl/pom.xml
+++ b/oidc-common-profile-impl/pom.xml
@@ -122,7 +122,26 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
-
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>${opensaml.groupId}</groupId>
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AbstractJSONResponseDecoderFunction.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AbstractJSONResponseDecoderFunction.java
new file mode 100644
index 00000000..0cd2fe6c
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AbstractJSONResponseDecoderFunction.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.profile.decoding.impl;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract class for JSON based Http client response decoders.
+ *
+ * <p>Note, any input stream obtained by the decoder MUST ensure the stream is closed.</p>
+ *
+ * @param <T> the return type of the function.
+ */
+public abstract class AbstractJSONResponseDecoderFunction<T> extends AbstractInitializableComponent
+ implements HttpClientResponseHandler<T>{
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /**
+ * Set the JSON Object Mapper to use.
+ *
+ * @param mapper the object mapper.
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+ }
+
+ /**
+ * Get the object mapper.
+ *
+ * @return the object mapper.
+ */
+ @NonnullAfterInit protected ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("objectMapper cannot be null");
+ }
+ }
+
+}
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AccessTokenResponseDecoder.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AccessTokenResponseDecoder.java
new file mode 100644
index 00000000..0fc96489
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/decoding/impl/AccessTokenResponseDecoder.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.oidc.profile.decoding.impl;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpStatus;
+import org.slf4j.Logger;
+import org.springframework.http.MediaType;
+import org.springframework.util.MimeType;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Default access token response decoder which converts a successful HTTP response into an
+ * {@link OIDCTokenResponse} and a unsuccessful response into an {@link TokenErrorResponse}.
+ * Any decoding error is logged and {@code null} is returned.
+ */
+public class AccessTokenResponseDecoder extends AbstractJSONResponseDecoderFunction<TokenResponse> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AccessTokenResponseDecoder.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public TokenResponse handleResponse(final ClassicHttpResponse httpResponse) throws HttpException, IOException {
+ try {
+ if (httpResponse == null) {
+ log.warn("HttpResponse was null, can not process response");
+ return null;
+ }
+ final HttpEntity entity = httpResponse.getEntity();
+ if (entity == null) {
+ log.warn("HTTP response did not contain an entity");
+ return null;
+ }
+
+ final ContentType contentType = ContentType.parse(httpResponse.getEntity().getContentType());
+ if (contentType == null || contentType.getMimeType() == null) {
+ log.warn("HTTP response did not contain a content-type, must contain a content-type");
+ return null;
+ }
+ final String mimeType = contentType.getMimeType();
+ assert mimeType != null;
+ if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(mimeType)) != 0) {
+ log.warn("Wrong content type header, expected 'application/json' found '{}'", contentType.getMimeType());
+ return null;
+ }
+
+ try (final InputStream input = httpResponse.getEntity().getContent()) {
+ if (input == null) {
+ log.warn("HTTP response does not contain a message entity, nothing to decode");
+ return null;
+ }
+
+ final Map<String, Object> tokenResponseAsMap = getObjectMapper().readValue(
+ input, new TypeReference<Map<String, Object>>() {});
+ if (log.isTraceEnabled()) {
+ log.trace("Token Response: {}", tokenResponseAsMap);
+ }
+ final int httpStatusCode = httpResponse.getCode();
+
+ if (httpStatusCode != HttpStatus.SC_OK) {
+ return TokenErrorResponse.parse(new JSONObject(tokenResponseAsMap));
+ }
+
+ return OIDCTokenResponse.parse(new JSONObject(tokenResponseAsMap));
+ }
+
+ } catch (final Exception e) {
+ log.warn("Unable to decode response", e);
+ return null;
+ }
+ }
+
+}
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractRequestEncoderFunction.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractRequestEncoderFunction.java
new file mode 100644
index 00000000..6907d7ea
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractRequestEncoderFunction.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.profile.encoding.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** Abstract request encoder function that pulls out various contexts and request/response messages.*/
+public abstract class AbstractRequestEncoderFunction extends AbstractInitializableComponent
+ implements Function<ProfileRequestContext, ClassicHttpRequest> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractRequestEncoderFunction.class);
+
+ /** Lookup strategy to locate the OP metadata to use.*/
+ @Nonnull private Function<ProfileRequestContext, OIDCProviderMetadataContext> providerMetadataLookupStrategy;
+
+ /** The strategy used to lookup or create the {@link OAuth2ClientAuthenticationContext}.*/
+ @Nonnull
+ private Function<ProfileRequestContext, OAuth2ClientAuthenticationContext>
+ oauth2ClientAuthenticationContextLookupStrategy;
+
+ /** OIDC authentication response from upstream OP. */
+ @Nullable private AuthenticationSuccessResponse authnResponse;
+
+ /** OIDC Metadata context. */
+ @Nullable private OIDCProviderMetadataContext providerMetadataContext;
+
+ /**
+ * The context used to store client authentication information for communication with a
+ * upstream OP.
+ */
+ @Nullable private OAuth2ClientAuthenticationContext clientAuthnContext;
+
+ /** Constructor.*/
+ protected AbstractRequestEncoderFunction() {
+ providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
+ new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+ new OutboundMessageContextLookup()));
+
+ oauth2ClientAuthenticationContextLookupStrategy =
+ new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class).compose(
+ new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+ new OutboundMessageContextLookup()));
+ }
+
+ /**
+ * Set the strategy to lookup the {@link OAuth2ClientAuthenticationContext}.
+ *
+ * @param strgy the strategy.
+ */
+ public void setOAuth2ClientAuthenticationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, OAuth2ClientAuthenticationContext> strgy) {
+ checkSetterPreconditions();
+
+ oauth2ClientAuthenticationContextLookupStrategy = Constraint.isNotNull(strgy,
+ "OAuth2 client context lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the lookup strategy to locate the OpenID providers metadata.
+ *
+ * @param strategy the strategy.
+ */
+ public void setProviderMetadataLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, OIDCProviderMetadataContext> strategy) {
+ checkSetterPreconditions();
+
+ providerMetadataLookupStrategy =
+ Constraint.isNotNull(strategy,"Provider metadata lookup strategy can not be null");
+ }
+
+ /**
+ * Returns the authentication response from the upstream OP.
+ *
+ * @return the authentication response.
+ */
+ @Nullable protected AuthenticationSuccessResponse getAuthenticationResponse() {
+ return authnResponse;
+ }
+
+ /**
+ * Get the client authentication context.
+ *
+ * @return the client context.
+ */
+ @Nullable protected OAuth2ClientAuthenticationContext getClientAuthenticationContext() {
+ return clientAuthnContext;
+ }
+
+ /**
+ * Returns the OIDC provider metadata context.
+ *
+ * @return The provider metadata context.
+ */
+ @Nullable protected OIDCProviderMetadataContext getProviderMetadataContext() {
+ return providerMetadataContext;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Creates the HTTP request. Any error in creating the request should return {@code null} to indicate
+ * failure.</p>
+ */
+ @Override
+ @Nullable public ClassicHttpRequest apply(@Nullable final ProfileRequestContext profileRequestContext) {
+
+ if (profileRequestContext == null) {
+ log.error("Profile request context is null, unable to encode request");
+ return null;
+ }
+
+ final MessageContext inboundMessageCtx = profileRequestContext.getInboundMessageContext();
+ if (inboundMessageCtx == null) {
+ log.error("No inbound message context");
+ return null;
+ }
+ if (inboundMessageCtx.getMessage() == null) {
+ log.error("No inbound message");
+ return null;
+ }
+
+ if (!(inboundMessageCtx.getMessage() instanceof AuthenticationSuccessResponse)) {
+ log.error("No inbound authentication success response");
+ return null;
+ }
+ authnResponse = (AuthenticationSuccessResponse)inboundMessageCtx.getMessage();
+
+ providerMetadataContext = providerMetadataLookupStrategy.apply(profileRequestContext);
+ if (providerMetadataContext == null) {
+ log.error("No provider metadata context found for peer");
+ return null;
+ }
+ assert providerMetadataContext != null;
+ final var providerMetadata = providerMetadataContext.getProviderInformation();
+ if (providerMetadata == null) {
+ log.error("No provider metadata found for peer");
+ return null;
+ }
+
+ clientAuthnContext = oauth2ClientAuthenticationContextLookupStrategy.apply(profileRequestContext);
+ if (clientAuthnContext == null) {
+ log.error("No OAuth 2.0 client authentication context found");
+ return null;
+ }
+
+ return doApply(profileRequestContext, providerMetadata);
+
+ }
+
+ /**
+ * Encode a ClassicHttpRequest from the given context. Implementations should override this method.
+ *
+ * @param profileRequestContext the profile request context.
+ * @param providerMetadata the provider metadata.
+ *
+ * @return the request to execute.
+ */
+ @Nullable protected abstract ClassicHttpRequest doApply(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final OIDCProviderMetadata providerMetadata);
+
+
+}
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AuthCodeTokenRequestEncoder.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AuthCodeTokenRequestEncoder.java
new file mode 100644
index 00000000..71c35a30
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AuthCodeTokenRequestEncoder.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.profile.encoding.impl;
+
+import java.net.URI;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A token request encoder that builds an OAuth2.0 Access Token Request for an authorization_code grant and returns an
+ * {@link HttpUriRequest}.
+ * */
+public class AuthCodeTokenRequestEncoder extends AbstractRequestEncoderFunction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(AuthCodeTokenRequestEncoder.class);
+
+ @Override
+ @Nullable public ClassicHttpRequest doApply(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final OIDCProviderMetadata providerMetadata) {
+
+ try {
+ final OAuth2ClientAuthenticationContext authnContext = getClientAuthenticationContext();
+ if (authnContext == null) {
+ log.warn("No client authentication context to base token request off");
+ return null;
+ }
+ final AuthenticationSuccessResponse authnResponse = getAuthenticationResponse();
+ if (authnResponse == null) {
+ log.warn("No authentication response from upstream OpenID Provider to base token request off");
+ return null;
+ }
+ // final var authnRequest = getAuthenticationRequest();
+ // if (authnRequest == null) {
+ // log.warn("No authentication request to base token request off");
+ // return null;
+ //}
+
+ // If PKCE was set in the request (is not null) use it, else set it to null
+ final AuthorizationGrant codeGrant =
+ new AuthorizationCodeGrant(authnResponse.getAuthorizationCode(), new URI("http://redirect/")); //TODO REDIRECT_URI
+ // authnRequest.getCodeVerifier() != null ? new CodeVerifier(authnRequest.getCodeVerifier())
+ // : null);
+
+ final TokenRequest tokenRequest = new TokenRequest(providerMetadata.getTokenEndpointURI(),
+ authnContext.getClientAuthentication(), codeGrant);
+
+ final var httpRequest = tokenRequest.toHTTPRequest();
+ assert httpRequest != null;
+ return convertHttpRequest(httpRequest);
+
+ } catch (final Exception e) {
+ log.warn("Unable to encode token request", e);
+ }
+ return null;
+ }
+
+ /**
+ * Convert the internally used {@link HTTPRequest} to the externally presented {@link HttpUriRequest}.
+ *
+ * @param request the HTTP request to convert
+ *
+ * @return the convert HTTP request
+ */
+ @Nullable private ClassicHttpRequest convertHttpRequest(@Nonnull final HTTPRequest request) {
+
+ if (request.getMethod() != HTTPRequest.Method.POST) {
+ // Should never happen as HTTPRequest should always use POST
+ log.warn("Token Request must use the HTTP POST method, is trying to use '{}'", request.getMethod());
+ return null;
+ }
+ final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(request.getURI()).setHeader(
+ "Content-Type", request.getEntityContentType().toString())
+ .setCharset(StandardCharset.UTF_8);
+
+ request.getQueryParameters().forEach((k,v) -> v.stream().forEach(value -> rb.addParameter(k, value)));
+ if (request.getAuthorization() != null && !request.getAuthorization().isEmpty()) {
+ rb.addHeader("Authorization", request.getAuthorization());
+ }
+ return rb.build();
+ }
+}
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/InitializeOAuth2ClientAuthenticationContext.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/InitializeOAuth2ClientAuthenticationContext.java
new file mode 100644
index 00000000..78994d55
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/InitializeOAuth2ClientAuthenticationContext.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link AbstractMessageHandler action} that initializes an {@link OAuth2ClientAuthenticationContext} for later use.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @post create an {@link OAuth2ClientAuthenticationContext}
+ */
+public class InitializeOAuth2ClientAuthenticationContext extends AbstractMessageHandler {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(InitializeOAuth2ClientAuthenticationContext.class);
+
+ /**
+ * The strategy used to lookup or create the {@link OAuth2ClientAuthenticationContext}
+ * for storing the client authentication.
+ */
+ @Nonnull private Function<MessageContext, OAuth2ClientAuthenticationContext>
+ oauth2ClientAuthenticationContextLookupStrategy;
+
+
+ /** Constructor.*/
+ public InitializeOAuth2ClientAuthenticationContext() {
+
+ // Default under the OIDC Peer Entity Context, create is true
+ oauth2ClientAuthenticationContextLookupStrategy =
+ new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class, true).compose(
+ new ChildContextLookup<>(OIDCPeerEntityContext.class));
+ }
+
+ /**
+ * Set the strategy to lookup the {@link OAuth2ClientAuthenticationContext}
+ * from the {@link ProfileRequestContext}.
+ *
+ * @param strgy the strategy.
+ */
+ public void setOAuth2ClientAuthenticationContextLookupStrategy(
+ @Nonnull final Function<MessageContext, OAuth2ClientAuthenticationContext> strgy) {
+ checkSetterPreconditions();
+
+ oauth2ClientAuthenticationContextLookupStrategy = Constraint.isNotNull(strgy,
+ "OAuth2 client authentication context lookup strategy cannot be null");
+ }
+
+
+ @Override
+ protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
+
+ final OAuth2ClientAuthenticationContext context =
+ oauth2ClientAuthenticationContextLookupStrategy.apply(messageContext);
+
+ if (context == null) {
+ throw new MessageHandlerException("No OAuth2 client authentication context found or created");
+ }
+
+ log.debug("{} Initialized OAuth2 Client Authentication Context",getLogPrefix());
+ }
+
+
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list