[java-idp-oidc] branch dev/JOIDC-226 updated: JOIDC-226 - Autoconfigure CORS for public OIDC clients
Codeberg
noreply at shibboleth.net
Thu Nov 20 16:59:37 UTC 2025
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/JOIDC-226
in repository java-idp-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/9051b738959a26c779a25d7e7c9f8c339eb17ba8
The following commit(s) were added to refs/heads/dev/JOIDC-226 by this push:
new 9051b738 JOIDC-226 - Autoconfigure CORS for public OIDC clients
9051b738 is described below
commit 9051b738959a26c779a25d7e7c9f8c339eb17ba8
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Nov 20 18:59:04 2025 +0200
JOIDC-226 - Autoconfigure CORS for public OIDC clients
https://shibboleth.atlassian.net/browse/JOIDC-226
- Refactored most of the logic into the originHeaderValidator from ValidateOriginHeader-action
- Added initial unit tests
---
.../oauth2/profile/impl/ValidateOriginHeader.java | 41 ++-----
.../DefaultOriginHeaderValidationPredicate.java | 67 +++++++++--
.../profile/impl/ValidateOriginHeaderTest.java | 66 +++++++++++
...DefaultOriginHeaderValidationPredicateTest.java | 129 +++++++++++++++++++++
4 files changed, 260 insertions(+), 43 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeader.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeader.java
index 8a41359f..f86c611b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeader.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeader.java
@@ -23,13 +23,13 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
+import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultOriginHeaderValidationPredicate;
import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.logic.FunctionSupport;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.primitive.StringSupport;
/**
* Validates the possibly existing Origin-header via a configurable validator.
@@ -44,44 +44,25 @@ public class ValidateOriginHeader extends AbstractProfileAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ValidateOriginHeader.class);
- /** Strategy used to locate the Origin -header value. */
- @Nonnull private Function<ProfileRequestContext, String> originHeaderLookupStrategy;
-
/** Enforcer function for validating client ID against the configured policy. */
- @Nonnull private Function<ProfileRequestContext, BiPredicate<ProfileRequestContext, String>>
+ @Nonnull private Function<ProfileRequestContext, BiPredicate<ProfileRequestContext, HttpServletRequest>>
originHeaderValidatorLookupStrategy;
- /** The Origin -header value to be validated. */
- @NonnullBeforeExec private String origin;
-
/** The validator for the Origin -header value. */
- @NonnullBeforeExec private BiPredicate<ProfileRequestContext, String> originValidator;
+ @NonnullBeforeExec private BiPredicate<ProfileRequestContext, HttpServletRequest> originValidator;
/** Constructor. */
public ValidateOriginHeader() {
- originHeaderLookupStrategy = prc -> getHttpServletRequest().getHeader("Origin");
originHeaderValidatorLookupStrategy = FunctionSupport.constant(new DefaultOriginHeaderValidationPredicate());
}
- /**
- * Set the strategy used to locate the Origin -header value.
- *
- * @param strategy lookup strategy
- */
- public void setOriginHeaderLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
- checkSetterPreconditions();
-
- originHeaderLookupStrategy =
- Constraint.isNotNull(strategy, "OriginHeaderLookupStrategy cannot be null");
- }
-
/**
* Set the strategy used to locate the validator for the Origin -header value.
*
* @param strategy lookup strategy
*/
public void setOriginHeaderValidatorLookupStrategy(
- final Function<ProfileRequestContext, BiPredicate<ProfileRequestContext, String>> strategy) {
+ final Function<ProfileRequestContext, BiPredicate<ProfileRequestContext, HttpServletRequest>> strategy) {
checkSetterPreconditions();
originHeaderValidatorLookupStrategy = Constraint.isNotNull(strategy,
@@ -95,15 +76,9 @@ public class ValidateOriginHeader extends AbstractProfileAction {
return false;
}
- origin = StringSupport.trimOrNull(originHeaderLookupStrategy.apply(profileRequestContext));
- if (origin == null) {
- log.trace("{} No Origin-header resolved, nothing to do", getLogPrefix());
- return false;
- }
-
originValidator = originHeaderValidatorLookupStrategy.apply(profileRequestContext);
if (originValidator == null) {
- log.error("{} No validator for the Origin-header resolved, nothing to do", getLogPrefix());
+ log.debug("{} No validator for the Origin-header resolved, nothing to do", getLogPrefix());
return false;
}
@@ -113,12 +88,12 @@ public class ValidateOriginHeader extends AbstractProfileAction {
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!originValidator.test(profileRequestContext, origin)) {
- log.warn("{} Origin {} did not pass the validation", getLogPrefix(), origin);
+ if (!originValidator.test(profileRequestContext, getHttpServletRequest())) {
+ log.debug("{} Origin-header validator retured false", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EVENT_ID_INVALID_ORIGIN_HEADER);
return;
}
- log.debug("{} Successfully validated origin {}", getLogPrefix(), origin);
+ log.debug("{} Successfully validated Origin-header", getLogPrefix());
}
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicate.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicate.java
index b4f8ab8c..76bce2f6 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicate.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicate.java
@@ -15,6 +15,7 @@
package net.shibboleth.idp.plugin.oidc.op.profile.logic;
import java.net.URI;
+import java.net.URISyntaxException;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
@@ -24,17 +25,26 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultValidRedirectUrisLookupFunction;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
/**
* Default validation strategy for the Origin -header. This checks if any of the valid URIs fetched via lookup strategy
- * starts with the Origin-value given in the input parameters.
+ * starts with the Origin-value resolved via input parameters. Before matching, the Origin-value is verified to be a
+ * valid URI with non-null values for scheme and host and not containing a trailing slash-character.
*
* @since 4.4.0
*/
-public class DefaultOriginHeaderValidationPredicate implements BiPredicate<ProfileRequestContext, String> {
+public class DefaultOriginHeaderValidationPredicate implements BiPredicate<ProfileRequestContext, HttpServletRequest> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultOriginHeaderValidationPredicate.class);
/** Strategy used to obtain the redirect uris to compare request value to. */
@Nonnull private Function<ProfileRequestContext, Set<URI>> validRedirectURIsLookupStrategy;
@@ -43,20 +53,57 @@ public class DefaultOriginHeaderValidationPredicate implements BiPredicate<Profi
* Constructor.
*/
public DefaultOriginHeaderValidationPredicate() {
- validRedirectURIsLookupStrategy = new DefaultValidRedirectUrisLookupFunction();
+ this(new DefaultValidRedirectUrisLookupFunction());
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param strategy strategy used to obtain the redirect uris to compare request Origin to
+ */
+ public DefaultOriginHeaderValidationPredicate(@Nonnull @ParameterName(name="strategy")
+ final Function<ProfileRequestContext, Set<URI>> strategy) {
+ validRedirectURIsLookupStrategy =
+ Constraint.isNotNull(strategy, "ValidRedirectURIsLookupStrategy cannot be null");
}
-
+
+ /** {@inheritDoc} */
@Override
- public boolean test(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final String origin) {
- if (StringSupport.trimOrNull(origin) == null) {
+ public boolean test(@Nullable final ProfileRequestContext profileRequestContext,
+ @Nullable final HttpServletRequest httpServletRequest) {
+ if (httpServletRequest == null) {
+ log.error("Cannot resolve Origin-header as servlet request is null");
+ return false;
+ }
+ final String origin = StringSupport.trimOrNull(httpServletRequest.getHeader("Origin"));
+ if (origin == null) {
+ log.debug("No Origin-header set in the servlet request");
+ return true;
+ }
+
+ final URI uri;
+ try {
+ uri = new URI(origin);
+ } catch (final URISyntaxException e) {
+ log.warn("Origin-header {} is not a valid URI", origin);
+ return false;
+ }
+ if (StringSupport.trimOrNull(uri.getScheme()) == null || StringSupport.trimOrNull(uri.getHost()) == null
+ || (origin.endsWith("/"))) {
+ log.warn("Origin-header {} has unexpected format", origin);
return false;
}
- return Optional.ofNullable(validRedirectURIsLookupStrategy.apply(profileRequestContext))
+
+ final boolean result = Optional.ofNullable(validRedirectURIsLookupStrategy.apply(profileRequestContext))
.map(set -> set.stream()
- .map(uri -> uri.toString())
- .map(uri -> uri.endsWith("/") ? uri : uri.concat("/"))
- .anyMatch(uri -> uri.startsWith(origin)))
+ .map(redirectUri -> redirectUri.toString())
+ .anyMatch(redirectUri -> redirectUri.startsWith(origin.concat("/"))))
.orElse(false);
+ if (!result) {
+ log.warn("Origin header {} is not matching any redirection URIs", origin);
+ return false;
+ }
+ return true;
}
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeaderTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeaderTest.java
new file mode 100644
index 00000000..a6444406
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateOriginHeaderTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+import java.util.function.BiPredicate;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.testing.ActionTestingSupport;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link ValidateOriginHeader}.
+ */
+public class ValidateOriginHeaderTest {
+
+ ValidateOriginHeader action;
+
+ @Mock
+ BiPredicate<ProfileRequestContext, HttpServletRequest> headerValidator;
+
+ ProfileRequestContext prc;
+
+ @BeforeMethod
+ public void setup() throws ComponentInitializationException {
+ MockitoAnnotations.openMocks(this);
+ action = new ValidateOriginHeader();
+ action.setOriginHeaderValidatorLookupStrategy(prc -> headerValidator);
+ action.initialize();
+ prc = new ProfileRequestContext();
+ }
+
+ @Test
+ public void testWithValidatorFailing() {
+ when(headerValidator.test(any(), any())).thenReturn(false);
+ action.execute(prc);
+ ActionTestingSupport.assertEvent(prc, ValidateOriginHeader.EVENT_ID_INVALID_ORIGIN_HEADER);
+ }
+
+ @Test
+ public void testWithValidatorSuccess() {
+ when(headerValidator.test(any(), any())).thenReturn(true);
+ action.execute(prc);
+ ActionTestingSupport.assertProceedEvent(prc);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicateTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicateTest.java
new file mode 100644
index 00000000..78f11917
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultOriginHeaderValidationPredicateTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.logic;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+import java.net.URI;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link DefaultOriginHeaderValidationPredicate}.
+ */
+public class DefaultOriginHeaderValidationPredicateTest {
+
+ DefaultOriginHeaderValidationPredicate predicate;
+
+ @Mock
+ Function<ProfileRequestContext, Set<URI>> redirectUriLookup;
+
+ MockHttpServletRequest request;
+
+ @BeforeMethod
+ public void setup() throws ComponentInitializationException {
+ MockitoAnnotations.openMocks(this);
+ predicate = new DefaultOriginHeaderValidationPredicate(redirectUriLookup);
+ request = new MockHttpServletRequest();
+ }
+
+ @Test
+ public void testWithSchemeOnlyOrigin() {
+ request.addHeader("Origin", "http");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithPartialOrigin1() {
+ request.addHeader("Origin", "http:");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithPartialOrigin2() {
+ request.addHeader("Origin", "http:/");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithPartialOrigin3() {
+ request.addHeader("Origin", "http://");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithOriginTralingSlash1() {
+ request.addHeader("Origin", "http://localhost/");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithOriginTralingSlash2() {
+ request.addHeader("Origin", "http://localhost:1234/");
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithValidUriValidatorFails1() {
+ request.addHeader("Origin", "http://localhost");
+ when(redirectUriLookup.apply(any())).thenReturn(Set.of(
+ URI.create("http://localhost:81/cb"), URI.create("http://localhost:82/cb2")));
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithValidUriValidatorFails2() {
+ request.addHeader("Origin", "http://localhost:83");
+ when(redirectUriLookup.apply(any())).thenReturn(Set.of(
+ URI.create("http://localhost:81/cb"), URI.create("http://localhost:82/cb2")));
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithValidUriValidatorFails3() {
+ request.addHeader("Origin", "http://localhost:");
+ when(redirectUriLookup.apply(any())).thenReturn(Set.of(
+ URI.create("http://localhost:81/cb"), URI.create("http://localhost:82/cb2")));
+ Assert.assertFalse(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithValidUriValidatorSucceeds1() {
+ request.addHeader("Origin", "http://localhost");
+ when(redirectUriLookup.apply(any())).thenReturn(Set.of(
+ URI.create("http://localhost:81/cb"), URI.create("http://localhost/cb")));
+ Assert.assertTrue(predicate.test(new ProfileRequestContext(), request));
+ }
+
+ @Test
+ public void testWithValidUriValidatorSucceeds2() {
+ request.addHeader("Origin", "http://localhost:1234");
+ when(redirectUriLookup.apply(any())).thenReturn(Set.of(
+ URI.create("http://localhost:81/cb"), URI.create("http://localhost:1234/cb")));
+ Assert.assertTrue(predicate.test(new ProfileRequestContext(), request));
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list