Skip to content

refactor cnf verification of refresh tokens#50927

Merged
pskopek merged 2 commits into
keycloak:mainfrom
graziang:issue-50459
Jul 17, 2026
Merged

refactor cnf verification of refresh tokens#50927
pskopek merged 2 commits into
keycloak:mainfrom
graziang:issue-50459

Conversation

@graziang

@graziang graziang commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Closes #50459

Extracts the cnf binding logic from verifyRefreshToken into a dedicated verifyConfirmationBinding method.
Adds client-config guards so that tokens issued before enabling a binding type (MTLS, DPoP, ABCA)
are rejected, before the PR only MTLS was rejected. Also rejects refresh tokens with a cnf claim with no known binding type.

Copilot AI review requested due to automatic review settings July 15, 2026 14:18
@graziang
graziang requested a review from a team as a code owner July 15, 2026 14:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates refresh-token cnf verification in TokenManager.

Changes:

  • Validates mTLS certificate bindings from x5t#S256.
  • Retains DPoP/ABCA validation and rejects unsupported confirmations.

AccessToken.Confirmation cnf = refreshToken.getConfirmation();
if (cnf != null) {
// MTLS certificate binding (x5t#S256)
if (cnf.getCertThumbprint() != null) {
Comment on lines +393 to +394
if (cnf.getCertThumbprint() == null && cnf.getKeyThumbprint() == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token confirmation");
Copilot AI review requested due to automatic review settings July 15, 2026 14:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment on lines +351 to +352
AccessToken.Confirmation cnf = refreshToken.getConfirmation();
if (cnf != null) {
Comment on lines +354 to +355
if (cnf.getCertThumbprint() != null) {
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request, session)) {
Copilot AI review requested due to automatic review settings July 15, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java Outdated
Comment thread services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java Outdated
@graziang
graziang marked this pull request as draft July 15, 2026 16:02
@graziang
graziang marked this pull request as ready for review July 16, 2026 11:41
Copilot AI review requested due to automatic review settings July 16, 2026 11:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment on lines +369 to +373
// DPoP required for public client but token has no DPoP binding (RFC 9449 §5)
if (clientConfig.isUseDPoP() && client.isPublicClient()) {
boolean dpopBound = cnf != null && cnf.getKeyThumbprint() != null && (cnf.getJktType() == null || DPOP_JKT_TYPE.equals(cnf.getJktType()));
if (!dpopBound) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "DPoP proof key binding is required");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DPoP and ABCA are alternatives to MTLS HoK, not complements.

Comment on lines +377 to +381
// ABCA attestation present but token has no attestation binding
if (abcaResult != null) {
boolean abcaBound = cnf != null && cnf.getKeyThumbprint() != null && ABCA_JKT_TYPE.equals(cnf.getJktType());
if (!abcaBound) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Attestation-based key binding is required for this refresh token");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DPoP and ABCA are alternatives to MTLS HoK, not complements.

Comment thread tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java Outdated
Comment thread tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java Outdated
Closes keycloak#50459

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
Copilot AI review requested due to automatic review settings July 16, 2026 12:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +377 to +381
// ABCA attestation present but token has no attestation binding
if (abcaResult != null) {
boolean abcaBound = cnf != null && cnf.getKeyThumbprint() != null && ABCA_JKT_TYPE.equals(cnf.getJktType());
if (!abcaBound) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Attestation-based key binding is required for this refresh token");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DPoP and ABCA are alternatives to MTLS HoK, not complements.

@lhanusov lhanusov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@graziang nice, thanks for finising it :-), please take a look on my comments and consider applying them.

Comment on lines +401 to +403
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN);

String code = oauth.parseLoginResponse().getCode();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to switch the position of these two methods, it may be a potential flaky test candidate if done otherwise.

Suggested change
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN);
String code = oauth.parseLoginResponse().getCode();
String code = oauth.parseLoginResponse().getCode();
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN);

Comment on lines +442 to +444
EventAssertion.assertSuccess(events.poll()).type(EventType.LOGIN);

String code = oauth.parseLoginResponse().getCode();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, swap the position, discussed above

assertEquals(200, response.getStatusCode());
refreshTokenString = response.getRefreshToken();

events.poll();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you need to skip 1 event or more you can do

Suggested change
events.poll();
events.skip(1);


@Test
public void refreshTokenRejectedAfterEnablingCNFCheck() throws Exception {
ClientRepresentation clientRep = testAppClient.toRepresentation();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, consider removing testAppClient and replacing it with oauth.clientResource()
potentially doing it in the whole class, but it's out of scope of this PR, since it's refactoring

Closes keycloak#50459

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
Copilot AI review requested due to automatic review settings July 16, 2026 14:21
@graziang

Copy link
Copy Markdown
Contributor Author

@graziang nice, thanks for finising it :-), please take a look on my comments and consider applying them.

@lhanusov thanks! applied all the suggestions!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +431 to +433
ClientRepresentation clientRep = oauth.clientResource().toRepresentation();
clientRep.setPublicClient(true);
oauth.clientResource().update(clientRep);
Comment on lines +377 to +381
// ABCA attestation present but token has no attestation binding
if (abcaResult != null) {
boolean abcaBound = cnf != null && cnf.getKeyThumbprint() != null && ABCA_JKT_TYPE.equals(cnf.getJktType());
if (!abcaBound) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Attestation-based key binding is required for this refresh token");
@pskopek
pskopek merged commit bd3c4d0 into keycloak:main Jul 17, 2026
91 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify cnf verification of refresh tokens

4 participants