mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13: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]>
192 lines
5.9 KiB
Go
192 lines
5.9 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/activity"
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/id"
|
|
"github.com/zitadel/zitadel/internal/repository/samlrequest"
|
|
"github.com/zitadel/zitadel/internal/repository/samlsession"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type SAMLSession struct {
|
|
SessionID string
|
|
SAMLResponseID string
|
|
EntityID string
|
|
UserID string
|
|
Audience []string
|
|
Expiration time.Time
|
|
AuthMethods []domain.UserAuthMethodType
|
|
AuthTime time.Time
|
|
PreferredLanguage *language.Tag
|
|
UserAgent *domain.UserAgent
|
|
}
|
|
|
|
type SAMLRequestComplianceChecker func(context.Context, *SAMLRequestWriteModel) error
|
|
|
|
func (c *Commands) CreateSAMLSessionFromSAMLRequest(ctx context.Context, samlReqId string, complianceCheck SAMLRequestComplianceChecker, samlResponseID string, samlResponseLifetime time.Duration) (err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
if samlReqId == "" {
|
|
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-0LxK6O31wH", "Errors.SAMLRequest.InvalidCode")
|
|
}
|
|
|
|
samlReqModel, err := c.getSAMLRequestWriteModel(ctx, samlReqId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
instanceID := authz.GetInstance(ctx).InstanceID()
|
|
sessionModel := NewSessionWriteModel(samlReqModel.SessionID, instanceID)
|
|
err = c.eventstore.FilterToQueryReducer(ctx, sessionModel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = sessionModel.CheckIsActive(); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd, err := c.newSAMLSessionAddEvents(ctx, sessionModel.UserID, sessionModel.UserResourceOwner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = complianceCheck(ctx, samlReqModel); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.AddSession(ctx,
|
|
sessionModel.UserID,
|
|
sessionModel.UserResourceOwner,
|
|
sessionModel.AggregateID,
|
|
samlReqModel.Issuer,
|
|
[]string{samlReqModel.Issuer},
|
|
samlReqModel.AuthMethods,
|
|
samlReqModel.AuthTime,
|
|
sessionModel.PreferredLanguage,
|
|
sessionModel.UserAgent,
|
|
)
|
|
|
|
if err = cmd.AddSAMLResponse(ctx, samlResponseID, samlResponseLifetime); err != nil {
|
|
return err
|
|
}
|
|
cmd.SetSAMLRequestSuccessful(ctx, samlReqModel.aggregate)
|
|
postCommit, err := cmd.SetMilestones(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = cmd.PushEvents(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
postCommit(ctx)
|
|
return err
|
|
}
|
|
|
|
func (c *Commands) newSAMLSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*SAMLSessionEvents, error) {
|
|
userStateModel, err := c.userStateForAuthentication(ctx, userID, resourceOwner, "SAML-1768ZQpmcP", "SAML-oR9nA")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sessionID, err := c.idGenerator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sessionID = IDPrefixV2 + sessionID
|
|
return &SAMLSessionEvents{
|
|
commands: c,
|
|
idGenerator: c.idGenerator,
|
|
encryptionAlg: c.keyAlgorithm,
|
|
events: pending,
|
|
samlSessionWriteModel: NewSAMLSessionWriteModel(sessionID, resourceOwner),
|
|
userStateModel: userStateModel,
|
|
}, nil
|
|
}
|
|
|
|
type SAMLSessionEvents struct {
|
|
commands *Commands
|
|
idGenerator id.Generator
|
|
encryptionAlg crypto.EncryptionAlgorithm
|
|
events []eventstore.Command
|
|
samlSessionWriteModel *SAMLSessionWriteModel
|
|
userStateModel *UserV2WriteModel
|
|
|
|
// samlResponseID is set by the command
|
|
samlResponseID string
|
|
}
|
|
|
|
func (c *SAMLSessionEvents) AddSession(
|
|
ctx context.Context,
|
|
userID,
|
|
userResourceOwner,
|
|
sessionID,
|
|
entityID string,
|
|
audience []string,
|
|
authMethods []domain.UserAuthMethodType,
|
|
authTime time.Time,
|
|
preferredLanguage *language.Tag,
|
|
userAgent *domain.UserAgent,
|
|
) {
|
|
c.events = append(c.events, samlsession.NewAddedEvent(
|
|
ctx,
|
|
c.samlSessionWriteModel.aggregate,
|
|
userID,
|
|
userResourceOwner,
|
|
sessionID,
|
|
entityID,
|
|
audience,
|
|
authMethods,
|
|
authTime,
|
|
preferredLanguage,
|
|
userAgent,
|
|
))
|
|
}
|
|
|
|
func (c *SAMLSessionEvents) SetSAMLRequestSuccessful(ctx context.Context, samlRequestAggregate *eventstore.Aggregate) {
|
|
c.events = append(c.events, samlrequest.NewSucceededEvent(ctx, samlRequestAggregate))
|
|
}
|
|
|
|
func (c *SAMLSessionEvents) SetSAMLRequestFailed(ctx context.Context, samlRequestAggregate *eventstore.Aggregate, err domain.SAMLErrorReason) {
|
|
c.events = append(c.events, samlrequest.NewFailedEvent(ctx, samlRequestAggregate, err))
|
|
}
|
|
|
|
func (c *SAMLSessionEvents) AddSAMLResponse(ctx context.Context, id string, lifetime time.Duration) error {
|
|
c.samlResponseID = id
|
|
c.events = append(c.events, samlsession.NewSAMLResponseAddedEvent(ctx, c.samlSessionWriteModel.aggregate, id, lifetime))
|
|
return nil
|
|
}
|
|
|
|
func (c *SAMLSessionEvents) PushEvents(ctx context.Context) (*SAMLSession, error) {
|
|
pushedEvents, err := c.commands.eventstore.Push(ctx, c.events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(c.samlSessionWriteModel, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
session := &SAMLSession{
|
|
SessionID: c.samlSessionWriteModel.SessionID,
|
|
EntityID: c.samlSessionWriteModel.EntityID,
|
|
UserID: c.samlSessionWriteModel.UserID,
|
|
Audience: c.samlSessionWriteModel.Audience,
|
|
Expiration: c.samlSessionWriteModel.SAMLResponseExpiration,
|
|
AuthMethods: c.samlSessionWriteModel.AuthMethods,
|
|
AuthTime: c.samlSessionWriteModel.AuthTime,
|
|
PreferredLanguage: c.samlSessionWriteModel.PreferredLanguage,
|
|
UserAgent: c.samlSessionWriteModel.UserAgent,
|
|
SAMLResponseID: c.samlSessionWriteModel.SAMLResponseID,
|
|
}
|
|
activity.Trigger(ctx, c.samlSessionWriteModel.UserResourceOwner, c.samlSessionWriteModel.UserID, activity.SAMLResponse, c.commands.eventstore.FilterToQueryReducer)
|
|
return session, nil
|
|
}
|