From 81f5956c719a4f51cd35d1d04ddb0f660c6008c1 Mon Sep 17 00:00:00 2001 From: Jimmy Chakkalakal Date: Wed, 8 Jul 2026 18:05:25 +0100 Subject: [PATCH 1/3] oid4vci-feat: Add provider ID detail to refresh token events and implement provider ID retrieval Closes #50342 Signed-off-by: Jimmy Chakkalakal --- .../java/org/keycloak/events/Details.java | 1 + .../refresh/OID4VCIRefreshTokenProvider.java | 5 + .../keycloak/protocol/oidc/TokenManager.java | 4 + .../endpoints/TokenRevocationEndpoint.java | 2 + .../refresh/AbstractRefreshTokenProvider.java | 3 + .../oidc/refresh/RefreshTokenProvider.java | 8 + .../OID4VCRefreshTokenProviderEventTest.java | 166 ++++++++++++++++++ 7 files changed, 189 insertions(+) create mode 100644 tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index 2fbfa4ead440..7d16015ce82e 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -60,6 +60,7 @@ public interface Details { String TOKEN_ISSUED_FOR = "token_issued_for"; String ORG_ID = "org_id"; String REFRESH_TOKEN_ID = "refresh_token_id"; + String REFRESH_TOKEN_PROVIDER_ID = "refresh_token_provider_id"; String REFRESH_TOKEN_TYPE = "refresh_token_type"; String REFRESH_TOKEN_SUB = "refresh_token_sub"; String CLIENT_ASSERTION_ID = "client_assertion_id"; diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/refresh/OID4VCIRefreshTokenProvider.java b/services/src/main/java/org/keycloak/protocol/oid4vc/refresh/OID4VCIRefreshTokenProvider.java index 29ea545caaf0..0377653172c1 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/refresh/OID4VCIRefreshTokenProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/refresh/OID4VCIRefreshTokenProvider.java @@ -234,6 +234,11 @@ public void revokeToken(AccessToken token, UserModel user, ClientModel client, E } } + @Override + public String getProviderId() { + return OID4VCIRefreshTokenProviderFactory.PROVIDER_ID; + } + // Might eventually be overridden for scenarios where a user is not available in the Keycloak DB protected UserModel getUser(RealmModel realm, RefreshToken oldToken) { String userId = oldToken.getSubject(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index c4a3285250e4..e11571c218c6 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -1290,6 +1290,10 @@ private void generateRefreshToken(boolean offlineTokenRequested) { refreshToken = refreshTokenProvider.generateRefreshToken(initialRefreshTokenContext); + if (refreshToken != null) { + String providerId = refreshToken.getProvider() != null ? refreshToken.getProvider() : refreshTokenProvider.getProviderId(); + event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerId); + } Boolean bindOnlyRefreshToken = session.getAttributeOrDefault(DPoPUtil.DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, false); if (bindOnlyRefreshToken) { DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index 5f63f7f39925..c8906331f916 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -48,6 +48,7 @@ import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.refresh.DefaultRefreshTokenProviderFactory; import org.keycloak.protocol.oidc.refresh.RefreshTokenProvider; +import org.keycloak.protocol.oidc.refresh.DefaultRefreshTokenProviderFactory; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; @@ -129,6 +130,7 @@ public Response revoke() { refreshTokenProvider.revokeToken(token, user, client, event); + event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerClaim != null ? providerClaim : DefaultRefreshTokenProviderFactory.PROVIDER_ID); event.detail(Details.REVOKED_CLIENT, client.getClientId()); event.session(token.getSessionId()); event.detail(Details.REFRESH_TOKEN_ID, token.getId()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java index ca20349f8f7f..26d1e2d44b1b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java @@ -76,6 +76,9 @@ public TokenManager.AccessTokenResponseBuilder refreshAccessToken(RefreshTokenCo .detail(Details.REFRESH_TOKEN_ID, oldRefreshToken.getId()) .detail(Details.REFRESH_TOKEN_TYPE, oldRefreshToken.getType()); + String providerIdDetail = oldRefreshToken.getProvider() != null ? oldRefreshToken.getProvider() : DefaultRefreshTokenProviderFactory.PROVIDER_ID; + event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerIdDetail); + if (oldRefreshToken.getSubject() != null) { event.detail(Details.REFRESH_TOKEN_SUB, oldRefreshToken.getSubject()); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java index 0cb8f7d9b2cb..182545db4098 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java @@ -66,6 +66,14 @@ default void revokeToken(AccessToken token, UserModel user, ClientModel client, // no-op by default } + /** + * Returns the provider ID that this instance uses to identify issued refresh tokens. + * Used for event detail emission during initial issuance. + */ + default String getProviderId() { + return DefaultRefreshTokenProviderFactory.PROVIDER_ID; + } + @Override default void close() { } diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java new file mode 100644 index 000000000000..e9775a38c73a --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java @@ -0,0 +1,166 @@ +package org.keycloak.tests.oid4vc; + +import java.util.List; + +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.protocol.oid4vc.refresh.OID4VCIRefreshTokenProviderFactory; +import org.keycloak.protocol.oidc.refresh.DefaultRefreshTokenProviderFactory; +import org.keycloak.testframework.annotations.InjectUser; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.annotations.TestSetup; +import org.keycloak.testframework.events.EventAssertion; +import org.keycloak.testframework.realm.ManagedUser; +import org.keycloak.testsuite.util.oauth.AccessTokenResponse; +import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@KeycloakIntegrationTest(config = OID4VCIssuerTestBase.VCTestServerConfig.class) +public class OID4VCRefreshTokenProviderEventTest extends OID4VCIssuerTestBase { + + @InjectUser(config = OID4VCActionTest.OID4VCTestUserConfig.class) + ManagedUser user; + + OID4VCTestContext ctx; + + @TestSetup + public void configureTestRealm() { + super.configureTestRealm(); + var realmRep = testRealm.admin().toRepresentation(); + realmRep.setEnabledEventTypes(List.of( + EventType.CODE_TO_TOKEN.toString(), + EventType.REVOKE_GRANT.toString(), + EventType.REFRESH_TOKEN.toString())); + testRealm.admin().update(realmRep); + } + + @BeforeEach + void beforeEach() { + ctx = new OID4VCTestContext(client, minimalJwtTypeCredentialScope); + user.admin().logout(); + user.admin().verifiableCredentials().getIssuedCredentials() + .forEach(issuedCred -> user.admin().verifiableCredentials().revokeIssuedCredential(issuedCred.getId())); + } + + @AfterEach + public void afterEach() { + timeOffSet.set(0); + } + + /** + * Verify that Refresh token must carry Details.REFRESH_TOKEN_PROVIDER_ID with value "oid4vci" + * when the token was issued by OID4VCIRefreshTokenProvider. + */ + @Test + public void testRefreshTokenEventCarriesOid4vciProviderId() { + AccessTokenResponse tokenResponse = authzCodeFlow(); + assertTrue(tokenResponse.isSuccess(), "Access token exchange should succeed"); + + String accessToken = tokenResponse.getAccessToken(); + String credentialIdentifier = ctx.getAuthorizedCredentialIdentifier(); + + wallet.credentialRequest(ctx, accessToken) + .credentialIdentifier(credentialIdentifier) + .send().getCredentialResponse(); + + timeOffSet.set(5); + + events.clear(); + + AccessTokenResponse refreshResponse = wallet.refreshRequest(ctx).send(); + assertTrue(refreshResponse.isSuccess(), "Refresh must succeed"); + + EventAssertion.assertSuccess(events.poll()) + .type(EventType.REFRESH_TOKEN) + .details(Details.REFRESH_TOKEN_PROVIDER_ID, + OID4VCIRefreshTokenProviderFactory.PROVIDER_ID); + } + + /** + * A standard token must carry provider id "default" + * when the token was issued by DefaultRefreshTokenProvider. + */ + @Test + public void testStandardRefreshTokenEventCarriesDefaultProviderId() { + // Perform a plain OIDC auth-code flow (no OID4VCI scope) + AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword()); + assertTrue(tokenResponse.isSuccess(), "Access token exchange should succeed"); + + String refreshToken = tokenResponse.getRefreshToken(); + AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(refreshToken); + assertTrue(refreshResponse.isSuccess(), "Standard refresh must succeed"); + + EventAssertion.assertSuccess(events.poll()) + .type(EventType.REFRESH_TOKEN) + .details(Details.REFRESH_TOKEN_PROVIDER_ID, + DefaultRefreshTokenProviderFactory.PROVIDER_ID); + } + + /** + * Verify that Revoke grant event must carry + * Details.REFRESH_TOKEN_PROVIDER_ID with value "oid4vci" + */ + @Test + public void testRevokeGrantEventCarriesOid4vciProviderId() { + ctx = new OID4VCTestContext(client, minimalJwtTypeCredentialScope); + user.admin().logout(); + + AccessTokenResponse tokenResponse = authzCodeFlow(); + assertTrue(tokenResponse.isSuccess(), "Access token exchange should succeed"); + + String accessToken = tokenResponse.getAccessToken(); + String credentialIdentifier = ctx.getAuthorizedCredentialIdentifier(); + + wallet.credentialRequest(ctx, accessToken) + .credentialIdentifier(credentialIdentifier) + .send().getCredentialResponse(); + + events.clear(); + + String refreshToken = tokenResponse.getRefreshToken(); + oauth.doTokenRevoke(refreshToken); + + EventAssertion.assertSuccess(events.poll()) + .type(EventType.REVOKE_GRANT) + .details(Details.REFRESH_TOKEN_PROVIDER_ID, + OID4VCIRefreshTokenProviderFactory.PROVIDER_ID); + } + + /** + * CODE exchange event must carry refresh_token_provider_id = "oid4vci" + */ + @Test + public void testAuthzCodeExchangeEventCarriesOid4vciProviderId() { + ctx = new OID4VCTestContext(client, minimalJwtTypeCredentialScope); + user.admin().logout(); + + AccessTokenResponse tokenResponse = authzCodeFlow(); + assertTrue(tokenResponse.isSuccess()); + + EventAssertion.assertSuccess(events.poll()) + .type(EventType.CODE_TO_TOKEN) + .details(Details.REFRESH_TOKEN_PROVIDER_ID, + OID4VCIRefreshTokenProviderFactory.PROVIDER_ID); + } + + private AccessTokenResponse authzCodeFlow() { + AuthorizationEndpointResponse authResponse = wallet.authorizationRequest() + .scope(ctx.getScope()) + .send(user.getUsername(), TEST_PASSWORD); + String code = authResponse.getCode(); + assertNotNull(code, "Authorization code should not be null"); + + AccessTokenResponse tokenResponse = wallet.accessTokenRequest(ctx, code).send(); + assertNotNull(tokenResponse, "Token response should not be null"); + + String accessToken = wallet.validateHolderAccessToken(ctx, tokenResponse); + assertNotNull(accessToken, "No accessToken"); + return tokenResponse; + } +} From 5be92350aa20999cc5aee49850fcf0bfe9bd5a1d Mon Sep 17 00:00:00 2001 From: Jimmy Chakkalakal Date: Wed, 8 Jul 2026 18:05:25 +0100 Subject: [PATCH 2/3] oid4vci-feat: Add provider ID detail to refresh token events and implement provider ID retrieval Closes #50342 Signed-off-by: Jimmy Chakkalakal --- .../main/java/org/keycloak/protocol/oidc/TokenManager.java | 7 +++---- .../protocol/oidc/endpoints/TokenRevocationEndpoint.java | 3 +-- .../oidc/refresh/AbstractRefreshTokenProvider.java | 2 +- .../protocol/oidc/refresh/RefreshTokenProvider.java | 3 ++- .../tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java | 5 +++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index e11571c218c6..af8c8e6b3e7e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -1290,10 +1290,9 @@ private void generateRefreshToken(boolean offlineTokenRequested) { refreshToken = refreshTokenProvider.generateRefreshToken(initialRefreshTokenContext); - if (refreshToken != null) { - String providerId = refreshToken.getProvider() != null ? refreshToken.getProvider() : refreshTokenProvider.getProviderId(); - event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerId); - } + String providerId = refreshToken.getProvider() != null ? refreshToken.getProvider() : refreshTokenProvider.getProviderId(); + event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerId); + Boolean bindOnlyRefreshToken = session.getAttributeOrDefault(DPoPUtil.DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, false); if (bindOnlyRefreshToken) { DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index c8906331f916..d5d259a5a499 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -48,7 +48,6 @@ import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.refresh.DefaultRefreshTokenProviderFactory; import org.keycloak.protocol.oidc.refresh.RefreshTokenProvider; -import org.keycloak.protocol.oidc.refresh.DefaultRefreshTokenProviderFactory; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; @@ -130,7 +129,7 @@ public Response revoke() { refreshTokenProvider.revokeToken(token, user, client, event); - event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerClaim != null ? providerClaim : DefaultRefreshTokenProviderFactory.PROVIDER_ID); + event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerClaim != null ? providerClaim : refreshTokenProvider.getProviderId()); event.detail(Details.REVOKED_CLIENT, client.getClientId()); event.session(token.getSessionId()); event.detail(Details.REFRESH_TOKEN_ID, token.getId()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java index 26d1e2d44b1b..c4d011c1f040 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/refresh/AbstractRefreshTokenProvider.java @@ -76,7 +76,7 @@ public TokenManager.AccessTokenResponseBuilder refreshAccessToken(RefreshTokenCo .detail(Details.REFRESH_TOKEN_ID, oldRefreshToken.getId()) .detail(Details.REFRESH_TOKEN_TYPE, oldRefreshToken.getType()); - String providerIdDetail = oldRefreshToken.getProvider() != null ? oldRefreshToken.getProvider() : DefaultRefreshTokenProviderFactory.PROVIDER_ID; + String providerIdDetail = oldRefreshToken.getProvider() != null ? oldRefreshToken.getProvider() : getProviderId(); event.detail(Details.REFRESH_TOKEN_PROVIDER_ID, providerIdDetail); if (oldRefreshToken.getSubject() != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java index 182545db4098..a599ef70042a 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/refresh/RefreshTokenProvider.java @@ -68,7 +68,8 @@ default void revokeToken(AccessToken token, UserModel user, ClientModel client, /** * Returns the provider ID that this instance uses to identify issued refresh tokens. - * Used for event detail emission during initial issuance. + * Used for event detail emission during initial issuance + * and refresh-token processing when the token does not carry a provider claim */ default String getProviderId() { return DefaultRefreshTokenProviderFactory.PROVIDER_ID; diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java index e9775a38c73a..5c82c1b1ae23 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java @@ -46,6 +46,7 @@ void beforeEach() { user.admin().logout(); user.admin().verifiableCredentials().getIssuedCredentials() .forEach(issuedCred -> user.admin().verifiableCredentials().revokeIssuedCredential(issuedCred.getId())); + events.skipAll(); } @AfterEach @@ -54,7 +55,7 @@ public void afterEach() { } /** - * Verify that Refresh token must carry Details.REFRESH_TOKEN_PROVIDER_ID with value "oid4vci" + * Verify that the REFRESH_TOKEN event carries Details.REFRESH_TOKEN_PROVIDER_ID with value "oid4vci" * when the token was issued by OID4VCIRefreshTokenProvider. */ @Test @@ -88,7 +89,7 @@ public void testRefreshTokenEventCarriesOid4vciProviderId() { */ @Test public void testStandardRefreshTokenEventCarriesDefaultProviderId() { - // Perform a plain OIDC auth-code flow (no OID4VCI scope) + // Perform a plain OIDC password grant flow (no OID4VCI scope) AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword()); assertTrue(tokenResponse.isSuccess(), "Access token exchange should succeed"); From bad26c95e6cd1ba865bc74a4c1c5d0b4f8863af0 Mon Sep 17 00:00:00 2001 From: Jimmy Chakkalakal Date: Thu, 16 Jul 2026 14:31:22 +0100 Subject: [PATCH 3/3] Worked on reviewers suggestions Closes #50342 Signed-off-by: Jimmy Chakkalakal --- .../OID4VCRefreshTokenProviderEventTest.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java index 5c82c1b1ae23..2c167f45b7f5 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCRefreshTokenProviderEventTest.java @@ -14,7 +14,6 @@ import org.keycloak.testsuite.util.oauth.AccessTokenResponse; import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,12 +45,7 @@ void beforeEach() { user.admin().logout(); user.admin().verifiableCredentials().getIssuedCredentials() .forEach(issuedCred -> user.admin().verifiableCredentials().revokeIssuedCredential(issuedCred.getId())); - events.skipAll(); - } - - @AfterEach - public void afterEach() { - timeOffSet.set(0); + events.clear(); } /** @@ -72,8 +66,6 @@ public void testRefreshTokenEventCarriesOid4vciProviderId() { timeOffSet.set(5); - events.clear(); - AccessTokenResponse refreshResponse = wallet.refreshRequest(ctx).send(); assertTrue(refreshResponse.isSuccess(), "Refresh must succeed"); @@ -115,6 +107,11 @@ public void testRevokeGrantEventCarriesOid4vciProviderId() { AccessTokenResponse tokenResponse = authzCodeFlow(); assertTrue(tokenResponse.isSuccess(), "Access token exchange should succeed"); + EventAssertion.assertSuccess(events.poll()) + .type(EventType.CODE_TO_TOKEN) + .details(Details.REFRESH_TOKEN_PROVIDER_ID, + OID4VCIRefreshTokenProviderFactory.PROVIDER_ID); + String accessToken = tokenResponse.getAccessToken(); String credentialIdentifier = ctx.getAuthorizedCredentialIdentifier(); @@ -122,8 +119,6 @@ public void testRevokeGrantEventCarriesOid4vciProviderId() { .credentialIdentifier(credentialIdentifier) .send().getCredentialResponse(); - events.clear(); - String refreshToken = tokenResponse.getRefreshToken(); oauth.doTokenRevoke(refreshToken); @@ -154,10 +149,9 @@ private AccessTokenResponse authzCodeFlow() { AuthorizationEndpointResponse authResponse = wallet.authorizationRequest() .scope(ctx.getScope()) .send(user.getUsername(), TEST_PASSWORD); - String code = authResponse.getCode(); - assertNotNull(code, "Authorization code should not be null"); + assertTrue(authResponse.isSuccess()); - AccessTokenResponse tokenResponse = wallet.accessTokenRequest(ctx, code).send(); + AccessTokenResponse tokenResponse = wallet.accessTokenRequest(ctx, authResponse.getCode()).send(); assertNotNull(tokenResponse, "Token response should not be null"); String accessToken = wallet.validateHolderAccessToken(ctx, tokenResponse);