[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