[java-idp-oidc] 27/31: JOIDC-222 - Support for OpenID Federation
Henri Mikkonen
henri.mikkonen at iki.fi
Tue Jun 24 08:53:03 UTC 2025
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch dev/JOIDC-222
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=7041daf7ca06b273bc74e064753a053b328f118d
commit 7041daf7ca06b273bc74e064753a053b328f118d
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 9 15:33:08 2025 +0300
JOIDC-222 - Support for OpenID Federation
https://shibboleth.atlassian.net/browse/JOIDC-222
- Improve logic for initiating automatic registration in authorize and PAR -flows
- Solely start trust chain resolution if:
- request object is not a plain one
- client_id in the request object matches with the one in the request query
- PAR request must be authenticated via request object or private_key_jwt
- Customizable via idp.oidfed.par.customAutomaticRegistrationCondition and idp.oidfed.authorize.customAutomaticRegistrationCondition
- Improve federation-specific request object validation to authorize and PAR -flows
- included claim validator for the mandatory client_id -claim
---
.../pushed-authorization-beans.xml | 42 +++++++++-
.../idp/flows/oidc/authorize/authorize-beans.xml | 34 ++++++--
.../AuthorizeFlowAutomaticRegistrationTest.java | 93 +++++++++++++++++++++
...shedAuthorizeFlowAutomaticRegistrationTest.java | 94 +++++++++++++++++++++-
4 files changed, 254 insertions(+), 9 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
index 8b499cd4..e0315e28 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -318,6 +318,14 @@
p:requiredClaims="exp" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
p:prohibitedClaims="sub" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="client_id">
+ <property name="valueToMatchLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression"
+ c:expression="#custom.apply(#input1.getInboundMessageContext()) == null ? null : #custom.apply(#input1.getInboundMessageContext()).toString()"
+ p:customObject-ref="shibboleth.ClientIDLookupStrategy" />
+ </property>
+ </bean>
</util:list>
<bean id="DefaultParUseOnlyRequestObjectCondition" parent="shibboleth.BiConditions.Expression"
@@ -431,6 +439,38 @@
</property>
</bean>
- <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.par.automaticRegistrationCondition:shibboleth.Conditions.FALSE}" />
+ <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.par.customAutomaticRegistrationCondition:DefaultAutomaticRegistrationCondition}" />
+
+ <bean id="DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
+ <constructor-arg>
+ <list>
+ <ref bean="%{idp.oidfed.par.automaticRegistrationCondition:shibboleth.Conditions.FALSE}"/>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage() instanceof T(com.nimbusds.oauth2.sdk.PushedAuthorizationRequest)" />
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="!(#input.ensureInboundMessageContext().getMessage().getAuthorizationRequest().getRequestObject() instanceof T(com.nimbusds.jwt.PlainJWT))" />
+ <bean parent="shibboleth.Conditions.OR">
+ <constructor-arg>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getAuthorizationRequest().specifiesRequestObject()" />
+ </constructor-arg>
+ <constructor-arg>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getClientAuthentication() != null and #input.ensureInboundMessageContext().getMessage().getClientAuthentication().getMethod().equals(T(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod).PRIVATE_KEY_JWT)" />
+ </constructor-arg>
+ </bean>
+ <bean parent="shibboleth.Conditions.OR">
+ <constructor-arg>
+ <list>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="!(#input.ensureInboundMessageContext().getMessage().getAuthorizationRequest().getRequestObject() instanceof T(com.nimbusds.jwt.SignedJWT))" />
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getAuthorizationRequest().getClientID().getValue().equals(#input.ensureInboundMessageContext().getMessage().getAuthorizationRequest().getRequestObject().getJWTClaimsSet().getClaim('client_id'))" />
+ </list>
+ </constructor-arg>
+ </bean>
+ </list>
+ </constructor-arg>
+ </bean>
</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index d6bd260b..3aad9088 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -468,6 +468,14 @@
p:requiredClaims="exp" />
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ProhibitedClaimsValidator"
p:prohibitedClaims="sub" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="client_id">
+ <property name="valueToMatchLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression"
+ c:expression="#custom.apply(#input1.getInboundMessageContext()) == null ? null : #custom.apply(#input1.getInboundMessageContext()).toString()"
+ p:customObject-ref="shibboleth.ClientIDLookupStrategy" />
+ </property>
+ </bean>
</util:list>
<bean id="DefaultRequestRedirectURILookupFunction"
@@ -1199,13 +1207,27 @@
</constructor-arg>
</bean>
- <bean id="AutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
- <constructor-arg>
- <ref bean="%{idp.oidfed.authorize.automaticRegistrationCondition:shibboleth.Conditions.FALSE}"/>
- </constructor-arg>
+ <alias alias="AutomaticRegistrationCondition" name="%{idp.oidfed.authorize.customAutomaticRegistrationCondition:DefaultAutomaticRegistrationCondition}" />
+
+ <bean id="DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
<constructor-arg>
- <bean parent="shibboleth.Conditions.Expression"
- c:expression="#input.ensureInboundMessageContext().getMessage() instanceof T(com.nimbusds.oauth2.sdk.AuthorizationRequest) and #input.ensureInboundMessageContext().getMessage().specifiesRequestObject()" />
+ <list>
+ <ref bean="%{idp.oidfed.authorize.automaticRegistrationCondition:shibboleth.Conditions.FALSE}"/>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage() instanceof T(com.nimbusds.oauth2.sdk.AuthorizationRequest) and #input.ensureInboundMessageContext().getMessage().specifiesRequestObject()" />
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="!(#input.ensureInboundMessageContext().getMessage().getRequestObject() instanceof T(com.nimbusds.jwt.PlainJWT))" />
+ <bean parent="shibboleth.Conditions.OR">
+ <constructor-arg>
+ <list>
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="!(#input.ensureInboundMessageContext().getMessage().getRequestObject() instanceof T(com.nimbusds.jwt.SignedJWT))" />
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().getMessage().getClientID().getValue().equals(#input.ensureInboundMessageContext().getMessage().getRequestObject().getJWTClaimsSet().getClaim('client_id'))" />
+ </list>
+ </constructor-arg>
+ </bean>
+ </list>
</constructor-arg>
</bean>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 592d071c..8bb080d9 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -85,6 +85,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
final FlowExecutionResult result =
launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
"iss", clientId,
+ "client_id", clientId,
"aud", issuer,
"exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
"jti", UUID.randomUUID(),
@@ -100,6 +101,24 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
}
+ @Test
+ public void testWithValidTrustChain_signedRequestObject_nonMatchingClientId()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId + "2", "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
@Test
public void testWithValidTrustChain_signedRequestObject_missingJti()
throws IOException, UnsupportedOperationException, URISyntaxException {
@@ -108,6 +127,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
final FlowExecutionResult result =
launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
"iss", clientId,
+ "client_id", clientId,
"aud", issuer,
"exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
"response_type", "code",
@@ -124,6 +144,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
final FlowExecutionResult result =
launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
"iss", clientId,
+ "client_id", clientId,
"aud", issuer,
"jti", UUID.randomUUID(),
"response_type", "code",
@@ -132,6 +153,76 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
assertErrorCode(result, "invalid_request_object");
}
+ @Test
+ public void testWithValidTrustChain_signedRequestObject_invalidIss()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId + "2",
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ assertErrorCode(result, "invalid_request_object");
+ }
+
+ @Test
+ public void testWithValidTrustChain_signedRequestObject_missingIss()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ assertErrorCode(result, "invalid_request_object");
+ }
+
+ @Test
+ public void testWithValidTrustChain_signedRequestObject_invalidClientId()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "client_id", clientId + "2",
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
+ @Test
+ public void testWithValidTrustChain_signedRequestObject_missingClientId()
+ throws IOException, UnsupportedOperationException, URISyntaxException {
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId);
+ final FlowExecutionResult result =
+ launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+ "iss", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid profile",
+ "redirect_uri", redirectUri)));
+ Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+ }
+
@Test
public void testWithValidTrustChain_signedRequestObject_containsForbiddenSub()
throws IOException, UnsupportedOperationException, URISyntaxException {
@@ -140,6 +231,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
final FlowExecutionResult result =
launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
"iss", clientId,
+ "client_id", clientId,
"sub", "mockValue",
"aud", issuer,
"exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
@@ -158,6 +250,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
final FlowExecutionResult result =
launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
"iss", clientId,
+ "client_id", clientId,
"aud", List.of(issuer, "https://another.example.org"),
"exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
"jti", UUID.randomUUID(),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
index ddf5dd40..6747f192 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
@@ -81,7 +81,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
setHttpFormRequest("POST", createRequestParameters(clientId));
final FlowExecutionResult result =
flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
}
@Test
@@ -132,6 +132,96 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
}
+ @Test
+ public void testWithPublicClientWithSignedRequestObject_missingIss() throws Exception {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setResponseTypes(Set.of(ResponseType.CODE));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId, metadata);
+ setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+ "client_id", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid",
+ "redirect_uri", redirectUri)).serialize()));
+ final FlowExecutionResult result =
+ flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ }
+
+ @Test
+ public void testWithPublicClientWithSignedRequestObject_invalidIss() throws Exception {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setResponseTypes(Set.of(ResponseType.CODE));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId, metadata);
+ setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+ "client_id", clientId,
+ "iss", clientId + "2",
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid",
+ "redirect_uri", redirectUri)).serialize()));
+ final FlowExecutionResult result =
+ flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ }
+
+ @Test
+ public void testWithPublicClientWithSignedRequestObject_missingClientId() throws Exception {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setResponseTypes(Set.of(ResponseType.CODE));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId, metadata);
+ setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+ "iss", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid",
+ "redirect_uri", redirectUri)).serialize()));
+ final FlowExecutionResult result =
+ flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+ }
+
+ @Test
+ public void testWithPublicClientWithSignedRequestObject_invalidClientId() throws Exception {
+ final OIDCClientMetadata metadata = new OIDCClientMetadata();
+ metadata.setRedirectionURI(new URI(redirectUri));
+ metadata.setResponseTypes(Set.of(ResponseType.CODE));
+ metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
+ metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+ final String clientId = uniqueClientId();
+ configureMockHttpClient(clientId, metadata);
+ setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+ "client_id", clientId + "2",
+ "iss", clientId,
+ "aud", issuer,
+ "exp", Instant.now().plus(Duration.ofMinutes(5)).toEpochMilli(),
+ "jti", UUID.randomUUID(),
+ "response_type", "code",
+ "scope", "openid",
+ "redirect_uri", redirectUri)).serialize()));
+ final FlowExecutionResult result =
+ flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+ }
+
@Test
public void testWithPublicClientWithSignedRequestObject_missingExp() throws Exception {
final OIDCClientMetadata metadata = new OIDCClientMetadata();
@@ -221,7 +311,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
"redirect_uri", redirectUri)).serialize()));
final FlowExecutionResult result =
flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
}
protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri) {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list