[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