Merge commit from fork

* fix: block Login V2 auth for users in deactivated organizations

Enforce organization state on session creation, OIDC token issuance/refresh,
and SAML session creation so deactivated org users cannot authenticate.

Co-authored-by: Cursor <[email protected]>

* fix: reject claim serving for users in deactivated organizations

Require an active resource-owner org in OIDC userinfo and SAML attribute
paths, and use distinct error IDs for inactive user vs inactive org at
token issuance.

Co-authored-by: Cursor <[email protected]>

* fix: drop sessions and tokens when an organization is deactivated

Mirror OrgRemoved cleanup for OrgDeactivated in the V2 session projection
and V1 auth user_session, token, and refresh_token handlers. Also delete
V2 sessions on OrgRemoved, which does not emit per-user removal events.

Co-authored-by: Cursor <[email protected]>

* fix: reject API tokens from deactivated organizations

Check the caller's resource-owner org state in authz middleware via a
cached OrgByID lookup, so already-issued tokens lose ZITADEL API access
when their organization is deactivated. Return unauthenticated (401).
Target org remains unrestricted so instance admins can still manage
deactivated orgs.

Co-authored-by: Cursor <[email protected]>

* fix: invalidate OIDC refresh tokens after org deactivation

Reject refresh exchange if the user's organization was deactivated
after the refresh token was issued, so grants stay dead after reactivate.

Co-authored-by: Cursor <[email protected]>

* fix: invalidate V2 access tokens after org deactivation

Treat OrgDeactivated after the token position as session termination in
ActiveAccessTokenByToken, so issued ATs stay dead after reactivation.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Livio Spring
2026-08-14 06:12:45 +02:00
committed by GitHub
co-authored by Cursor
parent a4266fa7fc
commit 260446f91f
52 changed files with 854 additions and 20 deletions
+60
View File
@@ -22,6 +22,7 @@ import (
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/authrequest"
"github.com/zitadel/zitadel/internal/repository/oidcsession"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/sessionlogout"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -388,6 +389,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
@@ -523,6 +525,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
@@ -664,6 +667,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -885,6 +889,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -981,6 +986,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1062,6 +1068,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1160,6 +1167,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1258,6 +1266,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1354,6 +1363,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -1459,6 +1469,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
),
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID"),
@@ -1516,6 +1527,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
user.NewUserImpersonatedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "clientID", &domain.TokenActor{
@@ -1799,6 +1811,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1835,6 +1848,48 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-J39h2", "Errors.User.NotActive"),
},
},
{
"org deactivated after refresh token issuance",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
&domain.UserAgent{FingerprintID: gu.Ptr("browserFP")},
),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDate(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
testNow,
),
),
expectFilter(
eventFromEventPusherWithCreationDate(
org.NewOrgDeactivatedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate),
testNow.Add(time.Minute),
),
),
),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID",
scope: []string{"openid", "offline_access"},
complianceCheck: mockRefreshTokenComplianceChecker(nil),
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-oR9nR", "Errors.OIDCSession.RefreshTokenInvalid"),
},
},
{
"refresh with an invalid client id fails",
fields{
@@ -1856,6 +1911,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1871,6 +1927,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(),
),
idGenerator: mock.NewIDGeneratorExpectIDs(t),
@@ -1908,6 +1965,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
expectFilter(
user.NewHumanAddedEvent(
context.Background(),
@@ -1923,6 +1981,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
false,
),
),
expectFilterActiveOrg("org1"),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@@ -2121,6 +2180,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
),
),
expectFilter(), // no OrgDeactivated after refresh token issuance
),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},