[java-idp-plugin-vci] 01/01: Remove OP resource prefix from PAR form parameters in PAR strategy, a workaround for OP shortcoming

Codeberg noreply at shibboleth.net
Wed Nov 4 08:11:21 UTC 2026


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

codeberg pushed a commit to branch dev/REFRESH
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/50ea06c118e4df0154d9f3026002d1a99335a459

commit 50ea06c118e4df0154d9f3026002d1a99335a459
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Wed Nov 4 10:10:48 2026 +0200

    Remove OP resource prefix from PAR form parameters in PAR strategy, a workaround for OP shortcoming
---
 ...edAuthorizationDPoPJktManipulationStrategy.java | 81 ++++++++++++++++---
 ...thorizationDPoPJktManipulationStrategyTest.java | 91 ++++++++++++++++++++++
 2 files changed, 162 insertions(+), 10 deletions(-)

diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategy.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategy.java
index 1e6dc9e..6fd7505 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategy.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategy.java
@@ -17,6 +17,7 @@
 package org.geant.shibboleth.plugin.openidvci.profile.logic;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -27,13 +28,17 @@ import javax.annotation.Nullable;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultRequestAudienceLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Manipulation of the claims set stored for a pushed authorization request that sets 'dpop_jkt' to the thumbprint
- * of the validated DPoP proof key of the request, when the request has no 'dpop_jkt' of its own.
+ * This strategy is a workaround for two OP shortcomings.
+ * 1) Sets 'dpop_jkt' to the thumbprint of the validated DPoP proof key of the request, when the request has no
+ * 'dpop_jkt' of its own. See <a href="https://www.rfc-editor.org/rfc/rfc9449.html#section-10.1">RFC 9449
+ * section 10.1</a>.
+ * 2) Removes the OP's internal prefix from the 'resource' values if resource is sent as a form parameter.
  */
 public class PushedAuthorizationDPoPJktManipulationStrategy
         implements BiFunction<ProfileRequestContext, Map<String, Object>, Map<String, Object>> {
@@ -42,6 +47,10 @@ public class PushedAuthorizationDPoPJktManipulationStrategy
     @Nonnull
     public static final String DPOP_JKT = "dpop_jkt";
 
+    /** Claim name of the resource indicators. */
+    @Nonnull
+    public static final String RESOURCE = "resource";
+
     /** Class logger. */
     @Nonnull
     private Logger log = LoggerFactory.getLogger(PushedAuthorizationDPoPJktManipulationStrategy.class);
@@ -66,17 +75,69 @@ public class PushedAuthorizationDPoPJktManipulationStrategy
     @Nullable
     public Map<String, Object> apply(@Nullable final ProfileRequestContext profileRequestContext,
             @Nullable final Map<String, Object> claims) {
-        if (claims == null || claims.get(DPOP_JKT) != null) {
-            return null;
-        }
-        final String thumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
-        if (thumbprint == null) {
+        if (claims == null) {
             return null;
         }
         final Map<String, Object> result = new HashMap<>(claims);
-        result.put(DPOP_JKT, thumbprint);
-        log.debug("Pushed authorization request bound to DPoP proof key {}", thumbprint);
-        return result;
+        boolean changed = removeResourcePrefix(result);
+        if (result.get(DPOP_JKT) == null) {
+            final String thumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+            if (thumbprint != null) {
+                result.put(DPOP_JKT, thumbprint);
+                log.debug("Pushed authorization request bound to DPoP proof key {}", thumbprint);
+                changed = true;
+            }
+        }
+        return changed ? result : null;
+    }
+
+    /**
+     * Remove the prefix the OP decoders add to 'resource' values.
+     *
+     * 'resource' sent to PAR as a form parameter has the prefix but it should not.
+     *
+     * From this point on the OP logic assumes there is no prefix in the value. The authorization endpoint reads
+     * the stored request as a request object, and {@link DefaultRequestAudienceLookupFunction} returns the 'resource'
+     * claim of a request object as it is.
+     *
+     * @param claims claims set to modify
+     * @return whether a value was changed
+     */
+    private boolean removeResourcePrefix(@Nonnull final Map<String, Object> claims) {
+        final Object resource = claims.get(RESOURCE);
+        if (resource instanceof String value && isPrefixed(value)) {
+            claims.put(RESOURCE, removePrefix(value));
+            return true;
+        }
+        if (resource instanceof List<?> values && values.stream().anyMatch(this::isPrefixed)) {
+            claims.put(RESOURCE, values.stream()
+                    .map(value -> isPrefixed(value) ? removePrefix((String) value) : value)
+                    .toList());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Whether the value is a string with the prefix of the OP decoders.
+     *
+     * @param value the value
+     * @return whether the value is prefixed
+     */
+    private boolean isPrefixed(@Nullable final Object value) {
+        return value instanceof String string
+                && string.startsWith(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_PREFIX);
+    }
+
+    /**
+     * Remove the prefix of the OP decoders.
+     *
+     * @param value prefixed value
+     * @return value without the prefix
+     */
+    @Nonnull
+    private String removePrefix(@Nonnull final String value) {
+        return value.substring(DefaultRequestAudienceLookupFunction.CUSTOM_RESOURCE_PARAM_PREFIX.length());
     }
 
 }
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategyTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategyTest.java
new file mode 100644
index 0000000..2555edd
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/PushedAuthorizationDPoPJktManipulationStrategyTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * 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 org.geant.shibboleth.plugin.openidvci.profile.logic;
+
+import java.util.List;
+import java.util.Map;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link PushedAuthorizationDPoPJktManipulationStrategy}.
+ */
+public class PushedAuthorizationDPoPJktManipulationStrategyTest {
+
+    private static final String PREFIX = "urn:shibboleth.oidc.op.resource:";
+
+    private static final String ISSUER = "https://issuer.example.org";
+
+    private PushedAuthorizationDPoPJktManipulationStrategy strategy(final String thumbprint) {
+        final PushedAuthorizationDPoPJktManipulationStrategy strategy =
+                new PushedAuthorizationDPoPJktManipulationStrategy();
+        strategy.setDpopProofThumbprintLookupStrategy(prc -> thumbprint);
+        return strategy;
+    }
+
+    @Test
+    public void setsThumbprint() {
+        final Map<String, Object> result = strategy("jkt").apply(new ProfileRequestContext(),
+                Map.of("scope", "Diploma"));
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.get(PushedAuthorizationDPoPJktManipulationStrategy.DPOP_JKT), "jkt");
+    }
+
+    @Test
+    public void keepsOwnThumbprint() {
+        Assert.assertNull(strategy("jkt").apply(new ProfileRequestContext(),
+                Map.of(PushedAuthorizationDPoPJktManipulationStrategy.DPOP_JKT, "own")));
+    }
+
+    @Test
+    public void noChange() {
+        Assert.assertNull(strategy(null).apply(new ProfileRequestContext(), Map.of("resource", ISSUER)));
+    }
+
+    @Test
+    public void removesPrefixOfResource() {
+        final Map<String, Object> result = strategy(null).apply(new ProfileRequestContext(),
+                Map.of("resource", PREFIX + ISSUER));
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.get("resource"), ISSUER);
+    }
+
+    @Test
+    public void removesPrefixOfResources() {
+        final Map<String, Object> result = strategy(null).apply(new ProfileRequestContext(),
+                Map.of("resource", List.of(PREFIX + ISSUER, "https://other.example.org")));
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.get("resource"), List.of(ISSUER, "https://other.example.org"));
+    }
+
+    @Test
+    public void removesPrefixAndSetsThumbprint() {
+        final Map<String, Object> result = strategy("jkt").apply(new ProfileRequestContext(),
+                Map.of("resource", PREFIX + ISSUER));
+        Assert.assertNotNull(result);
+        Assert.assertEquals(result.get("resource"), ISSUER);
+        Assert.assertEquals(result.get(PushedAuthorizationDPoPJktManipulationStrategy.DPOP_JKT), "jkt");
+    }
+
+    @Test
+    public void nullClaims() {
+        Assert.assertNull(strategy("jkt").apply(new ProfileRequestContext(), null));
+    }
+
+}

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


More information about the commits mailing list