[java-idp-oidc] branch maint-4.3 updated: JOIDC-283 - Scope in dynamic client registration request is not respected
Codeberg
noreply at shibboleth.net
Sun Jul 12 16:08:07 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch maint-4.3
in repository java-idp-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-oidc/commit/2ed3bcd5636f1a5383702fcfee3babcb92563e3b
The following commit(s) were added to refs/heads/maint-4.3 by this push:
new 2ed3bcd5 JOIDC-283 - Scope in dynamic client registration request is not respected
2ed3bcd5 is described below
commit 2ed3bcd5636f1a5383702fcfee3babcb92563e3b
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Sun Jul 12 19:02:05 2026 +0300
JOIDC-283 - Scope in dynamic client registration request is not respected
https://shibboleth.atlassian.net/browse/JOIDC-283
Support scope within the request as specified by rfc7591
- Scope is not included in OIDC dynamic client registration spec and that's (probably) why it wasn't supported by Nimbus years ago when the initial implementation was done
Added scope to the flow tests
- Metadata policy structure for subset_of and superset_of is using the JSON-array style as we have documented
- The final OpenID Federation 1.0 spec defines a special rule in section 6.1.3.1.8. to use space-separated string instead
---
.../impl/OIDCClientRegistrationRequestDecoder.java | 11 --
.../impl/ValidateRegistrationRequestMetadata.java | 15 ++-
.../oidc/op/profile/flow/RegistrationFlowTest.java | 112 ++++++++++++++++++++-
3 files changed, 124 insertions(+), 14 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCClientRegistrationRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCClientRegistrationRequestDecoder.java
index 283a2759..374cd7ff 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCClientRegistrationRequestDecoder.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCClientRegistrationRequestDecoder.java
@@ -30,7 +30,6 @@ import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientRegistrationRequest;
-import net.minidev.json.JSONObject;
import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -76,16 +75,6 @@ public class OIDCClientRegistrationRequestDecoder extends BaseOAuth2RequestDecod
try {
final HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpRequest, objectMapper));
- final JSONObject requestJson = httpRequest.getQueryAsJSONObject();
- //TODO: Nimbus seems to be interpreting scope in different way as many RPs, currently the scope
- //is removed in this phase, better solution TODO.
- if (requestJson.containsKey("scope")) {
- log.debug("Removed 'scope'");
- requestJson.remove("scope");
- httpRequest.setQuery(requestJson.toJSONString());
- }
-
- log.trace("JSON object: {}", httpRequest.getQueryAsJSONObject().toJSONString());
return OIDCClientRegistrationRequest.parse(httpRequest);
} catch (final IOException e) {
log.error("Could not create HTTP request from the request", e);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationRequestMetadata.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationRequestMetadata.java
index 0df17edd..0a7a7cb2 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationRequestMetadata.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationRequestMetadata.java
@@ -14,9 +14,12 @@
package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
+import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -169,7 +172,17 @@ public class ValidateRegistrationRequestMetadata extends AbstractProfileAction {
final Object value = requestMetadata.get(claim);
log.debug("{} Claim {} set in policy included in the request: {}", getLogPrefix(), claim,
value == null);
- final Pair<Object,Boolean> result = metadataPolicyEnforcer.apply(value, policy);
+ final Pair<Object,Boolean> result;
+ if ("scope".equals(claim) && value instanceof String string) {
+ result = metadataPolicyEnforcer.apply(Arrays.asList(string.split(" ")), policy);
+ if (result.getFirst() instanceof List<?> list) {
+ result.setFirst(list.stream()
+ .filter(String.class::isInstance).map(String.class::cast)
+ .collect(Collectors.joining(" ")));
+ }
+ } else {
+ result = metadataPolicyEnforcer.apply(value, policy);
+ }
final Boolean enforcerResult = result != null ? result.getSecond() : null;
if (enforcerResult == null || !enforcerResult.booleanValue()) {
log.warn("{} Metadata claim {} is not compliant with the policy", getLogPrefix(), claim);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
index 18006b13..8eecbfc7 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RegistrationFlowTest.java
@@ -36,6 +36,7 @@ import com.nimbusds.langtag.LangTag;
import com.nimbusds.langtag.LangTagException;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.client.RegistrationError;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
@@ -85,7 +86,14 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
assertErrorCode(result, "invalid_client_metadata");
}
-
+
+ @Test
+ public void testInvalidScopeMessage() throws Exception {
+ setJsonRequest("POST", "{ \"redirect_uris\":[\"" + redirectUri + "\"], \"scope\":[\"openid\"] }");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_client_metadata");
+ }
+
@Test
public void testUnauthenticated_nonCompliantWithProfilePolicy1() {
setJsonRequest("POST", "{ \"redirect_uris\":[\"https://not.compliant.org/cb\"] }");
@@ -100,6 +108,13 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
assertErrorCode(result, "invalid_client_metadata");
}
+ @Test
+ public void testUnauthenticated_nonCompliantWithProfilePolicy3() {
+ setJsonRequest("POST", "{ \"redirect_uris\":[\"" + redirectUri + "\"], \"scope\":\"openid notAllowed\" }");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_client_metadata");
+ }
+
@Test
public void testUnauthenticated_success() throws Exception {
final String requestUri = "https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA";
@@ -124,6 +139,38 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(), metadata.getRedirectionURIStrings());
Assert.assertEquals(storedInfo.getOIDCMetadata().getRequestObjectURIs(), Set.of(new URI(requestUri)));
Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
+ final Scope registeredScope = storedInfo.getOIDCMetadata().getScope();
+ Assert.assertTrue(registeredScope.equals(Scope.parse("openid profile email address phone offline_access")));
+ }
+
+ @Test
+ public void testUnauthenticated_success_customScope() throws Exception {
+ final String scope = "openid profile";
+ final String requestUri = "https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA";
+ setJsonRequest("POST", "{ \"redirect_uris\":[\"" + redirectUri + "\"], \"request_uris\":[\"" + requestUri
+ + "\"], \"scope\": \"" + scope + "\" }");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCClientInformationResponse parsedResponse =
+ parseSuccessResponse(result, OIDCClientInformationResponse.class);
+ final OIDCClientInformation clientInfo = parsedResponse.getOIDCClientInformation();
+ final OIDCClientMetadata metadata = clientInfo.getOIDCMetadata();
+ final String clientId = clientInfo.getID().getValue();
+ assert clientId != null;
+ assert storageService != null;
+ final StorageRecord<String> storageRecord = storageService.read(BaseStorageServiceClientInformationComponent.CONTEXT_NAME,
+ clientId);
+ assert storageRecord != null;
+ final String record = storageRecord.getValue();
+ Assert.assertNotNull(record);
+ final JSONParser parser = new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE);
+ final OIDCClientInformation storedInfo = OIDCClientInformation.parse((JSONObject) parser.parse(record));
+ Assert.assertEquals(storedInfo.getID(), clientInfo.getID());
+ Assert.assertEquals(storedInfo.getSecret(), clientInfo.getSecret());
+ Assert.assertEquals(storedInfo.getOIDCMetadata().getRedirectionURIStrings(), metadata.getRedirectionURIStrings());
+ Assert.assertEquals(storedInfo.getOIDCMetadata().getRequestObjectURIs(), Set.of(new URI(requestUri)));
+ Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
+ final Scope registeredScope = storedInfo.getOIDCMetadata().getScope();
+ Assert.assertTrue(registeredScope.equals(Scope.parse(scope)));
}
@Test
@@ -170,6 +217,8 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
Assert.assertEquals(storedInfo.getOIDCMetadata().getBackChannelLogoutURI(), new URI("https://example.org/backChannel"));
Assert.assertTrue(storedInfo.getOIDCMetadata().requiresFrontChannelLogoutSession());
Assert.assertTrue(storedInfo.getOIDCMetadata().requiresBackChannelLogoutSession());
+ final Scope registeredScope = storedInfo.getOIDCMetadata().getScope();
+ Assert.assertTrue(registeredScope.equals(Scope.parse("openid profile email address phone offline_access")));
}
@Test
@@ -199,6 +248,8 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
Assert.assertTrue(Boolean.valueOf("" + metadata.getCustomField("dpop_bound_access_tokens")));
Assert.assertFalse(metadata.requiresPushedAuthorizationRequests());
+ final Scope registeredScope = storedInfo.getOIDCMetadata().getScope();
+ Assert.assertTrue(registeredScope.equals(Scope.parse("openid profile email address phone offline_access")));
}
@Test
@@ -228,6 +279,8 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
Assert.assertTrue(metadata.getRedirectionURIStrings().contains(redirectUri));
Assert.assertTrue(metadata.requiresPushedAuthorizationRequests());
Assert.assertFalse(Boolean.valueOf("" + metadata.getCustomField("dpop_bound_access_tokens")));
+ final Scope registeredScope = storedInfo.getOIDCMetadata().getScope();
+ Assert.assertTrue(registeredScope.equals(Scope.parse("openid profile email address phone offline_access")));
}
@Test
@@ -246,6 +299,14 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
assertErrorCode(result, RegistrationError.INVALID_CLIENT_METADATA.getCode());
}
+ @Test
+ public void testAccessToken_nonCompliantWithProfilePolicy3() throws Exception {
+ setJsonRequest("POST", "{ \"redirect_uris\":[\"" + redirectUri + "\"], \"scope\":\"openid notAllowed\" }");
+ request.addHeader("Authorization", buildRegistrationAccessToken(false, "[\"https://example.org/cb\"]"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, RegistrationError.INVALID_CLIENT_METADATA.getCode());
+ }
+
@Test
public void testAccessToken_success() throws Exception {
setJsonRequest("POST", buildRequestMessage(redirectUri));
@@ -253,6 +314,41 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
assertSuccessfulResponse(flowExecutor.launchExecution(FLOW_ID, null, externalContext), null);
}
+ @Test
+ public void testAccessToken_successNonDefaultScope() throws Exception {
+ final String scope = "openid profile email";
+ setJsonRequest("POST", buildRequestMessage(redirectUri, "\"scope\":\"" + scope + "\""));
+ request.addHeader("Authorization", buildRegistrationAccessToken(false, "[\"https://example.org/cb\"]"));
+ assertSuccessfulResponse(flowExecutor.launchExecution(FLOW_ID, null, externalContext), null,
+ Scope.parse(scope));
+ }
+
+ @Test
+ public void testAccessToken_failsWithStricterPolicyNonDefaultScope() throws Exception {
+ final String scope = "openid profile email";
+ setJsonRequest("POST", buildRequestMessage(redirectUri, "\"scope\":\"" + scope + "\""));
+ final String policyJson = "{"
+ + "\"redirect_uris\":{\"subset_of\" : [\"https://example.org/cb\"] },"
+ + "\"scope\":{\"subset_of\" : [\"openid\", \"profile\"] }"
+ + "}";
+ request.addHeader("Authorization", buildRegistrationAccessTokenWithPolicy(false, policyJson));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, RegistrationError.INVALID_CLIENT_METADATA.getCode());
+ }
+
+ @Test
+ public void testAccessToken_successWithStricterPolicyNonDefaultScope() throws Exception {
+ final String scope = "openid";
+ setJsonRequest("POST", buildRequestMessage(redirectUri, "\"scope\":\"" + scope + "\""));
+ final String policyJson = "{"
+ + "\"redirect_uris\":{\"subset_of\" : [\"https://example.org/cb\"] },"
+ + "\"scope\":{\"subset_of\" : [\"openid\", \"profile\"] }"
+ + "}";
+ request.addHeader("Authorization", buildRegistrationAccessTokenWithPolicy(false, policyJson));
+ assertSuccessfulResponse(flowExecutor.launchExecution(FLOW_ID, null, externalContext), null,
+ Scope.parse(scope));
+ }
+
@Test
public void testAccessToken_success_withClientID() throws Exception {
clientId = "https://example.org";
@@ -374,6 +470,11 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
}
protected void assertSuccessfulResponse(final FlowExecutionResult result, final String clientId) throws Exception {
+ assertSuccessfulResponse(result, clientId, Scope.parse("openid profile email address phone offline_access"));
+ }
+
+ protected void assertSuccessfulResponse(final FlowExecutionResult result, final String clientId,
+ final Scope scope) throws Exception {
final OIDCClientInformationResponse parsedResponse =
parseSuccessResponse(result, OIDCClientInformationResponse.class);
final OIDCClientInformation clientInfo = OIDCClientInformation.parse(
@@ -408,6 +509,7 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
}
Assert.assertEquals(storedMetadata.getPolicyURIEntries(), metadata.getPolicyURIEntries());
Assert.assertNull(clientInfo.getSecret().getExpirationDate());
+ Assert.assertEquals(storedMetadata.getScope(), scope);
}
@Nonnull protected String buildRegistrationAccessToken(final boolean replacement, final String redirectUriSubset,
@@ -428,6 +530,12 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
metadata.append("\"redirect_uris\":{\"subset_of\":" + redirectUriSubset + "}");
}
+ return buildRegistrationAccessTokenWithPolicy(replacement,
+ metadata.length() == 0 ? null : "{ " + metadata.toString() + " }");
+ }
+
+ @Nonnull protected String buildRegistrationAccessTokenWithPolicy(final boolean replacement,
+ final String metadataPolicy) throws Exception {
final String json = "{" +
"\"prncpl\":\"jdoe\"," +
"\"type\":\"rat\"," +
@@ -437,7 +545,7 @@ public class RegistrationFlowTest extends AbstractOidcFlowTest {
"\"rp_id\":\"" + rpId + "\"," +
(clientId != null ? ("\"client_id\":\"" + clientId + "\",") : "") +
"\"replacement\":" + Boolean.toString(replacement) + "," +
- "\"metadata\":" + (metadata.length() == 0 ? "null" : "{" + metadata.toString()) + "}" +
+ "\"metadata\":" + (metadataPolicy == null ? "null" : metadataPolicy) +
"}";
final BearerAccessToken token = new BearerAccessToken(getDataSealer().wrap(json,
Instant.now().plusSeconds(30)));
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list