[java-oidc-common] branch main updated: JCOMOIDC-174 - Add Logout Request Redirect Encoder

Codeberg noreply at shibboleth.net
Thu Jul 2 15:32:48 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
https://codeberg.org/Shibboleth/java-oidc-common/commit/a5a2904abbe7804836cc1399949f0237a085d204

The following commit(s) were added to refs/heads/main by this push:
     new a5a2904a JCOMOIDC-174 - Add Logout Request Redirect Encoder
a5a2904a is described below

commit a5a2904abbe7804836cc1399949f0237a085d204
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Jul 2 16:32:37 2026 +0100

    JCOMOIDC-174 - Add Logout Request Redirect Encoder
    
     - Add new generic redirect encoder and deprecate the existing
    authentication specific encoder.
     - Add a Logout specific request message encoder
     - Add a HTTP request method getter to the Logout Profile Config.
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-174
---
 .../config/OIDCLogoutProfileConfiguration.java     |  18 ++
 .../impl/DefaultOIDCLogoutConfiguration.java       |  37 +++
 .../encoding/impl/AbstractOIDCMessageEncoder.java  |  74 +++++-
 .../impl/HTTPRedirectAuthnRequestEncoder.java      |   5 +
 ...ncoder.java => HTTPRedirectRequestEncoder.java} |  84 ++++---
 .../impl/LogoutRequestMessageEncoderFactory.java   | 107 ++++++++
 .../impl/HTTPRedirectRequestEncoderTest.java       | 274 +++++++++++++++++++++
 7 files changed, 568 insertions(+), 31 deletions(-)

diff --git a/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/OIDCLogoutProfileConfiguration.java b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/OIDCLogoutProfileConfiguration.java
index 7d25863d..21b430e0 100644
--- a/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/OIDCLogoutProfileConfiguration.java
+++ b/oidc-common-profile-api/src/main/java/net/shibboleth/oidc/profile/config/OIDCLogoutProfileConfiguration.java
@@ -23,6 +23,7 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2AuthorizationProfileConfiguration.HttpRequestMethod;
 import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenEncryptionProfileConfiguration;
 import net.shibboleth.shared.annotation.ConfigurationSetting;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
@@ -114,5 +115,22 @@ public interface OIDCLogoutProfileConfiguration extends OAuth2TokenEncryptionPro
      */
     @ConfigurationSetting(name="ignoreInvalidPostLogoutRedirectUri")
     boolean isIgnoreInvalidPostLogoutRedirectUri(@Nullable final ProfileRequestContext profileRequestContext);
+    
+    /**
+     * Get the HTTP request method for an authentication request.
+     *
+     * @param profileRequestContext profile request context
+     *
+     * @return the HTTP request method
+     * 
+     * TODO: remove the default in version 4.0.0
+     * 
+     * @since 3.4.0
+     */
+    @ConfigurationSetting(name="httpRequestMethod")
+    @Nullable default HttpRequestMethod getHttpRequestMethod(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        return HttpRequestMethod.GET;
+    }
 
 }
\ No newline at end of file
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/config/impl/DefaultOIDCLogoutConfiguration.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/config/impl/DefaultOIDCLogoutConfiguration.java
index 9b7ce38c..c82be878 100644
--- a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/config/impl/DefaultOIDCLogoutConfiguration.java
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/config/impl/DefaultOIDCLogoutConfiguration.java
@@ -26,10 +26,12 @@ import org.opensaml.profile.context.ProfileRequestContext;
 
 import net.shibboleth.idp.session.SPSession;
 import net.shibboleth.oidc.profile.config.OIDCLogoutProfileConfiguration;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2AuthorizationProfileConfiguration.HttpRequestMethod;
 import net.shibboleth.oidc.profile.oauth2.config.impl.AbstractOAuth2InterceptorAwareProfileConfiguration;
 import net.shibboleth.profile.config.OverriddenIssuerProfileConfiguration;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.shared.logic.PredicateSupport;
 
@@ -65,6 +67,12 @@ public class DefaultOIDCLogoutConfiguration extends AbstractOAuth2InterceptorAwa
     /** Lookup function to the RP-initiated logout's logout_hint parameter. */
     @Nonnull
     private Function<ProfileRequestContext,BiPredicate<String,SPSession>> logoutHintMatchingStrategyLookupStrategy;
+    
+    /**
+     * Which HTTP method should be used to issue OIDC logout requests.
+     * Supported values are POST and GET. The default is GET.
+     */
+    @Nonnull private Function<ProfileRequestContext,String> httpRequestMethodLookupStrategy;
 
     /**
      * Lookup function to supply strategy bi-predicate for custom valdation of post logout redirect URI in the request.
@@ -99,6 +107,7 @@ public class DefaultOIDCLogoutConfiguration extends AbstractOAuth2InterceptorAwa
         logoutHintMatchingStrategyLookupStrategy = FunctionSupport.constant((str, session) -> false);
         customPostLogoutRedirectUriValidationStrategyLookupStrategy = FunctionSupport.constant(null);
         ignoreInvalidPostLogoutRedirectUriPredicate = PredicateSupport.alwaysFalse();
+        httpRequestMethodLookupStrategy = FunctionSupport.constant(HttpRequestMethod.GET.toString());
     }
 
     @Override @Nullable @NotEmpty
@@ -333,5 +342,33 @@ public class DefaultOIDCLogoutConfiguration extends AbstractOAuth2InterceptorAwa
         ignoreInvalidPostLogoutRedirectUriPredicate =
                 Constraint.isNotNull(condition, "Ignore invalid post logout redirect URI predicate cannot be null");
     }
+    
+    /**
+     * Set a lookup strategy to determine the HTTP request method for an authentication request.
+     *
+     * @param strategy the strategy to set.
+     * 
+     * @since 3.4.0
+     */
+    public void setHttpRequestMethodLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        httpRequestMethodLookupStrategy =
+                Constraint.isNotNull(strategy, "HTTP request method strategy can not be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HttpRequestMethod getHttpRequestMethod(@Nullable final ProfileRequestContext profileRequestContext) {
+        final String method = httpRequestMethodLookupStrategy.apply(profileRequestContext);
+        if (method != null) {
+            try {
+                return HttpRequestMethod.valueOf(method);
+            } catch (final IllegalArgumentException e) {
+                throw new ConstraintViolationException("Unexpected HTTP method value: '" + method + "': "
+                        + e.getMessage());
+            }
+        }
+        return null;
+    }
 
 }
\ No newline at end of file
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractOIDCMessageEncoder.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractOIDCMessageEncoder.java
index b3b35da0..810dafb1 100644
--- a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractOIDCMessageEncoder.java
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/AbstractOIDCMessageEncoder.java
@@ -33,6 +33,7 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.langtag.LangTag;
 import com.nimbusds.oauth2.sdk.ResponseMode;
 import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.id.State;
 import com.nimbusds.openid.connect.sdk.Display;
 import com.nimbusds.openid.connect.sdk.Nonce;
@@ -42,12 +43,14 @@ import com.nimbusds.openid.connect.sdk.claims.ACR;
 
 import net.shibboleth.oidc.profile.core.OAuthAuthorizationRequest.CodeChallengeMethod;
 import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.core.OIDCLogoutRequest;
 import net.shibboleth.oidc.profile.encoding.AuthenticationContextClassReferenceSupport;
 import net.shibboleth.oidc.profile.encoding.OIDCMessageEncoder;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.net.URLBuilder;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
 
 /**
  * Base class for OIDC message encoders.
@@ -102,6 +105,22 @@ public abstract class AbstractOIDCMessageEncoder extends AbstractHttpServletResp
         params.forEach(param -> builder.getQueryParams().add(param));
     }
     
+    /**
+     * Serialize OIDC RP-Initiate logout parameters from the logout request to the query string 
+     * of the URL.
+     * 
+     * @param request the logout request.
+     * @param builder the URL builder to add the query parameters to.
+     * 
+     * @throws MessageEncodingException on error building the parameters
+     */
+    protected void serializeLogoutParamsToUrl(@Nonnull final OIDCLogoutRequest request,
+            @Nonnull final URLBuilder builder) throws MessageEncodingException {
+        
+        final List<Pair<String, String>> params = createParametersFromRequest(request);
+        params.forEach(param -> builder.getQueryParams().add(param));
+    }
+    
     /**
      * Serialize OAuth 2.0 authorization parameters from the authentication request to a query string.
      * 
@@ -110,10 +129,12 @@ public abstract class AbstractOIDCMessageEncoder extends AbstractHttpServletResp
      * @return the query string.
      * 
      * @throws MessageEncodingException on error building the parameters
+     * 
+     * @deprecated unused
      */
+    @Deprecated(since="3.4.0", forRemoval=true)
     protected String serializeAuthorizationParamsToQueryString(@Nonnull final OIDCAuthenticationRequest request)
             throws MessageEncodingException {
-        //TODO maybe a better way to do this than the full URL builder?
         final URLBuilder builder = new URLBuilder();
         final List<Pair<String, String>> params = createParametersFromRequest(request);
         params.forEach(param -> builder.getQueryParams().add(param));
@@ -124,8 +145,7 @@ public abstract class AbstractOIDCMessageEncoder extends AbstractHttpServletResp
     /**
      * Create a list of OAuth 2.0 authorization parameters from the {@link OIDCAuthenticationRequest} object.
      * 
-     * <p>Note, the parameters are not URL encoded here. This is left to the calling code e.g. the URLBuidler
-     * in the {@link #serializeAuthorizationParamsToQueryString(OIDCAuthenticationRequest)} method. </p>
+     * <p>Note, the parameters are not URL encoded here. This is left to the calling code. </p>
      * 
      * @param req the authentication request
      * 
@@ -147,6 +167,54 @@ public abstract class AbstractOIDCMessageEncoder extends AbstractHttpServletResp
         return params;
     }
     
+    /**
+     * Create a list of OIDC logout parameters from the {@link OIDCLogoutRequest} object.
+     * 
+     * <p>Note, the parameters are not URL encoded here. This is left to the calling code. </p>
+     * 
+     * @param req the logout request
+     * 
+     * @return a list of logout parameters.
+     * 
+     * @throws MessageEncodingException on error building the parameters
+     */
+    @Nonnull protected List<Pair<String, String>> createParametersFromRequest(
+            @Nonnull final OIDCLogoutRequest req) throws MessageEncodingException {
+        
+        final List<Pair<String, String>> params = new ArrayList<>();
+        
+        final JWT idTokenHint = req.getIdTokenHint();
+        if (idTokenHint != null) {
+            final String compactSerializedToken = idTokenHint.serialize();
+            params.add(new Pair<>("id_token_hint", compactSerializedToken));
+        }
+        final String logoutHint = req.getLogoutHint();
+        if (StringSupport.trimOrNull(logoutHint) != null) {
+            params.add(new Pair<>("logout_hint", logoutHint));
+        }
+        final ClientID clientId = req.getClientID();
+        if (clientId != null && StringSupport.trimOrNull(clientId.getValue()) != null) {
+            params.add(new Pair<>("client_id", clientId.getValue()));
+        }
+        final URI postLogoutRedirect = req.getPostLogoutRedirectURI();
+        if (postLogoutRedirect != null) {
+            // Just turn into a string here, do not URL encode e.g. ASCII String.
+            final String postLogoutRedirectString = postLogoutRedirect.toString();
+            params.add(new Pair<>("post_logout_redirect_uri", postLogoutRedirectString));
+        }
+        final State state = req.getState();
+        if (state != null) {
+            params.add(new Pair<>("state", state.getValue()));
+        }
+        if (!req.getUiLocales().isEmpty()) { 
+            final String locales = req.getUiLocales().stream().map(LangTag::toString).filter(s -> !s.isEmpty())
+                .collect(Collectors.joining(" "));
+            params.add(new Pair<>("ui_locales", locales));          
+        }
+             
+        return params;
+    }
+    
     /**
      * Add the standard set of OAuth2 2.0 authorization parameters to the params list using the
      * OAuth 2.0 request syntax. 
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java
index 3b54a304..df58759d 100644
--- a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java
@@ -36,14 +36,19 @@ import net.shibboleth.shared.servlet.HttpServletSupport;
 /**
  * A {@link MessageEncoder message encoder} that encodes an OpenID authentication request by 
  * Query String Serialization and sends a HTTP redirect response.
+ * 
+ * @deprecated The redirect encoder has been combined for both authentication and logout request in the 
+ *              {@link HTTPRedirectRequestEncoder}.
  */
 //TODO maybe this could encode authz responses as well? to replace the NimbusResponseEncoder
+ at Deprecated(since="3.4.0", forRemoval=true)
 public class HTTPRedirectAuthnRequestEncoder extends AbstractOIDCMessageEncoder  {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPRedirectAuthnRequestEncoder.class);
     
     /** {@inheritDoc} */
+    @Override
     public boolean test(@Nullable final HttpRequestMethod requestMethod) {
        return HttpRequestMethod.GET.equals(requestMethod);
     }    
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoder.java
similarity index 58%
copy from oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java
copy to oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoder.java
index 3b54a304..9d530eb5 100644
--- a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectAuthnRequestEncoder.java
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoder.java
@@ -28,22 +28,23 @@ import org.slf4j.Logger;
 
 import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.core.OIDCLogoutRequest;
 import net.shibboleth.oidc.profile.oauth2.config.OAuth2AuthorizationProfileConfiguration.HttpRequestMethod;
 import net.shibboleth.shared.net.URLBuilder;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.servlet.HttpServletSupport;
 
 /**
- * A {@link MessageEncoder message encoder} that encodes an OpenID authentication request by 
+ * A {@link MessageEncoder message encoder} that encodes an OpenID redirect request (e.g. authentication or logout) by 
  * Query String Serialization and sends a HTTP redirect response.
  */
-//TODO maybe this could encode authz responses as well? to replace the NimbusResponseEncoder
-public class HTTPRedirectAuthnRequestEncoder extends AbstractOIDCMessageEncoder  {
+public class HTTPRedirectRequestEncoder extends AbstractOIDCMessageEncoder  {
 
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPRedirectAuthnRequestEncoder.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPRedirectRequestEncoder.class);
     
     /** {@inheritDoc} */
+    @Override
     public boolean test(@Nullable final HttpRequestMethod requestMethod) {
        return HttpRequestMethod.GET.equals(requestMethod);
     }    
@@ -52,15 +53,19 @@ public class HTTPRedirectAuthnRequestEncoder extends AbstractOIDCMessageEncoder
     @Override
     protected void doEncode() throws MessageEncodingException {
         
-        log.debug("Encoding OIDC authentication request using Query String Serialization");
+        log.debug("Encoding OIDC request using Query String Serialization");
         final MessageContext messageContext = getMessageContext();
         final Object outboundMessage = messageContext.getMessage();
-        if (!(outboundMessage instanceof OIDCAuthenticationRequest)) {
-            throw new MessageEncodingException("No outbound OIDC authentication request message "
+        
+        String redirectURL;
+        if (outboundMessage instanceof final OIDCAuthenticationRequest authnRequest) {
+            redirectURL = buildRedirectURL(messageContext, authnRequest);
+        } else if (outboundMessage instanceof final OIDCLogoutRequest logoutRequest) {
+            redirectURL = buildRedirectURL(messageContext, logoutRequest);
+        } else {
+            throw new MessageEncodingException("Unsupported OIDC request message "
                     + "contained in message context");
         }
-
-        final String redirectURL = buildRedirectURL(messageContext, (OIDCAuthenticationRequest)outboundMessage);
         
         final HttpServletResponse response = getHttpServletResponse();
         if (response == null) {
@@ -71,7 +76,7 @@ public class HTTPRedirectAuthnRequestEncoder extends AbstractOIDCMessageEncoder
         HttpServletSupport.setContentType(response, "application/x-www-form-urlencoded");
 
         try {
-            log.trace("Redirecting user-agent to '{}'",redirectURL);
+            log.debug("Redirecting user-agent to '{}'",redirectURL);
             response.sendRedirect(redirectURL);
         } catch (final IOException e) {
             throw new MessageEncodingException("Problem sending HTTP redirect.", e);
@@ -89,31 +94,54 @@ public class HTTPRedirectAuthnRequestEncoder extends AbstractOIDCMessageEncoder
      * 
      * @throws MessageEncodingException if there is an issue building the URL or the endpoint is null.
      */
-    protected String buildRedirectURL(final MessageContext messageContext, final OIDCAuthenticationRequest request)
-            throws MessageEncodingException {
+    @Nonnull protected String buildRedirectURL(@Nonnull final MessageContext messageContext, 
+            @Nonnull final OIDCAuthenticationRequest request) throws MessageEncodingException {
         
-        if (request.getEndpointURI() == null) {
+        final URLBuilder urlBuilder = buildBaseRedirect(request.getEndpointURI());
+        serializeAuthorizationParamsToUrl(request, urlBuilder);       
+        return urlBuilder.buildURL();
+    }
+
+    
+    /**
+     * Build the URL to redirect the client to using parameters in the logout request.
+     * 
+     * @param messageContext the current message context
+     * @param request the logout request
+     * 
+     * @return a URL to redirect the client to.
+     * 
+     * @throws MessageEncodingException if there is an issue building the URL or the endpoint is null.
+     */
+    @Nonnull protected String buildRedirectURL(@Nonnull final MessageContext messageContext, 
+            @Nonnull final OIDCLogoutRequest request) throws MessageEncodingException {
+        
+        final URLBuilder urlBuilder = buildBaseRedirect(request.getLogoutEndpoint());
+        serializeLogoutParamsToUrl(request, urlBuilder);       
+        return urlBuilder.buildURL();
+        
+    }
+    
+    
+    /**
+     * Begin construction of a URL based on the endpoint given.
+     * 
+     * @param endpoint the endpoint to base the URL on
+     * @return the under construction URL
+     * 
+     * @throws MessageEncodingException on error building the base URL
+     */
+    @Nonnull private URLBuilder buildBaseRedirect(@Nullable final URI endpoint) throws MessageEncodingException {
+        if (endpoint == null) {
             throw new MessageEncodingException("No endpoint URI specified, URL can not be built.");
         }
-        URLBuilder urlBuilder = null;
         try {
-            if (request.getEndpointURI() == null) {
-                throw new MessageEncodingException("Endpoint URL is null");
-            }
-            //TODO check the endpoint is always the baseURL, otherwise this may go wrong.
-            final URI uri = request.getEndpointURI();
-            if (uri == null) {
-                throw new MessageEncodingException("No Endpoint URI available.");
-            }
-            final String uriValue = uri.toString();
+            final String uriValue = endpoint.toString();
             assert uriValue != null;
-            urlBuilder = new URLBuilder(uriValue);
+            return new URLBuilder(uriValue);
         } catch (final MalformedURLException e) {
-            throw new MessageEncodingException("Endpoint URL " + request.getEndpointURI() + " is not a valid URL", e);
+            throw new MessageEncodingException("Endpoint URL " + endpoint + " is not a valid URL", e);
         }
-        
-        serializeAuthorizationParamsToUrl(request, urlBuilder);       
-        return urlBuilder.buildURL();
     }
     
 }
\ No newline at end of file
diff --git a/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/LogoutRequestMessageEncoderFactory.java b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/LogoutRequestMessageEncoderFactory.java
new file mode 100644
index 00000000..325105eb
--- /dev/null
+++ b/oidc-common-profile-impl/src/main/java/net/shibboleth/oidc/profile/impl/LogoutRequestMessageEncoderFactory.java
@@ -0,0 +1,107 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.encoder.MessageEncoder;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.profile.config.OIDCLogoutProfileConfiguration;
+import net.shibboleth.oidc.profile.encoding.OIDCMessageEncoder;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2AuthorizationProfileConfiguration.HttpRequestMethod;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Message encoder factory function that returns the first encoder suitable for the given request method found in the
+ * profile configuration.
+ * 
+ * @since 3.4.0
+ */
+ at ThreadSafe
+public class LogoutRequestMessageEncoderFactory extends AbstractInitializableComponent
+        implements Function<ProfileRequestContext, MessageEncoder> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(LogoutRequestMessageEncoderFactory.class);
+
+    /** The list of message encoders to choose from. */
+    @Nonnull @Unmodifiable @NotLive private final List<OIDCMessageEncoder> encoders;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param encodersToUse the list of possible encoders to use.
+     */
+    public LogoutRequestMessageEncoderFactory(
+            @Nullable @ParameterName(name = "encoders") final List<OIDCMessageEncoder> encodersToUse) {
+        if (encodersToUse == null) {
+            encoders = CollectionSupport.emptyList();
+        } else {
+            encoders = CollectionSupport.copyToList(encodersToUse);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public MessageEncoder apply(@Nullable final ProfileRequestContext input) {
+
+        final RelyingPartyContext rpCtx = input != null ? input.getSubcontext(RelyingPartyContext.class) : null;
+        
+        OIDCLogoutProfileConfiguration profileConfiguration = null;
+        if (rpCtx != null && rpCtx.getProfileConfig() instanceof final OIDCLogoutProfileConfiguration downcast) {
+            profileConfiguration = downcast;
+        }
+        if (profileConfiguration == null) {
+            log.warn("OIDCLogoutProfileConfiguration not found, no encoders to lookup");
+            return null;
+        }
+
+        final HttpRequestMethod requestMethodFromConfig =
+                profileConfiguration.getHttpRequestMethod(input);
+
+        if (requestMethodFromConfig == null) {
+            log.warn("Authentication request method not found on profile, no encoders to lookup");
+            return null;
+        }
+
+        final Optional<OIDCMessageEncoder> encoder =
+                encoders.stream().filter(enc -> enc.test(requestMethodFromConfig)).findFirst();
+        if (encoder.isPresent()) {
+            log.trace("Returning OIDC message encoder of type '{}'", encoder.get().getClass());
+        } else {
+            log.warn("No message encoder was found for logout request method type '{}'",
+                    requestMethodFromConfig);
+        }
+
+        return encoder.orElse(null);
+    }
+
+}
diff --git a/oidc-common-profile-impl/src/test/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoderTest.java b/oidc-common-profile-impl/src/test/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoderTest.java
new file mode 100644
index 00000000..0d0f1e8f
--- /dev/null
+++ b/oidc-common-profile-impl/src/test/java/net/shibboleth/oidc/profile/encoding/impl/HTTPRedirectRequestEncoderTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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 static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.encoder.MessageEncodingException;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.langtag.LangTag;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.assurance.claims.VerifiedClaimsSetRequest;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.core.OIDCLogoutRequest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.UninitializedComponentException;
+import net.shibboleth.shared.servlet.impl.HttpServletRequestResponseContext;
+import net.shibboleth.shared.servlet.impl.ThreadLocalHttpServletResponseSupplier;
+
+/** Test for the HTTPRedirectRequestEncoder.*/
+ at SuppressWarnings("javadoc")
+public class HTTPRedirectRequestEncoderTest {
+    
+    private static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
+    
+    /** The encoder to test.*/
+    private HTTPRedirectRequestEncoder encoder;
+    
+    /** Mock servlet response.*/
+    private MockHttpServletResponse mockResponse;
+    
+    /** The OAuth authentication request.*/
+    private OIDCAuthenticationRequest request;
+    
+    /** The OIDC logout request.*/
+    private OIDCLogoutRequest logoutRequest;
+    
+    /** The Message context.*/
+    private MessageContext context;
+    
+    
+    @SuppressWarnings("null")
+    @BeforeMethod public void setUp() throws Exception {    
+        encoder = new HTTPRedirectRequestEncoder();      
+        context = new MessageContext();
+        request = new OIDCAuthenticationRequest(new ClientID("clientID"));
+        // Create an authentication request: This needs to be dynamic
+        request.setResponseType(ResponseType.CODE);
+        request.setEndpointURI(new URI("https://somewhere.com/oauth2/authz"));
+        request.setRedirectURI(new URI("https://localhost:8080/callback"));
+        
+        // Create a logout request
+        logoutRequest = new OIDCLogoutRequest(new URI("https://somewhere.com/end-session"));
+        logoutRequest.setClientID(new ClientID("clientID"));
+        
+        encoder.setMessageContext(context);
+        mockResponse = new MockHttpServletResponse();
+        encoder.setHttpServletResponseSupplier(new ThreadLocalHttpServletResponseSupplier());
+
+        assert mockResponse != null;
+    	HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), mockResponse);
+    }
+    
+    private JWTClaimsSet createClaims() {
+        return new JWTClaimsSet.Builder()
+                .issuer("https://localhost:9918")
+                .audience(List.of("test-client"))
+                .subject("jdoe")
+                .claim("nonce", "abadnonce")
+                .claim("azp", "test-client")
+                .claim("name","jdoe")
+                .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+                .build();
+    }
+    
+    private SignedJWT createdSignedJWT() throws KeyLengthException, JOSEException {        
+        final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+                .type(JOSEObjectType.JWT)
+                .keyID("mock-key")
+                .build();       
+        final var signedJWT = new SignedJWT(header,createClaims());
+        signedJWT.sign(new MACSigner(CLIENT_SECRET));
+        return signedJWT;
+    }
+    
+    @Test
+    public void testSuccesfullEncoding() throws Exception {
+        context.setMessage(request);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        // These are all required
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("response_type"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("scope"));
+       
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding_WithPostRedirect() throws Exception {
+        logoutRequest.setPostLogoutRedirectURI(new URI("https://localhost:8080/end-session-callback"));
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains(URLEncoder.encode("https://localhost:8080/end-session-callback",
+                StandardCharsets.UTF_8)));   
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding_WithIDTokenHint() throws Exception {
+        final JWT idToken = createdSignedJWT();
+        logoutRequest.setIdTokenHint(idToken);
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains(idToken.serialize()));   
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding_WithLogoutHint() throws Exception {
+        logoutRequest.setLogoutHint("hint");
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("hint"));   
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding_WithUILocales() throws Exception {
+        logoutRequest.setUiLocales(CollectionSupport.listOf(new LangTag("en"), new LangTag("cy")));
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));
+        //assertTrue(response.contains("hint"));   
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding_WithState() throws Exception {
+        logoutRequest.setState(new State("state"));
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("state"));   
+    }
+    
+    @Test
+    public void testSuccesfullLogoutRequestEncoding() throws Exception {
+        context.setMessage(logoutRequest);
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        assertTrue(response.contains("https://somewhere.com/end-session"));
+        assertTrue(response.contains("client_id"));       
+    }
+    
+    @Test
+    public void testSuccesfullEncoding_WithClaims() throws Exception {
+        context.setMessage(request);
+        final OIDCClaimsRequest requestedClaims =  new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(new VerifiedClaimsSetRequest().add("given_name"))
+                .withUserInfoClaimsRequest(new VerifiedClaimsSetRequest().add("family_name"));
+        request.setRequestedClaims(requestedClaims);
+        request.setProviderSupportsClaimsParameter(true);
+        
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        // These are all required
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("response_type"));
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("scope"));
+        assertTrue(response.contains("claims"));
+       
+    }
+    
+    @Test
+    public void testSuccesfullEncoding_WithRequestObject() throws Exception {
+        context.setMessage(request);
+    	request.setRequestObject(new PlainJWT(new JWTClaimsSet.Builder()
+    			.claim("response_type", "code")
+    			.claim("redirect_uri","https://localhost:8080/callback")
+    			.claim("scope", "openid")
+    			.claim("state", "somestate")
+    			.build()));
+        
+        encoder.initialize();
+        encoder.encode();
+        final String response = mockResponse.getRedirectedUrl();
+        assert response != null;
+        // These are all required
+        assertTrue(response.contains("client_id"));
+        assertTrue(response.contains("response_type"));
+        assertTrue(response.contains("request"));
+        assertTrue(response.contains("scope"));       
+    }
+    
+    @Test(expectedExceptions = UninitializedComponentException.class)
+    public void testUninitialized() throws MessageEncodingException {
+        context.setMessage(request);
+        encoder.encode();
+    }
+    
+    @Test(expectedExceptions = MessageEncodingException.class)
+    public void testNullEndpointURL() throws Exception {
+        context.setMessage(request);
+        request = new OIDCAuthenticationRequest(new ClientID("clientID"));
+        context.setMessage(request);
+        encoder.initialize();
+        encoder.encode();
+    }
+
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list