[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