This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 3.6.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 1fb4048da090e52e2f167527951c1e9d5c566d98 Author: Naveed Khan <[email protected]> AuthorDate: Sun Nov 1 21:16:23 2026 +0530 keep mtls certificate binding on refreshed access tokens (#3652) * keep mtls certificate binding on refreshed access tokens * test cnf claim on refreshed jwt and refresh without a certificate (cherry picked from commit 950c270eb25e9373b7dde7211d608dd20f043dce) --- .../oauth2/provider/AbstractOAuthDataProvider.java | 18 +++++--- .../provider/AbstractOAuthDataProviderTest.java | 54 ++++++++++++++++++++++ .../provider/JCacheJWTOAuthDataProviderTest.java | 38 +++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java index 4003d40772f..f1fdd0f5a15 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java +++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProvider.java @@ -86,6 +86,16 @@ public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, Cl atReg.getClient(), atReg.getApprovedScope()), atReg.getSubject()); + setCertificateThumbprintConfirmation(at); + + if (isUseJwtFormatForAccessTokens()) { + convertToJWTAccessToken(at); + } + + return at; + } + + private void setCertificateThumbprintConfirmation(ServerAccessToken at) { if (messageContext != null) { String certCnf = (String)messageContext.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256); if (certCnf != null) { @@ -93,12 +103,6 @@ public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, Cl at.getExtraProperties().put(JoseConstants.HEADER_X509_THUMBPRINT_SHA256, certCnf); } } - - if (isUseJwtFormatForAccessTokens()) { - convertToJWTAccessToken(at); - } - - return at; } //CHECKSTYLE:OFF @@ -476,6 +480,8 @@ public abstract class AbstractOAuthDataProvider implements OAuthDataProvider, Cl oldRefreshToken.getGrantType(), oldRefreshToken.getNonce(), null, theNewScopes, oldRefreshToken.getSubject()); + setCertificateThumbprintConfirmation(at); + if (isUseJwtFormatForAccessTokens()) { convertToJWTAccessToken(at); } diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java index 732fb18e58c..4406ba36958 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java +++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/AbstractOAuthDataProviderTest.java @@ -25,6 +25,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.rs.security.jose.common.JoseConstants; import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; import org.apache.cxf.rs.security.jose.jws.JwsSignatureProvider; @@ -48,6 +50,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; abstract class AbstractOAuthDataProviderTest { private static KeyPair keyPair; @@ -403,6 +407,56 @@ abstract class AbstractOAuthDataProviderTest { assertNotNull(getProvider().getAccessToken(atA.getTokenKey())); } + @Test + public void testRefreshedAccessTokenKeepsCertificateBinding() { + MessageContext mc = mock(MessageContext.class); + when(mc.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)).thenReturn("thumbprint"); + getProvider().setMessageContext(mc); + + Client c = addClient("101", "bob"); + + AccessTokenRegistration atr = new AccessTokenRegistration(); + atr.setClient(c); + atr.setApprovedScope(Arrays.asList("a", "refreshToken")); + atr.setSubject(c.getResourceOwnerSubject()); + + ServerAccessToken at = getProvider().createAccessToken(atr); + assertEquals("thumbprint", at.getExtraProperties().get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)); + + ServerAccessToken at2 = getProvider().refreshAccessToken(c, at.getRefreshToken(), Collections.emptyList()); + assertEquals("thumbprint", at2.getExtraProperties().get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)); + } + + /** + * The binding follows the certificate on the current request, as it does for the initial grant, + * so a refresh without a certificate issues an unbound token. + */ + @Test + public void testRefreshWithoutCertificateIssuesUnboundAccessToken() { + MessageContext mc = mock(MessageContext.class); + when(mc.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)).thenReturn("thumbprint"); + getProvider().setMessageContext(mc); + + Client c = addClient("101", "bob"); + + AccessTokenRegistration atr = new AccessTokenRegistration(); + atr.setClient(c); + atr.setApprovedScope(Arrays.asList("a", "refreshToken")); + atr.setSubject(c.getResourceOwnerSubject()); + + ServerAccessToken at = getProvider().createAccessToken(atr); + assertEquals("thumbprint", at.getExtraProperties().get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)); + + when(mc.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)).thenReturn(null); + + ServerAccessToken at2 = getProvider().refreshAccessToken(c, at.getRefreshToken(), Collections.emptyList()); + assertNull(at2.getExtraProperties().get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)); + if (getProvider().isUseJwtFormatForAccessTokens()) { + JwtToken jwt = new JwsJwtCompactConsumer(at2.getTokenKey()).getJwtToken(); + assertNull(jwt.getClaim(JwtConstants.CLAIM_CONFIRMATION)); + } + } + protected Client addClient(String clientId, String userLogin) { Client c = new Client(); c.setRedirectUris(Collections.singletonList("http://client/redirect")); diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/JCacheJWTOAuthDataProviderTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/JCacheJWTOAuthDataProviderTest.java index 53b7741a9d0..07f12b98d8b 100644 --- a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/JCacheJWTOAuthDataProviderTest.java +++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/JCacheJWTOAuthDataProviderTest.java @@ -18,7 +18,24 @@ */ package org.apache.cxf.rs.security.oauth2.provider; +import java.util.Arrays; +import java.util.Collections; + +import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.rs.security.jose.common.JoseConstants; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jwt.JwtConstants; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration; +import org.apache.cxf.rs.security.oauth2.common.Client; +import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken; + import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class JCacheJWTOAuthDataProviderTest extends AbstractOAuthDataProviderTest { @@ -30,4 +47,25 @@ public class JCacheJWTOAuthDataProviderTest extends AbstractOAuthDataProviderTes setProvider(provider); } + @Test + public void testRefreshedJwtAccessTokenKeepsCertificateBinding() { + MessageContext mc = mock(MessageContext.class); + when(mc.get(JoseConstants.HEADER_X509_THUMBPRINT_SHA256)).thenReturn("thumbprint"); + getProvider().setMessageContext(mc); + + Client c = addClient("101", "bob"); + + AccessTokenRegistration atr = new AccessTokenRegistration(); + atr.setClient(c); + atr.setApprovedScope(Arrays.asList("a", "refreshToken")); + atr.setSubject(c.getResourceOwnerSubject()); + + ServerAccessToken at = getProvider().createAccessToken(atr); + ServerAccessToken at2 = getProvider().refreshAccessToken(c, at.getRefreshToken(), Collections.emptyList()); + + JwtToken jwt = new JwsJwtCompactConsumer(at2.getTokenKey()).getJwtToken(); + assertEquals(Collections.singletonMap(JoseConstants.HEADER_X509_THUMBPRINT_SHA256, "thumbprint"), + jwt.getClaim(JwtConstants.CLAIM_CONFIRMATION)); + } + }
