[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