[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
Wed May 29 12:46:37 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=67f565e1d16dd07a2ca2cc3ee8afdcfa97c7af7a

The following commit(s) were added to refs/heads/main by this push:
     new 67f565e1 JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
67f565e1 is described below

commit 67f565e1d16dd07a2ca2cc3ee8afdcfa97c7af7a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed May 29 15:46:12 2024 +0300

    JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
    
    https://shibboleth.atlassian.net/browse/JOIDC-201
    
    - Switched 'iat' claim into mandatory in DPoP proofs
    - Wired DPoP proof claim validators for MDDriven recovation and introspection
    - Fine-tuned error event ID for DPoP proof signature validation
    - Improved testing
---
 .../dpop-proof-validation-beans.xml                |   3 +-
 .../idp/service/relying-party/postconfig.xml       |  10 +
 .../AbstractOidcClientAuthenticationFlowTest.java  | 386 ++++++++++++++++++++-
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |  42 +++
 4 files changed, 438 insertions(+), 3 deletions(-)

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 387cddf9..0685e549 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
@@ -75,7 +75,8 @@
 
     <bean id="ValidateDPoPProofSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
         scope="prototype" c:executionDirection="INBOUND"
-        p:activationCondition-ref="DPoPProofRequiredOrExists">
+        p:activationCondition-ref="DPoPProofRequiredOrExists"
+        p:errorEvent="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_DPOP_PROOF}">
         <constructor-arg>
             <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
                 <property name="handlers">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 07b74c47..9b5d9e44 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -160,6 +160,11 @@
         <property name="issuerLookupStrategy">
             <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer" p:defaultValue-ref="shibboleth.oidc.issuer" />
         </property>
+        <property name="dpopProofClaimsValidatorLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="dpopProofClaimsValidator"
+                p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+                p:defaultValue-ref="DefaultDPoPWithAthProofClaimsValidator" />
+        </property>
     </bean>
         
     <bean id="AbstractMDDrivenOIDCFlowAwareProfile" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" abstract="true">
@@ -1037,6 +1042,11 @@
     <util:list id="DPoPProofClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
         <ref bean="ExpiryClaimsValidator" />
         <ref bean="NotBeforeClaimsValidator" />
+        <bean id="DPoPProofIssuedAtClaimsValidator"
+            class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+            p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+            p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+            p:requiredRule="true" />
         <ref bean="IssuedAtClaimsValidator" />
         <bean id="JWTIdentifierClaimsValidator"
             class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
index c0b90cef..96bf48ae 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
@@ -29,7 +29,9 @@ import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.jwk.Curve;
 import com.nimbusds.jose.jwk.ECKey;
 import com.nimbusds.jwt.JWT;
@@ -653,12 +655,392 @@ public abstract class AbstractOidcClientAuthenticationFlowTest extends AbstractO
         }
     }
 
+    @SuppressWarnings("null")
     @Test
-    public void testInvalidDPoPProof() throws Exception {
+    public void testInvalidDPoPProof_missingMendatoryNonce() throws Exception {
         for (final String id : clientIds) {
+            request.removeHeader("DPoP");
             final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
                     clientSecret, JWSAlgorithm.HS256);
-            request.addHeader("DPoP", buildDPoPProof(super.defaultDPoPProofKey(), JWSAlgorithm.ES256, "POST", jwtAud,null, null));
+            request.addHeader("DPoP", buildDPoPProof(defaultDPoPProofKey(), JWSAlgorithm.ES256, "POST", jwtAud,
+                    null, null).serialize());
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.USE_DPOP_NONCE_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_unsignedProof() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final PlainJWT dpopProof = buildUnsignedDPoPProof(jwk, "POST", jwtAud,
+                    null, createValidDPoPNonce());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_wrongHeaderType() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(new Date())
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_missingIat() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_expiredIat() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now().minusSeconds(600)))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_futureIat() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now().plusSeconds(600)))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_twoHeaders() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_missingHtu() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_invalidHtu() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud + "2")
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_missingHtm() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_invalidHtm() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "GET")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_missingJti() throws Exception {
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_missingAth() throws Exception {
+        if ("oidc/token".equals(flowId) || "oauth2/pushed-authorization".equals(flowId)) {
+            return;
+        }
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+
+    @Test
+    public void testInvalidDPoPProof_invalidAth() throws Exception {
+        if ("oidc/token".equals(flowId) || "oauth2/pushed-authorization".equals(flowId)) {
+            return;
+        }
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            request.removeHeader("Authorization");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = defaultDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .claim("ath", "invalidContents")
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("Authorization", "DPoP mockAccessToken");
+            request.addHeader("DPoP", headerValue);
+            final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+            assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+        }
+    }
+    
+    @Test
+    public void testInvalidDPoPProof_excludedAlg() throws Exception {
+        if ("oidc/token".equals(flowId) || "oauth2/pushed-authorization".equals(flowId)) {
+            return;
+        }
+        for (final String id : clientIds) {
+            request.removeHeader("DPoP");
+            final SignedJWT jwt = createSecretJWT(validClaimsSet(id, "http://localhost/idp/profile/oidc/token"),
+                    clientSecret, JWSAlgorithm.HS256);
+            final ECKey jwk = excludedDPoPProofKey();
+            final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES384)
+                    .jwk(jwk.toPublicJWK())
+                    .type(new JOSEObjectType("dpop+jwt"))
+                    .build();
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .jwtID(idGenerator.generateIdentifier())
+                    .claim("htu", jwtAud)
+                    .claim("htm", "POST")
+                    .claim("nonce", createValidDPoPNonce())
+                    .issueTime(Date.from(Instant.now()))
+                    .build();
+            final SignedJWT dpopProof = buildDPoPProof(header, claimsSet, jwk.toECPrivateKey());
+            final String headerValue = dpopProof.serialize(); assert headerValue != null;
+            request.addHeader("DPoP", headerValue);
             final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
                     ClientAuthenticationMethod.CLIENT_SECRET_JWT);
             assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index ff431a5f..6d35d051 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -53,12 +53,14 @@ import org.testng.annotations.BeforeMethod;
 
 import com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWEAlgorithm;
 import com.nimbusds.jose.JWEHeader;
 import com.nimbusds.jose.JWEObject;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.PlainHeader;
 import com.nimbusds.jose.crypto.AESEncrypter;
 import com.nimbusds.jose.crypto.ECDHEncrypter;
 import com.nimbusds.jose.crypto.ECDSASigner;
@@ -73,6 +75,7 @@ import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
 import com.nimbusds.jwt.EncryptedJWT;
 import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.ErrorResponse;
 import com.nimbusds.oauth2.sdk.GrantType;
@@ -636,7 +639,34 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
             Assert.fail("Could not create DPoP proof", e);
         }
         return null;
+    }
+
+    protected static SignedJWT buildDPoPProof(final JWSHeader header, final JWTClaimsSet claimsSet,
+            final ECPrivateKey privateKey) {
+        final SignedJWT signedJWT = new SignedJWT(header, claimsSet);
+        try {
+            final ECDSASigner signer = new ECDSASigner(privateKey);
+            signedJWT.sign(signer);
+        } catch (JOSEException e) {
+            Assert.fail("Could not sign DPoP proof", e);
+        }
+        return signedJWT;
+    }
 
+    protected static PlainJWT buildUnsignedDPoPProof(final JWK jwk, final String method,
+            final String uri, final AccessToken accessToken, final String nonceValue) {
+        final PlainHeader header = new PlainHeader.Builder()
+                .type(new JOSEObjectType("dpop+jwt"))
+                .customParam("jwk", jwk.toJSONString())
+                .build();
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .claim("htu", uri.toString())
+                .claim("htm", method)
+                .claim("nonce", nonceValue)
+                .issueTime(new Date())
+                .build();
+        final PlainJWT jwt = new PlainJWT(header, claimsSet);
+        return jwt;
     }
 
     protected static ECKey defaultDPoPProofKey() {
@@ -649,4 +679,16 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         }
         return null;
     }
+
+    protected static ECKey excludedDPoPProofKey() {
+        try {
+            return new ECKeyGenerator(Curve.P_384)
+                    .keyID("1")
+                    .generate();
+        } catch (JOSEException e) {
+            Assert.fail("Could not initialize DPoP proof key", e);
+        }
+        return null;
+    }
+
 }

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


More information about the commits mailing list