[java-idp-oidc] 17/35: JOIDC-230 - Add flag to block additional audiences from initial access token

Henri Mikkonen henri.mikkonen at iki.fi
Thu Oct 24 14:01:42 UTC 2024


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=c4c79eceb73a80cba3ffa496d963710c0fa8b3f7

commit c4c79eceb73a80cba3ffa496d963710c0fa8b3f7
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 0d34abd0..e9db5719 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
@@ -126,6 +126,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