[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