[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
Fri May 24 08:52:18 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=1e8b28e1bb598eba6c87750c16b2cd9a22e1105f

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

commit 1e8b28e1bb598eba6c87750c16b2cd9a22e1105f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri May 24 11:51:47 2024 +0300

    JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
    
    https://shibboleth.atlassian.net/browse/JOIDC-201
    
    - Relocated the jkt-claim in claims set, it needs to be under 'cnf' claim in JWT / introspection
    - Initial support in the introspection endpoint (token type and cnf/jkt)
---
 .../oidc/op/token/support/TokenClaimsSet.java      |  34 ++--
 .../op/oauth2/profile/impl/BuildAccessToken.java   |   2 +-
 .../FormOutboundIntrospectionResponseMessage.java  |  12 +-
 .../op/profile/flow/IntrospectionFlowTest.java     | 176 +++++++++++++++++++++
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java |  30 +++-
 5 files changed, 238 insertions(+), 16 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index 0912f0f4..c832ece0 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -137,7 +137,10 @@ public class TokenClaimsSet {
     /** Identifier for the session id. */
     @Nonnull @NotEmpty public static final String KEY_SESSION_ID = "sid";
 
-    /** Identifier for the DPoP Proof JWK thumbprint. */
+    /** Identifier for the confirmation claim. */
+    @Nonnull @NotEmpty public static final String KEY_CONFIRMATION = "cnf";
+
+    /** Identifier for the DPoP Proof JWK thumbprint under confirmation claim. */
     @Nonnull @NotEmpty public static final String KEY_DPOP_PROOF_JWK_THUMBPRINT = "jkt";
 
     /** Claims set for the claim. */
@@ -233,10 +236,14 @@ public class TokenClaimsSet {
         if (tokenClaimsSet.getClaims().containsKey(KEY_CODE_CHALLENGE)) {
             tokenClaimsSet.getStringClaim(KEY_CODE_CHALLENGE);
         }
-        if (tokenClaimsSet.getClaims().containsKey(KEY_DPOP_PROOF_JWK_THUMBPRINT)) {
-            tokenClaimsSet.getStringClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT);
+        if (tokenClaimsSet.getClaims().get(KEY_CONFIRMATION) != null) {
+            final Map<String, Object> cnf = tokenClaimsSet.getJSONObjectClaim(KEY_CONFIRMATION);
+            if (cnf.containsKey(KEY_DPOP_PROOF_JWK_THUMBPRINT)) {
+                if (!(cnf.get(KEY_DPOP_PROOF_JWK_THUMBPRINT) instanceof String)) {
+                    throw new ParseException("dpop proof jwk thumbprint claim is of wrong type", 0);
+                }
+            }
         }
-
     }
 // Checkstyle: CyclomaticComplexity ON
 
@@ -660,10 +667,18 @@ public class TokenClaimsSet {
      */
     @Nullable public String getDpopProofJwkThumbprint() {
         final JWTClaimsSet tokenClaimsSet = assertedClaimsSet();
-        if (tokenClaimsSet.getClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT) == null) {
-            return null;
+        try {
+            final Map<String, Object> cnf = tokenClaimsSet.getJSONObjectClaim(KEY_CONFIRMATION);
+            if (cnf != null) {
+                if (cnf.containsKey(KEY_DPOP_PROOF_JWK_THUMBPRINT)) {
+                    return (String) cnf.get(KEY_DPOP_PROOF_JWK_THUMBPRINT);
+                }
+            }
+        } catch (final ParseException e) {
+            log.error("Error parsing confirmation claims {}",
+                    tokenClaimsSet.getClaim(KEY_CONFIRMATION));
         }
-        return (String) tokenClaimsSet.getClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT);
+        return null;
     }
 
     /**
@@ -810,8 +825,9 @@ public class TokenClaimsSet {
                     .claim(KEY_CONSENT_ENABLED, consentEnabled)
                     .claim(KEY_ROOT_JTI, rootTokenId)
                     .claim(KEY_SESSION_ID, sessionId)
-                    .claim(KEY_DPOP_PROOF_JWK_THUMBPRINT, dpopProofJwkThumbprint);
-            
+                    .claim(KEY_CONFIRMATION, dpopProofJwkThumbprint == null ? null : 
+                        CollectionSupport.singletonMap(KEY_DPOP_PROOF_JWK_THUMBPRINT, dpopProofJwkThumbprint));
+
             customClaims.forEach((n,v) -> {
                 if (n != null) {
                     builder.claim(n, v);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index b56388dd..3f48cba2 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -515,6 +515,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             assert idGenerator != null;
             builder.setJWTID(idGenerator, xmlSafeIdentifier);
             builder.setSessionIdentifier(responseCtx.getSessionId());
+            builder.setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint());
             // Set root token identifier to contain jit from the claims set used for building the new token
             if (StringSupport.trimOrNull(nonNullClaimsSet.getRootTokenIdentifier()) == null) {
                 builder.setRootTokenIdentifier(nonNullClaimsSet.getID());
@@ -523,7 +524,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             final OIDCAuthenticationResponseConsentContext consentCtx =
                     consentContextLookupStrategy.apply(profileRequestContext);
             final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
-
             builder = (Builder) new AccessTokenClaimsSet.Builder()
                     .setClientID(clientID)
                     .setIssuer(issuer)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
index 9aff26de..7cecee3b 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
@@ -25,6 +25,7 @@ import org.slf4j.Logger;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
+import com.nimbusds.oauth2.sdk.dpop.JWKThumbprintConfirmation;
 import com.nimbusds.oauth2.sdk.id.Audience;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.id.Issuer;
@@ -68,10 +69,10 @@ public class FormOutboundIntrospectionResponseMessage extends AbstractProfileAct
                 clientID = tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_LEGACY_CLIENTID);
             }
 
+            final JWKThumbprintConfirmation jktCnf = JWKThumbprintConfirmation.parse(tokenClaimsSet);
             final TokenIntrospectionSuccessResponse.Builder builder =
                     new TokenIntrospectionSuccessResponse.Builder(true)
                         .clientID(new ClientID(clientID))
-                        .tokenType(AccessTokenType.BEARER)
                         .expirationTime(tokenClaimsSet.getExpirationTime())
                         .issueTime(tokenClaimsSet.getIssueTime())
                         .subject(new Subject(tokenClaimsSet.getSubject()))
@@ -94,7 +95,14 @@ public class FormOutboundIntrospectionResponseMessage extends AbstractProfileAct
                         .collect(Collectors.toUnmodifiableList()));
 
             }
-            
+
+            if (jktCnf != null && "at".equals(tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_TYPE))) {
+                builder.jwkThumbprintConfirmation(jktCnf);
+                builder.tokenType(AccessTokenType.DPOP);
+            } else {
+                builder.tokenType(AccessTokenType.BEARER);
+            }
+
             profileRequestContext.ensureOutboundMessageContext().setMessage(builder.build());
             
         } catch (final ParseException e) {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index 2f1c7ebb..f6f757e7 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -19,6 +19,7 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PublicKey;
 import java.time.Instant;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -41,8 +42,11 @@ import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.dpop.JWKThumbprintConfirmation;
 import com.nimbusds.oauth2.sdk.id.Audience;
 import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.AccessTokenType;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
@@ -157,11 +161,33 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
         Assert.assertNull(resp.getAudience());
     }
 
+    @Test
+    public void testSuccessDPoPUnverified_compliantPolicy() throws IOException, NoSuchAlgorithmException,
+            URISyntaxException, DataSealerException, ComponentInitializationException {
+        final String clientId = "policyAcceptedClient1";
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", Map.of(
+                "token",
+                new BearerAccessToken(buildDefaultDPoPClaimsSet(clientId, "sub", null, "mockJkt")
+                        .serialize(getDataSealer())).toJSONObject().getAsString("access_token"),
+                "token_type",
+                "access_token"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final TokenIntrospectionSuccessResponse resp =
+                parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+        Assert.assertTrue(resp.isActive());
+        assertDPoPToken(resp, "mockJkt");
+        Assert.assertEquals(resp.getClientID().getValue(), clientId);
+        Assert.assertEquals(resp.getScope(), scope);
+        Assert.assertNull(resp.getAudience());
+    }
+
     @Test
     public void testSuccess() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
             ComponentInitializationException {
@@ -177,12 +203,36 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
             final TokenIntrospectionSuccessResponse resp =
                     parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
             Assert.assertTrue(resp.isActive());
+            assertBearerToken(resp);
             Assert.assertEquals(resp.getClientID().getValue(), clientId);
             Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
             Assert.assertNull(resp.getAudience());
         }
     }
 
+    @Test
+    public void testSuccessDPoP() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+            ComponentInitializationException {
+        for (final String clientId : clientIds) {
+            storeMetadata(storageService, clientId, clientSecret, scope);
+            setBasicAuth(clientId, clientSecret);
+            setHttpFormRequest("POST", Map.of(
+                    "token",
+                    new BearerAccessToken(buildDefaultDPoPClaimsSet(clientId, "sub", null, "mockJkt")
+                            .serialize(getDataSealer())).toJSONObject().getAsString("access_token"),
+                    "token_type",
+                    "access_token"));
+            final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            final TokenIntrospectionSuccessResponse resp =
+                    parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+            Assert.assertTrue(resp.isActive());
+            assertDPoPToken(resp, "mockJkt");
+            Assert.assertEquals(resp.getClientID().getValue(), clientId);
+            Assert.assertEquals(resp.getScope(), scope);
+            Assert.assertNull(resp.getAudience());
+        }
+    }
+
     @Test
     public void testSuccessWithPostAuth() throws IOException, NoSuchAlgorithmException, URISyntaxException,
             DataSealerException, ComponentInitializationException {
@@ -202,12 +252,40 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
             final TokenIntrospectionSuccessResponse resp =
                     parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
             Assert.assertTrue(resp.isActive());
+            assertBearerToken(resp);
             Assert.assertEquals(resp.getClientID().getValue(), clientId);
             Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
             Assert.assertNull(resp.getAudience());
         }
     }
 
+    @Test
+    public void testSuccessDPoPWithPostAuth() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        for (final String clientId : clientIds) {
+            storeMetadata(storageService, clientId, clientSecret, scope, null,
+                    ClientAuthenticationMethod.CLIENT_SECRET_POST);
+            setHttpFormRequest("POST", Map.of(
+                    "token",
+                    new BearerAccessToken(buildDefaultDPoPClaimsSet(clientId, "sub", null, "mockJkt")
+                            .serialize(getDataSealer())).toJSONObject().getAsString("access_token"),
+                    "token_type",
+                    "access_token",
+                    "client_id",
+                    clientId,
+                    "client_secret",
+                    clientSecret));
+            final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+            final TokenIntrospectionSuccessResponse resp =
+                    parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+            Assert.assertTrue(resp.isActive());
+            assertDPoPToken(resp, "mockJkt");
+            Assert.assertEquals(resp.getClientID().getValue(), clientId);
+            Assert.assertEquals(resp.getScope(), scope);
+            Assert.assertNull(resp.getAudience());
+        }
+    }
+
     @Test
     public void testSuccessWithRefreshToken() throws IOException, NoSuchAlgorithmException, URISyntaxException,
             DataSealerException, ComponentInitializationException {
@@ -225,6 +303,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
         Assert.assertNull(resp.getAudience());
@@ -247,6 +326,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
         Assert.assertNull(resp.getAudience());
@@ -357,6 +437,22 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertEquals(resp.getClientID().getValue(), clientIdSaml);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
+    }
+
+    @Test
+    public void testSuccessDPoPWithSamlMetadata() throws NoSuchAlgorithmException, URISyntaxException,
+            DataSealerException, ComponentInitializationException {
+        setBasicAuth(clientIdSaml, clientSecretSaml);
+        setHttpFormRequest("POST", Collections.singletonMap("token",
+                new BearerAccessToken(buildDefaultDPoPClaimsSet(clientIdSaml, "sub", null, "mockJkt")
+                        .serialize(getDataSealer())).toJSONObject().getAsString("access_token")));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final TokenIntrospectionSuccessResponse resp =
+                parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+        Assert.assertEquals(resp.getClientID().getValue(), clientIdSaml);
+        Assert.assertTrue(resp.isActive());
+        assertDPoPToken(resp, "mockJkt");
     }
 
     @Test
@@ -372,6 +468,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
     }
 
     @Test
@@ -387,6 +484,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
     }
 
     @Test
@@ -406,6 +504,26 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
         Assert.assertEquals(resp.getScope(), scope);
         Assert.assertNull(resp.getAudience());
+        assertBearerToken(resp);
+    }
+
+    @Test
+    public void testSuccessDPoPJWTNoAudience() throws JOSEException, IOException {
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", Map.of(
+                "token",
+                buildJWTDPoPToken(buildDefaultDPoPClaimsSet("sub", null, "mockJkt"),
+                        signingKey.getPrivateKey(), "RS256").toJSONObject().getAsString("access_token"),
+                "token_type",
+                "access_token"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final TokenIntrospectionSuccessResponse resp =
+                parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+        Assert.assertTrue(resp.isActive());
+        assertDPoPToken(resp, "mockJkt");
+        Assert.assertEquals(resp.getClientID().getValue(), clientId);
+        Assert.assertEquals(resp.getScope(), scope);
     }
 
     @Test
@@ -424,6 +542,29 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         final TokenIntrospectionSuccessResponse resp =
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertTrue(resp.isActive());
+        assertBearerToken(resp);
+        Assert.assertEquals(resp.getClientID().getValue(), "https://sp2.example.org");
+        Assert.assertEquals(resp.getScope(), scope);
+        Assert.assertEquals(resp.getAudience(),
+                List.of(new Audience("https://sp.example.org"), new Audience(clientId)));
+    }
+
+    @Test
+    public void testSuccessDPoPJWTAudience() throws JOSEException, IOException {
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret);
+        setHttpFormRequest("POST", Map.of(
+                "token",
+                buildJWTDPoPToken(buildDefaultDPoPClaimsSet("https://sp2.example.org",
+                        "sub", List.of("https://sp.example.org", clientId), "mockJkt"),
+                        signingKey.getPrivateKey(), "RS256").toJSONObject().getAsString("access_token"),
+                "token_type",
+                "access_token"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final TokenIntrospectionSuccessResponse resp =
+                parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+        Assert.assertTrue(resp.isActive());
+        assertDPoPToken(resp, "mockJkt");
         Assert.assertEquals(resp.getClientID().getValue(), "https://sp2.example.org");
         Assert.assertEquals(resp.getScope(), scope);
         Assert.assertEquals(resp.getAudience(),
@@ -570,4 +711,39 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         Assert.assertNull(resp.getAudience());
     }
 
+    protected void assertBearerToken(final TokenIntrospectionSuccessResponse resp) {
+        Assert.assertNull(resp.getJWKThumbprintConfirmation());
+        Assert.assertEquals(resp.getTokenType(), AccessTokenType.BEARER);
+   }
+
+    protected void assertDPoPToken(final TokenIntrospectionSuccessResponse resp, final String jkt) {
+        final JWKThumbprintConfirmation cnf = resp.getJWKThumbprintConfirmation();
+        Assert.assertNotNull(cnf);
+        Assert.assertEquals(cnf.getValue().toString(), jkt);
+        Assert.assertEquals(resp.getTokenType(), AccessTokenType.DPOP);
+    }
+
+    protected AccessTokenClaimsSet buildDefaultDPoPClaimsSet(final String subject, final Collection<String> audience,
+            final String jkt) {
+        return buildDefaultDPoPClaimsSet(clientId, subject, audience, jkt);
+    }
+
+    @SuppressWarnings("null")
+    protected AccessTokenClaimsSet buildDefaultDPoPClaimsSet(final String clientId, final String subject,
+            final Collection<String> audience, final String jkt) {
+        return new AccessTokenClaimsSet.Builder()
+                .setJWTID(idGenerator)
+                .setClientID(new ClientID(clientId))
+                .setIssuer("https://op.example.org")
+                .setSubject(subject)
+                .setIssuedAt(Instant.now())
+                .setNotBefore(Instant.now().minusSeconds(300))
+                .setExpiresAt(Instant.now().plusSeconds(1800))
+                .setAuthenticationTime(Instant.now())
+                .setScope(scope)
+                .setAudience(audience)
+                .setDpopProofJwkThumbprint(jkt)
+                .build();
+    }
+
 }
\ No newline at end of file
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 d16c4760..665207a7 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
@@ -882,6 +882,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+        final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+        final Map<String, Object> cnf = claimsSet.getClaimsSet().getJSONObjectClaim("cnf");
+        Assert.assertNotNull(cnf);
+        final String jkt = (String) cnf.get("jkt");
+        Assert.assertEquals(jkt, dpopProof.getHeader().getJWK().computeThumbprint().toString());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }
@@ -890,6 +895,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     public void testDPoPEnforcedValidGrantThumbprintNotIncluded() throws Exception {
         final String clientId = clientIdDPoPAccessToken;
         final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+        final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
+                createValidDPoPNonce());
         builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
             .setClientID(new ClientID(clientId))
             .setIssuer("https://op.example.org")
@@ -901,8 +908,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
             .setRedirectURI(new URI(redirectUri))
             .setScope(scope);
         final String authorizationCode = builder.build().serialize(getDataSealer());
-        request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
-                createValidDPoPNonce()).serialize());
+        request.addHeader("DPoP", dpopProof.serialize());
 
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
                 "authorization_code",
@@ -912,6 +918,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+        final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+        final Map<String, Object> cnf = claimsSet.getClaimsSet().getJSONObjectClaim("cnf");
+        Assert.assertNotNull(cnf);
+        final String jkt = (String) cnf.get("jkt");
+        Assert.assertEquals(jkt, dpopProof.getHeader().getJWK().computeThumbprint().toString());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }
@@ -1021,6 +1032,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+        final SignedJWT signedJwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final Map<String, Object> cnf = signedJwt.getJWTClaimsSet().getJSONObjectClaim("cnf");
+        Assert.assertNotNull(cnf);
+        final String jkt = (String) cnf.get("jkt");
+        Assert.assertEquals(jkt, dpopProof.getHeader().getJWK().computeThumbprint().toString());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }
@@ -1029,6 +1045,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     public void testDPoPJwtEnforcedValidGrantThumbprintNotIncluded() throws Exception {
         final String clientId = clientIdDPoPJwtAccessToken;
         final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+        final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
+                createValidDPoPNonce());
         builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
             .setClientID(new ClientID(clientId))
             .setIssuer("https://op.example.org")
@@ -1040,8 +1058,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
             .setRedirectURI(new URI(redirectUri))
             .setScope(scope);
         final String authorizationCode = builder.build().serialize(getDataSealer());
-        request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
-                createValidDPoPNonce()).serialize());
+        request.addHeader("DPoP", dpopProof.serialize());
 
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
                 "authorization_code",
@@ -1051,6 +1068,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.DPOP);
+        final SignedJWT signedJwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+        final Map<String, Object> cnf = signedJwt.getJWTClaimsSet().getJSONObjectClaim("cnf");
+        Assert.assertNotNull(cnf);
+        final String jkt = (String) cnf.get("jkt");
+        Assert.assertEquals(jkt, dpopProof.getHeader().getJWK().computeThumbprint().toString());
         Assert.assertNotNull(response.getTokens().getRefreshToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }

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


More information about the commits mailing list