RefreshToken invalidation vulnerability
Description
ZITADEL is a combination of Auth0 and Keycloak. RefreshTokens is an OAuth 2.0 feature that allows applications to retrieve new access tokens and refresh the user's session without the need for interacting with a UI. RefreshTokens were not invalidated when a user was locked or deactivated. The deactivated or locked user was able to obtain a valid access token only through a refresh token grant. When the locked or deactivated user’s session was already terminated (“logged out”) then it was not possible to create a new session. Renewal of access token through a refresh token grant is limited to the configured amount of time (RefreshTokenExpiration). As a workaround, ensure the RefreshTokenExpiration in the OIDC settings of your instance is set according to your security requirements. This issue has been patched in versions 2.17.3 and 2.16.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/zitadel/zitadelGo | >= 2.17.0, < 2.17.3 | 2.17.3 |
github.com/zitadel/zitadelGo | >= 2.0.0, < 2.16.4 | 2.16.4 |
Affected products
1Patches
2fc892c52a10cfix: deactivate refresh tokens of deactivated or locked users
4 files changed · +106 −6
internal/command/user.go+1 −1 modified@@ -261,7 +261,7 @@ func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteMo if err != nil { return nil, nil, err } - if !isUserStateExists(userWriteModel.UserState) { + if userWriteModel.UserState != domain.UserStateActive { return nil, nil, errors.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound") }
internal/command/user_human_refresh_token_model.go+8 −1 modified@@ -18,6 +18,7 @@ type HumanRefreshTokenWriteModel struct { UserState domain.UserState IdleExpiration time.Time Expiration time.Time + UserAgentID string } func NewHumanRefreshTokenWriteModel(userID, resourceOwner, tokenID string) *HumanRefreshTokenWriteModel { @@ -48,6 +49,8 @@ func (wm *HumanRefreshTokenWriteModel) AppendEvents(events ...eventstore.Event) continue } wm.WriteModel.AppendEvents(e) + default: + wm.WriteModel.AppendEvents(e) } } } @@ -61,14 +64,18 @@ func (wm *HumanRefreshTokenWriteModel) Reduce() error { wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration) wm.Expiration = e.CreationDate().Add(e.Expiration) wm.UserState = domain.UserStateActive + wm.UserAgentID = e.UserAgentID case *user.HumanRefreshTokenRenewedEvent: if wm.UserState == domain.UserStateActive { wm.RefreshToken = e.RefreshToken } wm.RefreshToken = e.RefreshToken wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration) + case *user.HumanSignedOutEvent: + if wm.UserAgentID == e.UserAgentID { + wm.UserState = domain.UserStateDeleted + } case *user.HumanRefreshTokenRemovedEvent, - *user.HumanSignedOutEvent, *user.UserLockedEvent, *user.UserDeactivatedEvent, *user.UserRemovedEvent:
internal/command/user_human_refresh_token_test.go+89 −2 modified@@ -64,10 +64,16 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) { }, }, { - name: "add refresh token, user inactive, error", + name: "add refresh token, user deactivated, error", fields: fields{ eventstore: eventstoreExpect(t, - expectFilter(), + expectFilter( + eventFromEventPusher( + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + ), + ), + ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "refreshTokenID1"), }, @@ -912,6 +918,87 @@ func TestCommands_renewRefreshToken(t *testing.T) { err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "user deactivated, error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithCreationDateNow(user.NewHumanRefreshTokenAddedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "tokenID", + "applicationID", + "userAgentID", + "de", + []string{"clientID1"}, + []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}, + []string{"password"}, + time.Now(), + 1*time.Hour, + 24*time.Hour, + )), + eventFromEventPusher( + user.NewUserDeactivatedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + ), + ), + ), + ), + keyAlgorithm: refreshTokenEncryptionAlgorithm(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "userID", + orgID: "orgID", + refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")), + idleExpiration: 1 * time.Hour, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user signedout, error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithCreationDateNow(user.NewHumanRefreshTokenAddedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "tokenID", + "applicationID", + "userAgentID", + "de", + []string{"clientID1"}, + []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}, + []string{"password"}, + time.Now(), + 1*time.Hour, + 24*time.Hour, + )), + eventFromEventPusher( + user.NewHumanSignedOutEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "userAgentID", + ), + ), + ), + ), + keyAlgorithm: refreshTokenEncryptionAlgorithm(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "userID", + orgID: "orgID", + refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")), + idleExpiration: 1 * time.Hour, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "token renewed, ok", fields: fields{
internal/repository/user/human.go+8 −2 modified@@ -396,7 +396,13 @@ func NewHumanSignedOutEvent( } func HumanSignedOutEventMapper(event *repository.Event) (eventstore.Event, error) { - return &HumanSignedOutEvent{ + signedOut := &HumanSignedOutEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil + } + err := json.Unmarshal(event.Data, signedOut) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-WFS3g", "unable to unmarshal human signed out") + } + + return signedOut, nil }
301e22c4956efix: deactivate refresh tokens of deactivated or locked users
4 files changed · +106 −6
internal/command/user.go+1 −1 modified@@ -261,7 +261,7 @@ func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteMo if err != nil { return nil, nil, err } - if !isUserStateExists(userWriteModel.UserState) { + if userWriteModel.UserState != domain.UserStateActive { return nil, nil, errors.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound") }
internal/command/user_human_refresh_token_model.go+8 −1 modified@@ -18,6 +18,7 @@ type HumanRefreshTokenWriteModel struct { UserState domain.UserState IdleExpiration time.Time Expiration time.Time + UserAgentID string } func NewHumanRefreshTokenWriteModel(userID, resourceOwner, tokenID string) *HumanRefreshTokenWriteModel { @@ -48,6 +49,8 @@ func (wm *HumanRefreshTokenWriteModel) AppendEvents(events ...eventstore.Event) continue } wm.WriteModel.AppendEvents(e) + default: + wm.WriteModel.AppendEvents(e) } } } @@ -61,14 +64,18 @@ func (wm *HumanRefreshTokenWriteModel) Reduce() error { wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration) wm.Expiration = e.CreationDate().Add(e.Expiration) wm.UserState = domain.UserStateActive + wm.UserAgentID = e.UserAgentID case *user.HumanRefreshTokenRenewedEvent: if wm.UserState == domain.UserStateActive { wm.RefreshToken = e.RefreshToken } wm.RefreshToken = e.RefreshToken wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration) + case *user.HumanSignedOutEvent: + if wm.UserAgentID == e.UserAgentID { + wm.UserState = domain.UserStateDeleted + } case *user.HumanRefreshTokenRemovedEvent, - *user.HumanSignedOutEvent, *user.UserLockedEvent, *user.UserDeactivatedEvent, *user.UserRemovedEvent:
internal/command/user_human_refresh_token_test.go+89 −2 modified@@ -64,10 +64,16 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) { }, }, { - name: "add refresh token, user inactive, error", + name: "add refresh token, user deactivated, error", fields: fields{ eventstore: eventstoreExpect(t, - expectFilter(), + expectFilter( + eventFromEventPusher( + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + ), + ), + ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "refreshTokenID1"), }, @@ -912,6 +918,87 @@ func TestCommands_renewRefreshToken(t *testing.T) { err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "user deactivated, error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithCreationDateNow(user.NewHumanRefreshTokenAddedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "tokenID", + "applicationID", + "userAgentID", + "de", + []string{"clientID1"}, + []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}, + []string{"password"}, + time.Now(), + 1*time.Hour, + 24*time.Hour, + )), + eventFromEventPusher( + user.NewUserDeactivatedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + ), + ), + ), + ), + keyAlgorithm: refreshTokenEncryptionAlgorithm(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "userID", + orgID: "orgID", + refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")), + idleExpiration: 1 * time.Hour, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user signedout, error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithCreationDateNow(user.NewHumanRefreshTokenAddedEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "tokenID", + "applicationID", + "userAgentID", + "de", + []string{"clientID1"}, + []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}, + []string{"password"}, + time.Now(), + 1*time.Hour, + 24*time.Hour, + )), + eventFromEventPusher( + user.NewHumanSignedOutEvent( + context.Background(), + &user.NewAggregate("userID", "orgID").Aggregate, + "userAgentID", + ), + ), + ), + ), + keyAlgorithm: refreshTokenEncryptionAlgorithm(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "userID", + orgID: "orgID", + refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")), + idleExpiration: 1 * time.Hour, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "token renewed, ok", fields: fields{
internal/repository/user/human.go+8 −2 modified@@ -396,7 +396,13 @@ func NewHumanSignedOutEvent( } func HumanSignedOutEventMapper(event *repository.Event) (eventstore.Event, error) { - return &HumanSignedOutEvent{ + signedOut := &HumanSignedOutEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), - }, nil + } + err := json.Unmarshal(event.Data, signedOut) + if err != nil { + return nil, errors.ThrowInternal(err, "USER-WFS3g", "unable to unmarshal human signed out") + } + + return signedOut, nil }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-6rrr-78xp-5jp8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-22492ghsaADVISORY
- github.com/zitadel/zitadel/commit/301e22c4956ead6014a8179463c37263f7301a83ghsax_refsource_MISCWEB
- github.com/zitadel/zitadel/commit/fc892c52a10cd4ffdac395747494f3a93a7494c2ghsax_refsource_MISCWEB
- github.com/zitadel/zitadel/releases/tag/v2.16.4ghsaWEB
- github.com/zitadel/zitadel/releases/tag/v2.17.3ghsaWEB
- github.com/zitadel/zitadel/security/advisories/GHSA-6rrr-78xp-5jp8ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.