[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