[java-idp-oidc] branch main updated: JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Henri Mikkonen
henri.mikkonen at iki.fi
Tue May 14 15:10:06 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=cf54a13c171738600fbfce0ab9262f9a54351437
The following commit(s) were added to refs/heads/main by this push:
new cf54a13c JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
cf54a13c is described below
commit cf54a13c171738600fbfce0ab9262f9a54351437
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue May 14 18:09:50 2024 +0300
JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
https://shibboleth.atlassian.net/browse/JOIDC-201
- Updated activation conditions for DPoP Proof validation actions
- Always active if requireDopProof flag in profile config is enabled
- Also active if DPoP Proof is included in the request
- Updated ValidateGrant to make sure that DPoP Proof JWT thumbprint matches with the one in token claims set (if specified)
- OidcEventIds.INVALID_DPOP_PROOF event is built if they don't match
- Updated DPoP events to return DPoP-specific error message in API endpoints
---
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 32 ++-
.../META-INF/net.shibboleth.idp/postconfig.xml | 7 +-
.../dpop-proof-validation-beans.xml | 35 ++-
.../op/profile/flow/PushedAuthorizeFlowTest.java | 4 +-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 306 +++++++++++++++++++++
.../shibboleth/idp/module/conf/relying-party.xml | 15 +
6 files changed, 380 insertions(+), 19 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 2a565ccf..f1916ad5 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -39,6 +39,7 @@ import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
import com.nimbusds.oauth2.sdk.id.ClientID;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy;
import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
@@ -104,6 +105,9 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
/** List of deserializer bi-functions for refresh tokens to be used in addition to unsealing opaque value. */
@Nonnull private List<BiFunction<ProfileRequestContext,String,RefreshTokenClaimsSet>> refreshTokenDeserializers;
+ /** Strategy used to locate thumbprint of validated DPoP Proof JWT. */
+ @Nonnull private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy;
+
/** The RelyingPartyContext to operate on. */
@Nullable private RelyingPartyContext rpCtx;
@@ -123,6 +127,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
((RevocationLifetimeLookupFunction) chainRevocationLifetimeLookupStrategy).setUseActiveProfileOnly(false);
refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
refreshTokenDeserializers = CollectionSupport.emptyList();
+ dpopProofThumbprintLookupStrategy = new DefaultDPoPProofThumbprintLookupFunction();
}
/**
@@ -206,6 +211,18 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
Constraint.isNotNull(deserializers, "List of refresh token deserializers cannot be null");
}
+ /**
+ * Set the strategy used to locate the thumbprint of validated DPoP Proof JWT.
+ *
+ * @param strategy lookup strategy
+ *
+ * @since 4.2.0
+ */
+ public void setDpopProofThumbprintLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ dpopProofThumbprintLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -379,7 +396,20 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
assert oidcResponseContext != null;
oidcResponseContext.setAuthorizationGrantClaimsSet(tokenClaimsSet);
- oidcResponseContext.setDpopProofJwkThumbprint(tokenClaimsSet.getDpopProofJwkThumbprint());
+ final String claimsSetThumbprint = tokenClaimsSet.getDpopProofJwkThumbprint();
+ final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+ if (claimsSetThumbprint == null) {
+ oidcResponseContext.setDpopProofJwkThumbprint(proofThumbprint);
+ } else if (proofThumbprint == null) {
+ oidcResponseContext.setDpopProofJwkThumbprint(claimsSetThumbprint);
+ } else if (!claimsSetThumbprint.equals(proofThumbprint)) {
+ log.warn("{} Invalid DPoP Proof thumbprint issued to client {}, invalid for {}", getLogPrefix(),
+ clientId.getValue(), relyingPartyId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ } else {
+ oidcResponseContext.setDpopProofJwkThumbprint(proofThumbprint);
+ }
}
// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount ON
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 81e67379..92bffb96 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -681,9 +681,14 @@
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).REVOCATION_FAILED}"
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
- <entry key="#{'InvalidScope'}"
+ <entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_SCOPE}"
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_SCOPE}" />
+ <entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_DPOP_PROOF}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF_CODE}" />
+ <entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).MISSING_DPOP_PROOF}"
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF_CODE}" />
+
<!-- Missing from Nimbus. -->
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_TARGET}"
value="#{T(net.shibboleth.oidc.profile.core.OidcError).INVALID_TARGET}" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml
index ec8b49dc..9c270b19 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml
@@ -10,11 +10,8 @@
<bean id="InitializeDPoPProofContext" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.InitializeDPoPProofContext"
scope="prototype"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
- p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}">
- <property name="requireDpopProofCondition">
- <bean class="net.shibboleth.oidc.profile.config.logic.RequireDPoPProofPredicate"/>
- </property>
- </bean>
+ p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}"
+ p:requireDpopProofCondition-ref="DPoPProofRequired" />
<bean id="DPoPSecurityParametersContextProfileRequestContextLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
@@ -37,25 +34,37 @@
</constructor-arg>
</bean>
+ <bean id="DPoPProofRequired" class="net.shibboleth.oidc.profile.config.logic.RequireDPoPProofPredicate"/>
+
+ <bean id="DPoPProofExists" parent="shibboleth.Conditions.Expression"
+ c:expression="#input.getInboundMessageContext() != null and #input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)) != null and #input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
+
+ <bean id="DPoPProofRequiredOrExists" parent="shibboleth.Conditions.OR">
+ <constructor-arg>
+ <list>
+ <ref bean="DPoPProofRequired" />
+ <ref bean="DPoPProofExists" />
+ </list>
+ </constructor-arg>
+ </bean>
+
<bean id="PopulateDPoPProofSignatureValidationParameters"
class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParameters"
scope="prototype"
c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
- p:securityParametersContextLookupStrategy-ref="DPoPSecurityParametersContextProfileRequestContextLookup">
+ p:securityParametersContextLookupStrategy-ref="DPoPSecurityParametersContextProfileRequestContextLookup"
+ p:activationCondition-ref="DPoPProofRequiredOrExists">
<property name="configurationLookupStrategy">
<bean class="net.shibboleth.oidc.profile.config.navigate.DPoPProofSignatureValidationConfigurationLookupFunction" />
</property>
<property name="signatureValidationParametersResolver">
<bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
</property>
- <property name="activationCondition">
- <bean parent="shibboleth.Conditions.Expression"
- c:expression="#input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
- </property>
</bean>
<bean id="ValidateDPoPProofSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
- scope="prototype" c:executionDirection="INBOUND">
+ scope="prototype" c:executionDirection="INBOUND"
+ p:activationCondition-ref="DPoPProofRequiredOrExists">
<constructor-arg>
<bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
<property name="handlers">
@@ -75,10 +84,6 @@
</property>
</bean>
</constructor-arg>
- <property name="activationCondition">
- <bean parent="shibboleth.Conditions.Expression"
- c:expression="#input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
- </property>
</bean>
<bean id="ValidateDPoPProof" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateDPoPProof"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
index 7bfc7ccf..b173f6b9 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
@@ -200,7 +200,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
request.addHeader("DPoP", "invalid");
setHttpFormRequest("POST", createRequestParameters(clientId));
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
removeMetadata(storageService, "mockClientIdRequestObjectEnforced");
}
@@ -214,7 +214,7 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
requestParameters.put("dpop_jkt", "notMatching");
setHttpFormRequest("POST", requestParameters);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
}
@SuppressWarnings("null")
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 15176a3f..6eff3d33 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
@@ -59,6 +59,7 @@ import com.nimbusds.oauth2.sdk.pkce.CodeChallenge;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.AccessTokenType;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -97,6 +98,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
String clientIdJwtAccessToken = "mockClientIdJwtAccessToken";
String clientIdNoIdTokenViaRefreshToken = "mockClientIdNoIdTokenViaRefreshToken";
String clientIdJwtRefreshToken = "mockClientIdNotMDDrivenRefreshTokenJwt";
+ String clientIdDPoPAccessToken = "mockClientIdDPoPAccessToken";
+ String clientIdDPoPJwtAccessToken = "mockClientIdDPoPJwtAccessToken";
String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
String resourceUri = "https://rp.example.org";
@@ -129,6 +132,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
removeMetadata(storageService, resourceUri);
removeMetadata(storageService, resourceNonUri);
removeMetadata(storageService, clientIdNoIdTokenViaRefreshToken);
+ removeMetadata(storageService, clientIdDPoPAccessToken);
+ removeMetadata(storageService, clientIdDPoPJwtAccessToken);
}
@Test
@@ -683,6 +688,307 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(idToken.getJWTClaimsSet().getClaim("at_hash"));
}
+ @Test
+ public void testDPoPValidGrantThumbprintIncludedNotMatchingProof() throws Exception {
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint("mockJkt");
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPValidGrantThumbprintIncludedMatchingProof() throws Exception {
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token");
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint(dpopProof.getHeader().getJWK().computeThumbprint().toString());
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
+ @Test
+ public void testDPoPValidGrantThumbprintNotIncluded() throws Exception {
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
+ @Test
+ public void testDPoPEnforcedValidGrantNoProof() throws Exception {
+ final String clientId = clientIdDPoPAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPEnforcedValidGrantThumbprintIncludedNotMatchingProof() throws Exception {
+ final String clientId = clientIdDPoPAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint("mockJkt");
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPEnforcedValidGrantThumbprintIncludedMatchingProof() throws Exception {
+ final String clientId = clientIdDPoPAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token");
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint(dpopProof.getHeader().getJWK().computeThumbprint().toString());
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
+ @Test
+ public void testDPoPEnforcedValidGrantThumbprintNotIncluded() throws Exception {
+ final String clientId = clientIdDPoPAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
+ @Test
+ public void testDPoPJwtEnforcedValidGrantNoProof() throws Exception {
+ final String clientId = clientIdDPoPJwtAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPJwtEnforcedValidGrantThumbprintIncludedNotMatchingProof() throws Exception {
+ final String clientId = clientIdDPoPJwtAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint("mockJkt");
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPJwtEnforcedValidGrantThumbprintIncludedMatchingProof() throws Exception {
+ final String clientId = clientIdDPoPJwtAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token");
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint(dpopProof.getHeader().getJWK().computeThumbprint().toString());
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
+ @Test
+ public void testDPoPJwtEnforcedValidGrantThumbprintNotIncluded() throws Exception {
+ final String clientId = clientIdDPoPJwtAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token").serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+ Assert.assertNotNull(response.getTokens().getRefreshToken());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
protected String buildAuthorizationCode(final String clientId) throws Exception {
return buildAuthorizationCode(clientId, null);
}
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 5bf9baca..eb932bfa 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
@@ -229,6 +229,21 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdDPoPAccessToken">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO.MDDriven" p:requireDpopProof="true"/>
+ </list>
+ </property>
+ </bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdDPoPJwtAccessToken">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO.MDDriven" p:requireDpopProof="true" p:accessTokenType="JWT"/>
+ <bean parent="OAUTH2.Token.MDDriven" p:requireDpopProof="true" p:accessTokenType="JWT" p:encryptionOptional="true" />
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdNoRefreshTokensInSSOProfile">
<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