[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 Aug 9 06:58:41 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=cf08bcd5558a87ce51f3265978b7dfa2a41e862f
The following commit(s) were added to refs/heads/main by this push:
new cf08bcd5 JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
cf08bcd5 is described below
commit cf08bcd5558a87ce51f3265978b7dfa2a41e862f
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Aug 9 09:58:22 2024 +0300
JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
https://shibboleth.atlassian.net/browse/JOIDC-201
Bind the DPoP key to refresh token of public clients as mandated by section 5 of the spec.
Confidential clients may rotate the key as they authenticate to the token endpoint with the
refresh token anyway.
---
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 31 +++++-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 121 +++++++++++++++++++++
2 files changed, 151 insertions(+), 1 deletion(-)
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 f1916ad5..3c2c087c 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
@@ -36,7 +36,9 @@ import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
@@ -46,6 +48,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
import net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate;
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenChainLifetimeLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RevocationLifetimeLookupFunction;
@@ -396,11 +399,18 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
assert oidcResponseContext != null;
oidcResponseContext.setAuthorizationGrantClaimsSet(tokenClaimsSet);
- final String claimsSetThumbprint = tokenClaimsSet.getDpopProofJwkThumbprint();
+ final String claimsSetThumbprint = GrantType.REFRESH_TOKEN.equals(grant.getType()) && !isPublicClient() ?
+ null : tokenClaimsSet.getDpopProofJwkThumbprint();
final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
if (claimsSetThumbprint == null) {
oidcResponseContext.setDpopProofJwkThumbprint(proofThumbprint);
} else if (proofThumbprint == null) {
+ if (GrantType.REFRESH_TOKEN.equals(grant.getType()) && isPublicClient()) {
+ log.warn("{} Missing mandatory DPoP proof for public client with refresh token: {}", getLogPrefix(),
+ clientId.getValue());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
oidcResponseContext.setDpopProofJwkThumbprint(claimsSetThumbprint);
} else if (!claimsSetThumbprint.equals(proofThumbprint)) {
log.warn("{} Invalid DPoP Proof thumbprint issued to client {}, invalid for {}", getLogPrefix(),
@@ -413,6 +423,25 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
}
// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount ON
+ /**
+ * Checks if the client is a public client. If not metadata exists for the client, it's assumed to be confidential.
+ * The client is considered as confidential if its metadata doesn't specify "none" as the token endpoint
+ * authentication method.
+ *
+ * @return true if the client is public, false otherwise.
+ */
+ protected boolean isPublicClient() {
+ final OIDCMetadataContext metadataContext = getMetadataContext();
+ if (metadataContext != null) {
+ final OIDCClientInformation clientInformation = metadataContext.getClientInformation();
+ if (clientInformation != null) {
+ return ClientAuthenticationMethod.NONE.equals(
+ clientInformation.getOIDCMetadata().getTokenEndpointAuthMethod());
+ }
+ }
+ return false;
+ }
+
/**
* Attempt to deseriaalize a (serialized) refresh token value via configured deserializers.
*
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 665207a7..9b6a49ba 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
@@ -747,6 +747,127 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(response.getOIDCTokens().getIDToken());
}
+ @Test
+ public void testDPoPValidGrantThumbprintIncludedMatchingProof_publicClient() throws Exception {
+ final String clientId = clientIdPkcePlainPublic;
+ 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")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setCodeChallenge(plainVerifier())
+ .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, null, null,
+ codeVerifier), false, ClientAuthenticationMethod.NONE);
+ 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 testDPoPValidGrantThumbprintIncludedMatchingProof_publicClientRefresh() throws Exception {
+ final String clientId = clientIdPkcePlainPublic;
+ final RefreshTokenClaimsSet.Builder builder = new RefreshTokenClaimsSet.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")
+ .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 refreshToken = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ refreshToken, clientId), false, ClientAuthenticationMethod.NONE);
+ 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 testDPoPValidGrantThumbprintIncludedNonMatchingProof_publicClientRefresh() throws Exception {
+ final String clientId = clientIdPkcePlainPublic;
+ final RefreshTokenClaimsSet.Builder builder = new RefreshTokenClaimsSet.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")
+ .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 refreshToken = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ refreshToken, clientId), false, ClientAuthenticationMethod.NONE);
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @Test
+ public void testDPoPValidGrantThumbprintIncludedNonMatchingProof_confidentialClientRefresh() throws Exception {
+ final RefreshTokenClaimsSet.Builder builder = new RefreshTokenClaimsSet.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")
+ .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 refreshToken = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ refreshToken, 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();
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list