[java-idp-oidc] branch main updated: JOIDC-230 - Add flag to block additional audiences from initial access token
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Oct 4 10:29:49 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=fb4dcc49c99202b9b40e0df7ddbfa32800c21f7b
The following commit(s) were added to refs/heads/main by this push:
new fb4dcc49 JOIDC-230 - Add flag to block additional audiences from initial access token
fb4dcc49 is described below
commit fb4dcc49c99202b9b40e0df7ddbfa32800c21f7b
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Oct 4 13:29:29 2024 +0300
JOIDC-230 - Add flag to block additional audiences from initial access token
https://shibboleth.atlassian.net/browse/JOIDC-230
The access token audience is limited to self when the following conditions are met
- The new profile configuration flag 'limitInitialAccessTokenToSelf' is set to true
- The inbound message is TokenRequest with authorization_code grant
- The openid scope is involved
- If not, a warning is logged that the flag is not respected
---
.../op/oauth2/profile/impl/ValidateAudience.java | 33 ++++++++-
.../logic/DefaultEnforceSelfAudienceCondition.java | 78 ++++++++++++++++++++++
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 60 +++++++++++++++++
.../shibboleth/idp/module/conf/relying-party.xml | 16 +++++
4 files changed, 185 insertions(+), 2 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
index d493eb1b..a655bac0 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
@@ -36,6 +36,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.TokenRequest
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoAudienceLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCAuthenticationResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultEnforceSelfAudienceCondition;
import net.shibboleth.idp.plugin.oidc.op.profile.logic.IssueIDTokenCondition;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.oidc.profile.core.OidcEventIds;
@@ -78,7 +79,10 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
/** Whether the request includes the OP as an audience. */
@Nonnull private Predicate<ProfileRequestContext> selfAudienceCondition;
-
+
+ /** Whether to enforce solely the OP as audience. */
+ @Nonnull private Predicate<ProfileRequestContext> enforceSelfAudienceCondition;
+
/** Constructor. */
public ValidateAudience() {
requestedAudienceLookupStrategy = new TokenRequestAudienceLookupFunction();
@@ -95,6 +99,7 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
// openid scope -> we're issuing an ID token -> the OP will be an audience for the access token
selfAudienceCondition = new IssueIDTokenCondition();
+ enforceSelfAudienceCondition = new DefaultEnforceSelfAudienceCondition();
}
/**
@@ -159,7 +164,21 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
selfAudienceCondition = Constraint.isNotNull(condition, "Self audience condition cannot be null");
}
-
+
+ /**
+ * Set whether to enforce solely the OP as audience.
+ *
+ * @param condition condition to set
+ *
+ * @since 4.2.0
+ */
+ public void setEnforceSelfAudienceCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ enforceSelfAudienceCondition =
+ Constraint.isNotNull(condition, "Enforce self audience condition cannot be null");
+ }
+
// Checkstyle: CyclomaticComplexity|MethodLength OFF
/** {@inheritDoc} */
@Override
@@ -201,6 +220,16 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
return;
}
+ final boolean enforceSelfAudience = enforceSelfAudienceCondition.test(profileRequestContext);
+ if (enforceSelfAudience) {
+ if (allowNone) {
+ log.debug("{} Self-audience enforced for {}, OP will be sole audience", getLogPrefix(), clientId);
+ return;
+ } else {
+ log.warn("{} Ignoring the self-audience enforcing for {}, as self-audience is not allowed",
+ getLogPrefix(), clientId);
+ }
+ }
if (requestedAudience == null) {
// With none requested, simply swap requested for previously granted, if any.
// Set previous set to null since there's no need to filter against it.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultEnforceSelfAudienceCondition.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultEnforceSelfAudienceCondition.java
new file mode 100644
index 00000000..8f3e9543
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultEnforceSelfAudienceCondition.java
@@ -0,0 +1,78 @@
+/*
+ * 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 net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+
+import net.shibboleth.oidc.profile.config.logic.LimitInitialAccessTokenToSelfPredicate;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A predicate that returns true if all the following conditions are met:
+ * <ul>
+ * <li>{@link DefaultEnforceSelfAudienceCondition#limitInitialAccessTokenToSelfPredicate} returns true</li>
+ * <li>The inbound message is a {@link TokenRequest}</li>
+ * <li>The token request grant_type is authorization_code</li>
+ * </ul>
+ */
+public class DefaultEnforceSelfAudienceCondition implements Predicate<ProfileRequestContext> {
+
+ /** Whether the initial access token audience is solely to self (i.e. UserInfo) use. */
+ @Nonnull private Predicate<ProfileRequestContext> limitInitialAccessTokenToSelfPredicate;
+
+ /**
+ * Constructor.
+ */
+ public DefaultEnforceSelfAudienceCondition() {
+ this(new LimitInitialAccessTokenToSelfPredicate());
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param condition whether the initial access token audience is solely to self (i.e. UserInfo) use
+ */
+ public DefaultEnforceSelfAudienceCondition(@Nonnull @ParameterName(name = "limitInitialAccessTokenToSelfPredicate")
+ final Predicate<ProfileRequestContext> condition) {
+ limitInitialAccessTokenToSelfPredicate = Constraint.isNotNull(condition,
+ "Self initial access token predicate cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext profileRequestContext) {
+ if (limitInitialAccessTokenToSelfPredicate.test(profileRequestContext)) {
+ assert profileRequestContext != null;
+ if (profileRequestContext.getInboundMessageContext() == null) {
+ return false;
+ }
+ if (profileRequestContext.ensureInboundMessageContext().getMessage()
+ instanceof TokenRequest tokenRequest) {
+ return GrantType.AUTHORIZATION_CODE.equals(tokenRequest.getAuthorizationGrant().getType());
+ }
+ }
+ return false;
+ }
+
+}
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 fa9af095..86e6beb3 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
@@ -293,6 +293,55 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertTrue(audience.contains(issuer));
}
+ @Test
+ public void testValidWithUriResourceLimitedAudience() throws Exception {
+ final String clientId = "mockClientIdLimitedInitialAudience";
+ final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+ buildAuthorizationCode(clientId, null, "openid profile email offline_access",
+ List.of(resourceUri)), clientId);
+ requestParameters.put("resource", resourceUri);
+ initializeGrantAndRequest(clientId, requestParameters);
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+ resourceMetadata.setScope(Scope.parse("openid profile email offline_access"));
+ storeMetadataObject(storageService, resourceUri, clientSecret, resourceMetadata);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+ Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+ final List<String> audience = getAudienceFromAccessToken(response.getTokens().getAccessToken());
+ Assert.assertEquals(audience.size(), 1);
+ Assert.assertTrue(audience.contains(issuer));
+ removeMetadata(storageService, clientId);
+ }
+
+ @Test
+ public void testValidWithUriResourceLimitedAudienceJwt() throws Exception {
+ final String clientId = "mockClientIdLimitedInitialAudienceJwt";
+ final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
+ buildAuthorizationCode(clientId, null, "openid profile email offline_access",
+ List.of(resourceUri)), clientId);
+ requestParameters.put("resource", resourceUri);
+ initializeGrantAndRequest(clientId, requestParameters);
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final OIDCClientMetadata resourceMetadata = buildMetadataSkeleton();
+ resourceMetadata.setScope(Scope.parse("openid profile email offline_access"));
+ storeMetadataObject(storageService, resourceUri, clientSecret, resourceMetadata);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNull(getSidFromAccessToken(response.getTokens().getAccessToken()));
+ Assert.assertNull(getSidFromRefreshToken(response.getTokens().getRefreshToken()));
+ final SignedJWT jwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+ final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+ Assert.assertEquals(audience.size(), 1);
+ Assert.assertTrue(audience.contains(issuer));
+ removeMetadata(storageService, clientId);
+ }
+
@Test
public void testValidGrantWithNonUriResource() throws Exception {
final Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code",
@@ -2083,6 +2132,17 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
}
}
+ protected List<String> getAudienceFromAccessToken(final AccessToken accessToken) {
+ Assert.assertNotNull(accessToken.getValue());
+ final AccessTokenClaimsSet claims;
+ try {
+ claims = AccessTokenClaimsSet.parse(accessToken.getValue(), getDataSealer());
+ return claims.getAudience();
+ } catch (ParseException | DataSealerException e) {
+ return null;
+ }
+ }
+
protected String getSidFromRefreshToken(final RefreshToken refreshToken) {
Assert.assertNotNull(refreshToken.getValue());
final RefreshTokenClaimsSet claims;
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 91580544..08cec303 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
@@ -125,6 +125,22 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLimitedInitialAudience">
+ <property name="profileConfigurations">
+ <list>
+ <ref bean="OIDC.SSO" />
+ <bean parent="OAUTH2.Token" p:limitInitialAccessTokenToSelf="true"/>
+ </list>
+ </property>
+ </bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLimitedInitialAudienceJwt">
+ <property name="profileConfigurations">
+ <list>
+ <ref bean="OIDC.SSO" />
+ <bean parent="OAUTH2.Token" p:limitInitialAccessTokenToSelf="true" p:accessTokenType="JWT"/>
+ </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