mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
* 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]>
138 lines
4.2 KiB
Go
138 lines
4.2 KiB
Go
package authz
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type tokenVerifierMock struct {
|
|
AccessTokenVerifier
|
|
SystemTokenVerifier
|
|
existsOrg func(ctx context.Context, id, domain string) (string, error)
|
|
checkOrgActive func(ctx context.Context, orgID string) error
|
|
projectIDOrigin func(ctx context.Context, clientID string) (string, []string, error)
|
|
}
|
|
|
|
func (m *tokenVerifierMock) RegisterServer(string, string, MethodMapping) {}
|
|
|
|
func (m *tokenVerifierMock) CheckAuthMethod(string) (Option, bool) {
|
|
return Option{}, false
|
|
}
|
|
|
|
func (m *tokenVerifierMock) SearchMyMemberships(context.Context, string, bool) ([]*Membership, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *tokenVerifierMock) ExistsOrg(ctx context.Context, id, domain string) (string, error) {
|
|
if m.existsOrg != nil {
|
|
return m.existsOrg(ctx, id, domain)
|
|
}
|
|
if id != "" {
|
|
return id, nil
|
|
}
|
|
return domain, nil
|
|
}
|
|
|
|
func (m *tokenVerifierMock) CheckOrgActive(ctx context.Context, orgID string) error {
|
|
if m.checkOrgActive != nil {
|
|
return m.checkOrgActive(ctx, orgID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *tokenVerifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
|
|
if m.projectIDOrigin != nil {
|
|
return m.projectIDOrigin(ctx, clientID)
|
|
}
|
|
return "", nil, nil
|
|
}
|
|
|
|
func TestVerifyTokenAndCreateCtxData_orgActive(t *testing.T) {
|
|
activeCaller := AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
|
|
return "user1", "", "", "", "caller-org", nil
|
|
})
|
|
systemTokenNOK := SystemTokenVerifierFunc(func(context.Context, string, string) (Memberships, string, error) {
|
|
return nil, "", zerrors.ThrowUnauthenticated(nil, "TEST-sys", "unauthenticated")
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
orgID string
|
|
orgDomain string
|
|
accessToken AccessTokenVerifier
|
|
checkOrgActive func(context.Context, string) error
|
|
wantErr func(error) bool
|
|
wantResource string
|
|
wantOrgID string
|
|
}{
|
|
{
|
|
name: "active caller org",
|
|
accessToken: activeCaller,
|
|
wantResource: "caller-org",
|
|
wantOrgID: "caller-org",
|
|
},
|
|
{
|
|
name: "deactivated caller org",
|
|
accessToken: activeCaller,
|
|
checkOrgActive: func(_ context.Context, orgID string) error {
|
|
assert.Equal(t, "caller-org", orgID)
|
|
return zerrors.ThrowPreconditionFailed(nil, "QUERY-oR9nA", "Errors.Org.NotActive")
|
|
},
|
|
wantErr: zerrors.IsUnauthenticated,
|
|
},
|
|
{
|
|
name: "deactivated target org with active caller",
|
|
orgID: "target-org",
|
|
accessToken: activeCaller,
|
|
checkOrgActive: func(_ context.Context, orgID string) error {
|
|
// Must check the caller's org, not the target.
|
|
assert.Equal(t, "caller-org", orgID)
|
|
return nil
|
|
},
|
|
wantResource: "caller-org",
|
|
wantOrgID: "target-org",
|
|
},
|
|
{
|
|
name: "empty resource owner for system token",
|
|
accessToken: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
|
|
return "", "", "", "", "", zerrors.ThrowUnauthenticated(nil, "TEST-tok", "unauthenticated")
|
|
}),
|
|
orgID: "system-org",
|
|
checkOrgActive: func(context.Context, string) error {
|
|
t.Fatal("CheckOrgActive must not be called for system tokens without resource owner")
|
|
return nil
|
|
},
|
|
wantOrgID: "system-org",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
verifier := &tokenVerifierMock{
|
|
AccessTokenVerifier: tt.accessToken,
|
|
SystemTokenVerifier: SystemTokenVerifierFunc(func(ctx context.Context, token, orgID string) (Memberships, string, error) {
|
|
if tt.name == "empty resource owner for system token" {
|
|
return Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}}, "system-user", nil
|
|
}
|
|
return systemTokenNOK.VerifySystemToken(ctx, token, orgID)
|
|
}),
|
|
checkOrgActive: tt.checkOrgActive,
|
|
}
|
|
got, err := VerifyTokenAndCreateCtxData(context.Background(), BearerPrefix+"token", tt.orgID, tt.orgDomain, verifier, nil)
|
|
if tt.wantErr != nil {
|
|
require.Error(t, err)
|
|
assert.True(t, tt.wantErr(err))
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantResource, got.ResourceOwner)
|
|
assert.Equal(t, tt.wantOrgID, got.OrgID)
|
|
})
|
|
}
|
|
}
|