[java-idp-oidc] 01/02: JOIDC-229 - Provide method for strict scope validation
Henri Mikkonen
henri.mikkonen at iki.fi
Wed Oct 2 12:36:41 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=cb56b9841e22dd4a68a6f5935c79dca57f37e85b
commit cb56b9841e22dd4a68a6f5935c79dca57f37e85b
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Oct 2 15:34:12 2024 +0300
JOIDC-229 - Provide method for strict scope validation
https://shibboleth.atlassian.net/browse/JOIDC-229
- Exploit the strictScopeValidation profile config setting
- OIDC.SSO and OAUTH2.Token: idp.oidc.strictScopeValidation -property, defaults to false
- OAUTH2.PAR: defaults to true
---
.../oidc/op/oauth2/profile/impl/ValidateScope.java | 53 ++++++++++++++--
.../idp/service/relying-party/postconfig.xml | 19 +++++-
.../idp/plugin/oidc/op/conf/oidc.properties | 6 +-
.../oidc/op/profile/flow/AuthorizeFlowTest.java | 74 ++++++++++++++++++++++
.../flow/ClientCredentialsTokenFlowTest.java | 11 ++++
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 14 ++++
.../shibboleth/idp/module/conf/relying-party.xml | 9 +++
7 files changed, 177 insertions(+), 9 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
index 0a090c62..78cf415c 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateScope.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
import java.util.Iterator;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -36,6 +37,7 @@ import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoScop
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.profile.config.logic.StrictScopeValidationPredicate;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
import net.shibboleth.shared.logic.Constraint;
@@ -80,6 +82,9 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
/** Lookup strategy for fetching the requested response type. */
@Nullable private Function<ProfileRequestContext, ResponseType> requestedResponseTypeLookupStrategy;
+ /** Condition whether to apply strict scope validation, i.e. unallowed scope is an error. */
+ @Nonnull private Predicate<ProfileRequestContext> strictScopeValidationCondition;
+
/** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
@Nonnull
private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
@@ -98,6 +103,7 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
new OIDCAuthenticationResponseContextLookupFunction());
assert tccls != null;
tokenClaimsContextLookupStrategy = tccls;
+ strictScopeValidationCondition = new StrictScopeValidationPredicate();
}
/**
@@ -173,6 +179,17 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
requestedResponseTypeLookupStrategy = strategy;
}
+ /**
+ * Set the condition whether to apply strict scope validation, i.e. unallowed scope is an error.
+ *
+ * @param predicate What to set.
+ */
+ public void setStrictScopeValidationCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+ strictScopeValidationCondition = Constraint.isNotNull(predicate,
+ "StrictScopeValidationCondition cannot be null");
+ }
+
// Checkstyle: CyclomaticComplexity OFF
// Checkstyle: MethodLength OFF
/** {@inheritDoc} */
@@ -227,6 +244,14 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
}
}
}
+
+ final boolean strictValidation = strictScopeValidationCondition.test(profileRequestContext);
+ if (strictValidation && requestedScopes != null && !requestedScopes.isEmpty() &&
+ (allowedScopes == null || allowedScopes.isEmpty())) {
+ log.warn("{} Strict scope validation is active and no allowed scopes for RP {}", getLogPrefix(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_SCOPE);
+ return;
+ }
if (allowedScopes == null || allowedScopes.isEmpty()) {
log.debug("{} No allowed scope for client {}, nothing to do", getLogPrefix(), clientId);
return;
@@ -241,14 +266,28 @@ public class ValidateScope extends AbstractOAuthAuthorizationResponseAction {
for (Iterator<Scope.Value> i = requestedScopes.iterator(); i.hasNext();) {
final Scope.Value scope = i.next();
if (!allowedScopes.contains(scope)) {
- log.warn("{} Removing requested but unregistered scope {} for RP {}", getLogPrefix(), scope.getValue(),
- clientId);
- i.remove();
+ if (strictValidation) {
+ log.warn("{} Requested unregistered scope {} for RP {} with strict validation", getLogPrefix(),
+ scope.getValue(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_SCOPE);
+ return;
+ } else {
+ log.warn("{} Removing requested but unregistered scope {} for RP {}", getLogPrefix(),
+ scope.getValue(), clientId);
+ i.remove();
+ }
} else if (previouslyGrantedScopes != null && !previouslyGrantedScopes.contains(scope)) {
- log.warn("{} Removing requested but previously ungranted scope {} for RP {}", getLogPrefix(),
- scope.getValue(), clientId);
- i.remove();
- reducedRequestedScopes = true;
+ if (strictValidation) {
+ log.warn("{} Requested previously ungranted scope {} for RP {}", getLogPrefix(),
+ scope.getValue(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_SCOPE);
+ return;
+ } else {
+ log.warn("{} Removing requested but previously ungranted scope {} for RP {}", getLogPrefix(),
+ scope.getValue(), clientId);
+ i.remove();
+ reducedRequestedScopes = true;
+ }
}
}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 27dc2463..20b23060 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -100,7 +100,8 @@
p:dpopProofClaimsValidator-ref="DefaultDPoPProofClaimsValidator"
p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}"
p:requestUriType="%{idp.oauth2.par.requestUriType:}"
- p:requestUriLifetime="%{idp.oauth2.par.requestUriLifetime:PT1M}"/>
+ p:requestUriLifetime="%{idp.oauth2.par.requestUriLifetime:PT1M}"
+ p:strictScopeValidation="true"/>
<bean id="DefaultLogoutHintMatchingPredicate"
class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultLogoutHintMatchingPredicate"/>
@@ -302,6 +303,14 @@
p:propertyType="#{T(java.util.function.Function)}"
p:defaultValue-ref="DefaultOAuth2DPoPNonceGenerator" />
</property>
+ <property name="strictScopeValidationPredicate">
+ <bean class="net.shibboleth.shared.logic.PredicateSupport" factory-method="fromFunction">
+ <constructor-arg>
+ <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="strictScopeValidation" />
+ </constructor-arg>
+ <constructor-arg value="%{idp.oidc.strictScopeValidation:false}" />
+ </bean>
+ </property>
</bean>
<bean id="OIDC.SSO.MDDriven" parent="AbstractMDDrivenOIDCSSOProfile" lazy-init="true"
@@ -740,6 +749,14 @@
<bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="requestUriLifetime"
p:defaultValue="%{idp.oauth2.par.requestUriLifetime:PT1M}" />
</property>
+ <property name="strictScopeValidationPredicate">
+ <bean class="net.shibboleth.shared.logic.PredicateSupport" factory-method="fromFunction">
+ <constructor-arg>
+ <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="strictScopeValidation" />
+ </constructor-arg>
+ <constructor-arg value="true" />
+ </bean>
+ </property>
</bean>
<!-- Default client-auth JWT validation wiring. -->
diff --git a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
index 7a4e15b1..7d602743 100644
--- a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
@@ -204,4 +204,8 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
#idp.oauth2.jwtAuth.audienceValidator.endpointTargets = /profile/oauth2/introspection,/profile/oauth2/revocation,/profile/oauth2/pushed-authorization
# Bean to determine whether refresh token is issuance is activated
-#idp.oauth2.refreshToken.activation = DefaultRefreshTokenActivationCondition
\ No newline at end of file
+#idp.oauth2.refreshToken.activation = DefaultRefreshTokenActivationCondition
+
+# Set to true to enable strict scope validation, i.e. unallowed requested scope is considered as an error (defaults to false)
+# Affects OIDC.SSO and OAUTH2.Token profile configurations
+#idp.oidc.strictScopeValidation = true
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index f46445dc..3a54eff1 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -171,6 +171,80 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNull(successResponse.getIssuer());
}
+ @Test
+ public void testWithAuthorizationCodeFlowUnallowedScopeNotStrict() throws IOException, SessionException {
+ setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "notAllowed"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthorizationResponse responseMessage = parseSuccessResponse(result, AuthorizationResponse.class);
+ final AuthorizationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertNull(successResponse.getIssuer());
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlowOpenidUnallowedScopeNotStrict() throws IOException, SessionException {
+ setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid notAllowed"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthorizationResponse responseMessage = parseSuccessResponse(result, AuthorizationResponse.class);
+ final AuthorizationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertNull(successResponse.getIssuer());
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlowUnallowedScopeStrict() throws IOException, SessionException {
+ final String clientId = "mockClientIdStrictScope";
+ setRequestParameters(List.of(new Pair<>("client_id", clientId),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "notAllowed"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_scope");
+ removeMetadata(storageService, clientId);
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlowOpenidUnallowedScopeStrict() throws IOException, SessionException {
+ final String clientId = "mockClientIdStrictScope";
+ setRequestParameters(List.of(new Pair<>("client_id", clientId),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid notAllowed"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_scope");
+ removeMetadata(storageService, clientId);
+ }
+
@Test
public void testWithAuthorizationCodeFlow_parRequiredByProfileNotProvided() throws IOException, SessionException {
setRequestParameters(List.of(new Pair<>("client_id", clientIdRequirePAR),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
index cb32a14e..058729b8 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
@@ -138,6 +138,17 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
Collections.singletonList(resource), "eduPersonScopedAffiliation");
}
+ @Test
+ public void testNoScopeRegisteredStrictValidation() throws Exception {
+ final String clientId = "mockClientIdStrictScope";
+ setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+ storeMetadata(storageService, clientId, clientSecret, null);
+ setBasicAuth(clientId, clientSecret);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_SCOPE_CODE);
+ removeMetadata(storageService, clientId);
+ }
+
@Test
public void testNoScopeRequestedNorRegistered() throws Exception {
setHttpFormRequest("POST", createRequestParameters(clientId, null, resource));
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 82a43ee5..fa9af095 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -206,6 +206,20 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(response.getTokens().getAccessToken());
}
+ @Test
+ public void testNoScopesRegisteredWithStrictScopeValidation() throws Exception {
+ final String clientId = "mockClientIdStrictScope";
+ setHttpFormRequest("POST",
+ createRequestParameters(redirectUri, "authorization_code",
+ buildAuthorizationCode(clientId, null, "profile"), clientId));
+ storeMetadata(storageService, clientId, clientSecret, null);
+ setBasicAuth(clientId, clientSecret);
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, "invalid_scope");
+ removeMetadata(storageService, clientId);
+ }
+
@Test
public void testNoScopesRegisteredWithoutOpenidScopeRequestedNoResourcesInMetadata() throws Exception {
setHttpFormRequest("POST",
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index c75c3c7b..91580544 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -116,6 +116,15 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdStrictScope">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO" p:strictScopeValidation="true" />
+ <bean parent="OAUTH2.Token" p:strictScopeValidation="true" />
+ <bean parent="OAUTH2.PAR" p:strictScopeValidation="true" />
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdNotMDDrivenRefreshTokenJwt">
<property name="profileConfigurations">
<list>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list