Changed userId value for refresh token events

Closes #28567

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-04-10 10:00:56 +02:00 committed by Marek Posolda
parent 33f580daa4
commit 33b747286e
11 changed files with 41 additions and 23 deletions

View file

@ -103,6 +103,13 @@ You can use the `Subject (sub)` mapper to configure the `sub` claim only for acc
The mapper has no effects for service accounts, because no user session exists, and the`sub` claim is always added to the access token.
= Changed `userId` for events related to refresh token
The `userId` in the `REFRESH_TOKEN` event is now always taken from user session instead of `sub` claim in the refresh token. The `userId` in the `REFRESH_TOKEN_ERROR` event is now always null.
The reason for this change is that the value of the `sub` claim in the refresh token may be null with the introduction of the optional `sub` claim or even different from the real user id when using pairwise subject identifiers or other ways to override the `sub` claim.
However a `refresh_token_sub` detail is now added as backwards compatibility to have info about the user in the case of missing userId in the `REFRESH_TOKEN_ERROR` event.
= Default `http-pool-max-threads` reduced
`http-pool-max-threads` if left unset will default to the greater of 50 or 4 x (available processors). Previously it defaulted to the greater of 200 or 8 x (available processors). Reducing the number or task threads for most usage scenarios will result in slightly higher performance due to less context switching among active threads.

View file

@ -51,6 +51,7 @@ public interface Details {
String TOKEN_ID = "token_id";
String REFRESH_TOKEN_ID = "refresh_token_id";
String REFRESH_TOKEN_TYPE = "refresh_token_type";
String REFRESH_TOKEN_SUB = "refresh_token_sub";
String VALIDATE_ACCESS_TOKEN = "validate_access_token";
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
String NODE_HOST = "node_host";

View file

@ -405,9 +405,14 @@ public class TokenManager {
String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request, String scopeParameter) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(session, realm, authorizedClient, request, encodedRefreshToken, true);
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
event.session(refreshToken.getSessionState())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
if (refreshToken.getSubject() != null) {
event.detail(Details.REFRESH_TOKEN_SUB, refreshToken.getSubject());
}
// Setup clientScopes from refresh token to the context
String oldTokenScope = refreshToken.getScope();
//The requested scope MUST NOT include any scope not originally granted by the resource owner
@ -430,6 +435,8 @@ public class TokenManager {
validateTokenReuseForRefresh(session, realm, refreshToken, validation);
event.user(validation.userSession.getUser());
if (refreshToken.getAuthorization() != null) {
validation.newToken.setAuthorization(refreshToken.getAuthorization());
}

View file

@ -461,7 +461,7 @@ public class ConsentsTest extends AbstractKeycloakTest {
Assert.assertEquals(OAuthErrorException.INVALID_SCOPE, refreshTokenResponse.getError());
Assert.assertEquals("Client no longer has requested consent from user", refreshTokenResponse.getErrorDescription());
events.expectRefresh(accessTokenResponse.getRefreshToken(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
events.expectRefresh(accessTokenResponse.getRefreshToken(), sessionId).user((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
} finally {
clientRepresentation.setConsentRequired(false);
adminClient.realm(TEST_REALM_NAME).clients().get(clientRepresentation.getId()).update(clientRepresentation);

View file

@ -631,8 +631,8 @@ public final class KcOidcBrokerTransientSessionsTest extends AbstractAdvancedBro
events.expectRefresh(offlineToken.getId(), newRefreshToken.getSessionState())
.realm(consumerRealmRep)
.client(CONSUMER_BROKER_APP_CLIENT_ID)
.user((String) null)
.error(Errors.INVALID_TOKEN)
.user(lwUserId)
.clearDetails()
.assertEvent();
} finally {

View file

@ -115,7 +115,7 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
.client("client1")
.user(client1SAUserId)
.user((String) null)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.detail(Details.CLIENT_AUTH_METHOD, JWTClientAuthenticator.PROVIDER_ID)

View file

@ -272,8 +272,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
events.expectRefresh(offlineToken.getId(), newRefreshToken.getSessionState())
.client("offline-client")
.user((String) null)
.error(Errors.INVALID_TOKEN)
.user(userId)
.clearDetails()
.assertEvent();
@ -401,8 +401,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(400, response.getStatusCode());
events.expectRefresh(offlineToken.getId(), token.getSessionState())
.client("offline-client")
.user((String) null)
.error(Errors.INVALID_TOKEN)
.user(userId)
.clearDetails()
.assertEvent();
@ -411,8 +411,8 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(400, response2.getStatusCode());
events.expectRefresh(offlineToken2.getId(), offlineToken2.getSessionState())
.client("offline-client")
.user((String) null)
.error(Errors.INVALID_TOKEN)
.user(userId)
.clearDetails()
.assertEvent();

View file

@ -613,12 +613,12 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, response3.getStatusCode());
events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
events.expectRefresh(refreshToken1.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
// Client session invalidated hence old refresh token not valid anymore
OAuthClient.AccessTokenResponse response4 = oauth.doRefreshTokenRequest(response2.getRefreshToken(), "password");
assertEquals(400, response4.getStatusCode());
events.expectRefresh(refreshToken2.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
events.expectRefresh(refreshToken2.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
} finally {
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
}
@ -656,17 +656,18 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
// Second refresh (allowed).
OAuthClient.AccessTokenResponse responseFirstReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
RefreshToken newTokenFirstReuse = oauth.parseRefreshToken(responseFirstReuse.getRefreshToken());
String userId = newTokenFirstReuse.getSubject();
assertEquals(200, responseFirstReuse.getStatusCode());
events.expectRefresh(initialRefreshToken.getId(), sessionId).assertEvent();
events.expectRefresh(initialRefreshToken.getId(), sessionId).detail(Details.REFRESH_TOKEN_SUB, userId).assertEvent();
// Token reused twice, became invalid.
OAuthClient.AccessTokenResponse responseSecondReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
assertEquals(400, responseSecondReuse.getStatusCode());
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID)
events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).detail(Details.REFRESH_TOKEN_SUB, userId).removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
// Refresh token from first use became invalid.
@ -675,7 +676,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, responseUseOfInvalidatedRefreshToken.getStatusCode());
events.expectRefresh(newTokenFirstUse.getId(), sessionId).removeDetail(Details.TOKEN_ID)
events.expectRefresh(newTokenFirstUse.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
// Refresh token from reuse is not valid. Client session was invalidated
@ -684,7 +685,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, responseUseOfValidRefreshToken.getStatusCode());
events.expectRefresh(newTokenFirstReuse.getId(), sessionId).removeDetail(Details.TOKEN_ID)
events.expectRefresh(newTokenFirstReuse.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
} finally {
RealmManager.realm(adminClient.realm("test"))
@ -725,7 +726,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, responseReuseExceeded.getStatusCode());
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
} finally {
RealmManager.realm(adminClient.realm("test"))
.refreshTokenMaxReuse(0)
@ -760,7 +761,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, responseReuseExceeded.getStatusCode());
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID)
events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
@ -768,7 +769,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
// Config changed, token cannot be used again at this point due the client session invalidated
OAuthClient.AccessTokenResponse responseReuseExceeded2 = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
assertEquals(400, responseReuseExceeded2.getStatusCode());
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID)
events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
} finally {
RealmManager.realm(adminClient.realm("test"))
@ -816,7 +817,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, response3.getStatusCode());
events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).user((String) null).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
// No client sessions available after revoke
Assert.assertFalse(hasClientSessionForTestApp());
@ -863,7 +864,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
// Try to refresh with one of the old refresh tokens before SSO re-authentication - should fail
OAuthClient.AccessTokenResponse response5 = oauth.doRefreshTokenRequest(response2.getRefreshToken(), "password");
assertEquals(400, response5.getStatusCode());
events.expectRefresh(refreshToken2.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
events.expectRefresh(refreshToken2.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
} finally {
resetTimeOffset();
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
@ -1559,7 +1560,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
} finally {
UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true);
}
@ -1589,7 +1590,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
}
@Test

View file

@ -452,7 +452,9 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner")
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
.client("resource-owner")
.user((String) null)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.error(Errors.INVALID_TOKEN).assertEvent();

View file

@ -240,7 +240,7 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState())
.client("service-account-cl-refresh-on")
.user(userIdClRefreshOn)
.user((String) null)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.error(Errors.INVALID_TOKEN).assertEvent();

View file

@ -517,7 +517,7 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest {
assertEquals(400, refreshResponse.getStatusCode());
events.expectRefresh(refreshToken1.getId(), idToken.getSessionState())
.client("third-party")
.user(userId)
.user((String) null)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)