[java-idp-oidc] branch main updated: JOIDC-11 - Support for client_credentials grant

Scott Cantor cantor.2 at osu.edu
Wed Feb 9 18:44:05 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor 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=d44e3857b5a6b05115345116f898d3ca835c0b63

The following commit(s) were added to refs/heads/main by this push:
     new d44e3857 JOIDC-11 - Support for client_credentials grant
d44e3857 is described below

commit d44e3857b5a6b05115345116f898d3ca835c0b63
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Feb 9 13:44:02 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Move client back into attribute requester role.
    Populate audience into proxied requester context.
    Add rules/tests to exercise.
---
 .../profile/impl/EnforceAudienceConfiguration.java | 12 ++++++
 .../op/profile/impl/AddAttributesToClaimsSet.java  |  2 +-
 .../idp/flows/oidc/token/token-beans.xml           | 16 ++++++--
 .../flow/ClientCredentialsTokenFlowTest.java       | 43 +++++++++++++++-------
 .../src/test/resources/conf/attribute-filter.xml   |  4 ++
 .../src/test/resources/conf/attribute-resolver.xml |  1 +
 6 files changed, 59 insertions(+), 19 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java
index 70ab9f43..ee321b3b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/EnforceAudienceConfiguration.java
@@ -26,6 +26,7 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.ProxiedRequesterContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,6 +46,13 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  * the primary recipient are both relying parties of the system at different stages of the
  * flow. Either or both of them may be unverified, but enforcement of this for the audience
  * depends on a profile setting applied to the client's original profile configuration.</p>
+ * 
+ * <p>In addition, this action establishes the audience of a token request as a proxied
+ * requester for the purposes of attribute filtering. While it is arguably more correct
+ * in many cases to think of the audience as the requester, this is complicated by OIDC
+ * and four-legged scenarios in which either no audience exists or both the client and
+ * audience may be considered requesters. Treating the audience as proxied is done for
+ * consistency across all those use cases.</p> 
  *
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
@@ -162,6 +170,10 @@ public class EnforceAudienceConfiguration extends AbstractProfileAction {
                 audContext.setProfileConfig(reqContext.getProfileConfig());
             }
         }
+
+        final ProxiedRequesterContext proxyCtx = new ProxiedRequesterContext();
+        proxyCtx.getRequesters().add(audContext.getRelyingPartyId());
+        profileRequestContext.getOutboundMessageContext().addSubcontext(proxyCtx);
         
         log.debug("{} Authorized token issuance from {} client ({}) to {} audience ({})", getLogPrefix(),
                 reqContext.isVerified() ? "verified" : "unverified", reqContext.getRelyingPartyId(),
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
index 5b325fb3..d44dae6c 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
@@ -343,7 +343,7 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
             }
         }
         
-        log.debug("{} Claims set after mapping attributes to claims {}", getLogPrefix(),
+        log.debug("{} Claims set after mapping attributes to claims: {}", getLogPrefix(),
                 claimsSet.toJSONObject().toJSONString());
     }
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index d434469a..54290282 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -290,15 +290,23 @@
         c:resolverService-ref="shibboleth.AttributeResolverService"
         p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
         p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}"
-        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
-        p:recipientLookupStrategy-ref="AudienceIDLookupFunction" />
+        p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction" />
 
     <bean id="FilterAttributesForAudience" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
         c:filterService-ref="shibboleth.AttributeFilterService"
         p:maskFailures="%{idp.service.attribute.filter.maskFailures:true}"
         p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
-        p:recipientLookupStrategy-ref="AudienceIDLookupFunction"
-        p:metadataContextLookupStrategy-ref="LookupSAMLMetadataContext" />
+        p:proxiedRequesterContextLookupStrategy-ref="AudienceProxiedRequesterLookupFunction" />
+
+    <bean id="AudienceProxiedRequesterLookupFunction" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+                c:type="#{ T(org.opensaml.profile.context.ProxiedRequesterContext) }" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Outbound" />
+        </constructor-arg>
+    </bean>
 
     <bean id="AudienceIssuerLookupFunction"
         class="net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction"
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 b109adfc..79e95294 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
@@ -19,6 +19,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 import java.io.IOException;
@@ -76,12 +77,13 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
     @Qualifier("shibboleth.StorageService")
     StorageService storageService;
     
+    /** Constructor. */
     public ClientCredentialsTokenFlowTest() {
         super(FLOW_ID);
     }
     
     @AfterMethod
-    public void removeMetadata() throws IOException {
+    public void tearDown() throws IOException {
         removeMetadata(storageService, clientId);
         removeMetadata(storageService, clientId + "JWT");
     }
@@ -123,7 +125,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
         Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
         verifyClaims(null, response.getTokens().getBearerAccessToken(), new Scope(),
-                Collections.singletonList(resource));
+                Collections.singletonList(resource), "eduPersonScopedAffiliation");
     }
 
     @Test
@@ -137,7 +139,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
         Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
         verifyClaims("JWT", response.getTokens().getBearerAccessToken(), new Scope(),
-                Collections.singletonList(resource));
+                Collections.singletonList(resource), "eduPersonScopedAffiliation");
     }
 
     @Test
@@ -151,7 +153,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getScope(), scope);
         verifyClaims(null, response.getTokens().getBearerAccessToken(), scope,
-                Collections.singletonList(resource));
+                Collections.singletonList(resource), "email", "eduPersonScopedAffiliation");
     }
 
     @Test
@@ -165,7 +167,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getScope(), scope);
         verifyClaims("JWT", response.getTokens().getBearerAccessToken(), scope,
-                Collections.singletonList(resource));
+                Collections.singletonList(resource), "email", "eduPersonScopedAffiliation");
     }
 
     
@@ -185,7 +187,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
         Assert.assertEquals(response.getTokens().getBearerAccessToken().getScope(), scope);
         verifyClaims(null, response.getTokens().getBearerAccessToken(), scope,
-                Collections.singletonList(resource));
+                Collections.singletonList(resource), "email", "eduPersonScopedAffiliation");
     }
     
     private AccessTokenClaimsSet unwrapAccessToken(final AccessTokenResponse tokenResponse) {
@@ -200,8 +202,8 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         }
     }
     
-    protected FlowExecutionResult launchWithJwtAuthentication(final JWTAuthentication authnMethod, final JWSAlgorithm algorithm)
-            throws Exception {
+    protected FlowExecutionResult launchWithJwtAuthentication(final JWTAuthentication authnMethod,
+            final JWSAlgorithm algorithm) throws Exception {
         storeMetadata(storageService, clientId, clientSecret, scope, JWSAlgorithm.HS256,
                 ClientAuthenticationMethod.CLIENT_SECRET_JWT);
         final Map<String, String> requestParameters = createRequestParameters(clientId, scope, resource);
@@ -223,14 +225,15 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
         return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
 
-    protected Map<String, String> createRequestParameters(final String clientId, final Scope scope, final String resource) {
+    protected Map<String, String> createRequestParameters(final String clientId, final Scope s,
+            final String r) {
         final Map<String, String> parameters = new HashMap<>();
         addNonNullValue(parameters, "grant_type", GrantType.CLIENT_CREDENTIALS.getValue());
-        if (scope != null) {
-            addNonNullValue(parameters, "scope", scope.toString());
+        if (s != null) {
+            addNonNullValue(parameters, "scope", s.toString());
         }
-        if (resource != null) {
-            addNonNullValue(parameters, "resource", resource);
+        if (r != null) {
+            addNonNullValue(parameters, "resource", r);
         }
         return parameters;
     }
@@ -252,6 +255,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
     * @param token access token
     * @param s scope to check for
     * @param audiences audiences to check for
+    * @param customClaims custom claim names to check for
     * 
     * @throws ComponentInitializationException 
     * @throws DataSealerException 
@@ -259,7 +263,7 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
     * @throws NoSuchAlgorithmException 
     */
    private void verifyClaims(@Nullable final String type, @Nonnull final AccessToken token, @Nonnull final Scope s,
-           @Nonnull @NonnullElements final Collection<String> audiences)
+           @Nonnull @NonnullElements final Collection<String> audiences, @Nullable final String...customClaims)
            throws NoSuchAlgorithmException, ParseException, DataSealerException, ComponentInitializationException {
        
        if (type == null) {
@@ -275,6 +279,12 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
            assertTrue(at.getIssuedAt().isBefore(Instant.now()));
            assertEquals(at.getScope(), s);
            assertEquals(at.getSubject(), clientId);
+           if (customClaims != null) {
+               for (final String c : customClaims) {
+                   // These should be absent in opaque tokens.
+                   assertNull(at.getClaimsSet().getClaim(c));
+               }
+           }
        } else if ("JWT".equals(type)) {
            final JWTClaimsSet claims = SignedJWT.parse(token.getValue()).getJWTClaimsSet();
            assertNotNull(claims);
@@ -287,6 +297,11 @@ public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthentica
            assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
            assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), s.toString());
            assertEquals(claims.getSubject(), clientId + type);
+           if (customClaims != null) {
+               for (final String c : customClaims) {
+                   assertNotNull(claims.getClaim(c));
+               }
+           }
        } else {
            throw new RuntimeException("Bad token format");
        }
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/attribute-filter.xml b/idp-oidc-extension-impl/src/test/resources/conf/attribute-filter.xml
index e6c69f53..840230b3 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/attribute-filter.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/attribute-filter.xml
@@ -90,6 +90,10 @@
     <AttributeFilterPolicy id="example2">
         <PolicyRequirementRule xsi:type="OR">
             <Rule xsi:type="Requester" value="https://sp.example.org" />
+            <Rule xsi:type="AND">
+                <Rule xsi:type="Requester" value="mockClientIdJWT" />
+                <Rule xsi:type="ProxiedRequester" value="https://rp.example.org" />
+            </Rule>
             <Rule xsi:type="Requester" value="https://another.example.org/shibboleth" />
         </PolicyRequirementRule>
 
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
index a9301b28..4853ed0c 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
@@ -64,6 +64,7 @@
     -->
     <AttributeDefinition id="eduPersonScopedAffiliation" xsi:type="Scoped" scope="%{idp.scope}">
         <InputDataConnector ref="staticAttributes" attributeNames="affiliation" />
+        <AttributeEncoder xsi:type="oidc:OIDCScopedString" />
     </AttributeDefinition>
 
     <!-- Subject Identifier is a attribute that must always be resolved.

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list