From 2953366b4c1796058e4d7a242522147fc27a281f Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 11 Nov 2025 11:57:33 +0100 Subject: [PATCH] feat(relational): define session repository (#11054) # Which Problems Are Solved As part moving sessions to the relational table, the repository to be able to manage session needs to be defined. This allows the business logic / service-layer to implement the necessary commands and the storage-layer to implement the underlying repository. # How the Problems Are Solved This PR defines the `SessionRepository` with all underlying object like the user agent, metadata, factors (including. challenges). # Additional Changes None # Additional Context - relates to #10212 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/v3/domain/session.go | 119 +++++++++++ backend/v3/domain/session_factor.go | 254 ++++++++++++++++++++++++ backend/v3/domain/session_metadata.go | 22 ++ backend/v3/domain/session_user_agent.go | 38 ++++ 4 files changed, 433 insertions(+) create mode 100644 backend/v3/domain/session.go create mode 100644 backend/v3/domain/session_factor.go create mode 100644 backend/v3/domain/session_metadata.go create mode 100644 backend/v3/domain/session_user_agent.go diff --git a/backend/v3/domain/session.go b/backend/v3/domain/session.go new file mode 100644 index 000000000..5dfb459a4 --- /dev/null +++ b/backend/v3/domain/session.go @@ -0,0 +1,119 @@ +package domain + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +type Session struct { + InstanceID string `json:"instance_id,omitempty" db:"instance_id"` + ID string `json:"id,omitempty" db:"id"` + Token string `json:"token,omitempty" db:"token"` + Lifetime time.Duration `json:"lifetime,omitempty" db:"lifetime"` + Expiration time.Time `json:"expiration,omitzero" db:"expiration"` + UserID string `json:"user_id,omitempty" db:"user_id"` + CreatedAt time.Time `json:"created_at,omitzero" db:"created_at"` + UpdatedAt time.Time `json:"updated_at,omitzero" db:"updated_at"` + Factors SessionFactors `json:"factors,omitempty"` + Challenges SessionChallenges `json:"challenges,omitempty"` + Metadata []SessionMetadata `json:"metadata,omitempty"` + UserAgent *SessionUserAgent `json:"user_agent,omitempty"` +} + +//go:generate mockgen -typed -package domainmock -destination ./mock/session.mock.go . SessionRepository + +type SessionRepository interface { + Repository + + sessionColumns + sessionConditions + sessionChanges + + // Get returns a session based on the given condition. + Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*Session, error) + // List returns a list of sessions based on the given condition. + List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*Session, error) + // Create creates a new session. + Create(ctx context.Context, client database.QueryExecutor, user *Session) error + // Update one or more existing sessions. + // The condition must include at least the instanceID of the session to update. + Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) + // Delete removes sessions based on the given condition. + Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) error +} + +// sessionColumns define all the columns of the session table. +type sessionColumns interface { + // InstanceIDColumn returns the column for the instance id field. + InstanceIDColumn() database.Column + // IDColumn returns the column for the id field. + IDColumn() database.Column + // TokenColumn returns the column for the token field. + TokenColumn() database.Column + // LifetimeColumn returns the column for the lifetime field. + LifetimeColumn() database.Column + // ExpirationColumn returns the column for the expiration field. + ExpirationColumn() database.Column + // UserIDColumn returns the column for the user id field. + UserIDColumn() database.Column + // CreatedAtColumn returns the column for the created at field. + CreatedAtColumn() database.Column + // UpdatedAtColumn returns the column for the updated at field. + UpdatedAtColumn() database.Column + + // FactorColumns returns the columns for the factors fields. + FactorColumns() sessionFactorColumns + // MetadataColumns returns the columns for the metadata fields. + MetadataColumns() sessionMetadataColumns + // UserAgentColumns returns the columns for the user agent fields. + UserAgentColumns() sessionUserAgentColumns +} + +// sessionConditions define all the conditions for the session table. +type sessionConditions interface { + // PrimaryKeyCondition returns a filter on the primary key fields. + PrimaryKeyCondition(instanceID, sessionID string) database.Condition + // InstanceIDCondition returns an equal filter on the instance id field. + InstanceIDCondition(instanceID string) database.Condition + // IDCondition returns an equal filter on the id field. + IDCondition(sessionID string) database.Condition + // UserAgentIDCondition returns an equal filter on the user agent ID field. + UserAgentIDCondition(userAgentID string) database.Condition + // UserIDCondition returns an equal filter on the user id field. + UserIDCondition(userID string) database.Condition + // CreatedAtCondition returns a filter on the created at field. + CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition + // UpdatedAtCondition returns a filter on the updated at field. + UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition + + // FactorConditions returns the conditions for the factors fields. + FactorConditions() sessionFactorConditions + // MetadataConditions returns the conditions for the metadata fields. + MetadataConditions() sessionMetadataConditions + // UserAgentConditions returns the conditions for the user agent fields. + UserAgentConditions() sessionUserAgentConditions +} + +type sessionChanges interface { + // SetUpdatedAt sets the updated at column. + // Only use this when reducing events, + // during regular updates the DB sets this column automatically. + SetUpdatedAt(updatedAt time.Time) database.Change + // SetToken sets the token field of the session. + SetToken(token string) database.Change + // SetLifetime sets the lifetime field of the session and will update the computed expiration field. + SetLifetime(lifetime time.Duration) database.Change + + // SetChallenge adds or updates the challenge of the corresponding type. + SetChallenge(challenge SessionChallenge) database.Change + // SetFactor adds or updates the factor of the corresponding type. + SetFactor(factor SessionFactor) database.Change + // ClearFactor resets the factor of the corresponding type. + ClearFactor(factor SessionFactorType) database.Change + // SetMetadata adds or updates the metadata of the session. + SetMetadata(metadata []SessionMetadata) database.Change + // SetUserAgent adds or updates the user agent of the session. + SetUserAgent(userAgent SessionUserAgent) database.Change +} diff --git a/backend/v3/domain/session_factor.go b/backend/v3/domain/session_factor.go new file mode 100644 index 000000000..76b408621 --- /dev/null +++ b/backend/v3/domain/session_factor.go @@ -0,0 +1,254 @@ +package domain + +import ( + "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" +) + +type SessionFactorType int + +const ( + SessionFactorTypeUnknown SessionFactorType = iota + SessionFactorTypeUser + SessionFactorTypePassword + SessionFactorTypePasskey // formerly known as WebAuthn + SessionFactorTypeIdentityProviderIntent + SessionFactorTypeTOTP + SessionFactorTypeOTPSMS + SessionFactorTypeOTPEmail +) + +type SessionFactor interface { + sessionFactorType() SessionFactorType +} + +type SessionFactorUser struct { + UserID string + LastVerifiedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorUser) sessionFactorType() SessionFactorType { + return SessionFactorTypeUser +} + +type SessionFactorPassword struct { + LastVerifiedAt time.Time + LastFailedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorPassword) sessionFactorType() SessionFactorType { + return SessionFactorTypePassword +} + +type SessionFactorIdentityProviderIntent struct { + LastVerifiedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorIdentityProviderIntent) sessionFactorType() SessionFactorType { + return SessionFactorTypeIdentityProviderIntent +} + +type SessionFactorPasskey struct { + LastVerifiedAt time.Time + UserVerified bool +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorPasskey) sessionFactorType() SessionFactorType { + return SessionFactorTypePasskey +} + +type SessionFactorTOTP struct { + LastVerifiedAt time.Time + LastFailedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorTOTP) sessionFactorType() SessionFactorType { + return SessionFactorTypeTOTP +} + +type SessionFactorOTPSMS struct { + LastVerifiedAt time.Time + LastFailedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorOTPSMS) sessionFactorType() SessionFactorType { + return SessionFactorTypeOTPSMS +} + +type SessionFactorOTPEmail struct { + LastVerifiedAt time.Time + LastFailedAt time.Time +} + +// sessionFactorType implements [SessionFactor]. +func (s *SessionFactorOTPEmail) sessionFactorType() SessionFactorType { + return SessionFactorTypeOTPEmail +} + +type SessionFactors []SessionFactor + +// GetUserFactor returns the user factor from the factors. +func (s SessionFactors) GetUserFactor() *SessionFactorUser { + factor, _ := GetFactor[*SessionFactorUser](s) + return factor +} + +// GetPasswordFactor returns the password factor from the factors. +func (s SessionFactors) GetPasswordFactor() *SessionFactorPassword { + factor, _ := GetFactor[*SessionFactorPassword](s) + return factor +} + +// GetIDPIntentFactor returns the IDP Intent factor from the factors. +func (s SessionFactors) GetIDPIntentFactor() *SessionFactorIdentityProviderIntent { + factor, _ := GetFactor[*SessionFactorIdentityProviderIntent](s) + return factor +} + +// GetPasskeyFactor returns the passkey factor from the factors. +func (s SessionFactors) GetPasskeyFactor() *SessionFactorPasskey { + factor, _ := GetFactor[*SessionFactorPasskey](s) + return factor +} + +// GetTOTPFactor returns the TOTP factor from the factors. +func (s SessionFactors) GetTOTPFactor() *SessionFactorTOTP { + factor, _ := GetFactor[*SessionFactorTOTP](s) + return factor +} + +// GetOTPSMSFactor returns the OTP SMS factor from the factors. +func (s SessionFactors) GetOTPSMSFactor() *SessionFactorOTPSMS { + factor, _ := GetFactor[*SessionFactorOTPSMS](s) + return factor +} + +// GetOTPEmailFactor returns the OTP Email factor from the factors. +func (s SessionFactors) GetOTPEmailFactor() *SessionFactorOTPEmail { + factor, _ := GetFactor[*SessionFactorOTPEmail](s) + return factor +} + +// GetFactor returns the factor of the given type from the factors. +func GetFactor[T SessionFactor](factors SessionFactors) (T, bool) { + var nilT T + for _, factor := range factors { + if f, ok := factor.(T); ok { + return f, true + } + } + return nilT, false +} + +type SessionChallenge interface { + sessionChallengeType() SessionFactorType +} + +type SessionChallengePasskey struct { + LastChallengedAt time.Time + Challenge string + AllowedCredentialIDs [][]byte + UserVerification domain.UserVerificationRequirement + RPID string +} + +// sessionChallengeType implements [SessionChallenge]. +func (s *SessionChallengePasskey) sessionChallengeType() SessionFactorType { + return SessionFactorTypePasskey +} + +type SessionChallengeOTPSMS struct { + LastChallengedAt time.Time + Code *crypto.CryptoValue + Expiry time.Duration + CodeReturned bool + GeneratorID string + TriggeredAtOrigin string +} + +// sessionChallengeType implements [SessionChallenge]. +func (s *SessionChallengeOTPSMS) sessionChallengeType() SessionFactorType { + return SessionFactorTypeOTPSMS +} + +type SessionChallengeOTPEmail struct { + LastChallengedAt time.Time + Code *crypto.CryptoValue + Expiry time.Duration + CodeReturned bool + URLTmpl string + TriggeredAtOrigin string +} + +// sessionChallengeType implements [SessionChallenge]. +func (s *SessionChallengeOTPEmail) sessionChallengeType() SessionFactorType { + return SessionFactorTypeOTPEmail +} + +type SessionChallenges []SessionChallenge + +// GetPasskeyChallenge returns the passkey challenge from the challenges. +func (s SessionChallenges) GetPasskeyChallenge() *SessionChallengePasskey { + challenge, _ := GetChallenge[*SessionChallengePasskey](s) + return challenge +} + +// GetOTPSMSChallenge returns the OTP SMS challenge from the challenges. +func (s SessionChallenges) GetOTPSMSChallenge() *SessionChallengeOTPSMS { + challenge, _ := GetChallenge[*SessionChallengeOTPSMS](s) + return challenge +} + +// GetOTPEmailChallenge returns the OTP Email challenge from the challenges. +func (s SessionChallenges) GetOTPEmailChallenge() *SessionChallengeOTPEmail { + challenge, _ := GetChallenge[*SessionChallengeOTPEmail](s) + return challenge +} + +// GetChallenge returns the challenge of the given type from the challenges. +func GetChallenge[T SessionChallenge](challenges SessionChallenges) (T, bool) { + var nilT T + for _, challenge := range challenges { + if c, ok := challenge.(T); ok { + return c, true + } + } + return nilT, false +} + +type sessionFactorColumns interface { + // InstanceIDColumn returns the column for the instance id field. + InstanceIDColumn() database.Column + // SessionIDColumn returns the column for the session id field. + SessionIDColumn() database.Column + // FactorTypeColumn returns the column for the factor type field. + FactorTypeColumn() database.Column + // LastChallengedAtColumn returns the column for the last challenged at field. + LastChallengedAtColumn() database.Column + // LastFailedAtColumn returns the column for the last failed at field. + LastFailedAtColumn() database.Column + // LastVerifiedAtColumn returns the column for the last verified at field. + LastVerifiedAtColumn() database.Column +} + +type sessionFactorConditions interface { + // PrimaryKeyCondition returns a filter on the primary key fields. + PrimaryKeyCondition(instanceID, sessionID string, factorType SessionFactorType) database.Condition + // InstanceIDCondition returns an equal filter on the instance id field. + InstanceIDCondition(instanceID string) database.Condition + // SessionIDCondition returns an equal filter on the session id field. + SessionIDCondition(sessionID string) database.Condition + // FactorTypeCondition returns an equal filter on the factor type field. + FactorTypeCondition(factorType SessionFactorType) database.Condition + // FactorLastVerifiedBeforeCondition returns a filter on the factor last verified field before the given time. + FactorLastVerifiedBeforeCondition(lastVerifiedAt time.Time) database.Condition +} diff --git a/backend/v3/domain/session_metadata.go b/backend/v3/domain/session_metadata.go new file mode 100644 index 000000000..107f37605 --- /dev/null +++ b/backend/v3/domain/session_metadata.go @@ -0,0 +1,22 @@ +package domain + +import "github.com/zitadel/zitadel/backend/v3/storage/database" + +type SessionMetadata struct { + Metadata + SessionID string `json:"sessionId,omitempty" db:"session_id"` +} + +type sessionMetadataColumns interface { + MetadataColumns + // SessionIDColumn returns the column for the session id field. + SessionIDColumn() database.Column +} + +type sessionMetadataConditions interface { + MetadataConditions + // PrimaryKeyCondition returns a filter on the primary key fields. + PrimaryKeyCondition(instanceID, sessionID, key string) database.Condition + // SessionIDCondition returns an equal filter on the session id field. + SessionIDCondition(sessionID string) database.Condition +} diff --git a/backend/v3/domain/session_user_agent.go b/backend/v3/domain/session_user_agent.go new file mode 100644 index 000000000..7216438bf --- /dev/null +++ b/backend/v3/domain/session_user_agent.go @@ -0,0 +1,38 @@ +package domain + +import ( + "net" + "net/http" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +type SessionUserAgent struct { + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + FingerprintID *string `json:"fingerprintId,omitempty" db:"fingerprint_id"` + Description *string `json:"description,omitempty" db:"description"` + IP net.IP `json:"ip,omitempty" db:"ip"` + Header http.Header `json:"header,omitempty" db:"headers"` +} + +type sessionUserAgentColumns interface { + // InstanceIDColumn returns the column for the instance id field. + InstanceIDColumn() database.Column + // FingerprintIDColumn returns the column for the fingerprint id field. + FingerprintIDColumn() database.Column + // IPColumn returns the column for the ip field. + IPColumn() database.Column + // DescriptionColumn returns the column for the description field. + DescriptionColumn() database.Column + // HeadersColumn returns the column for the headers field. + HeadersColumn() database.Column +} + +type sessionUserAgentConditions interface { + // PrimaryKeyCondition returns a filter on the primary key fields. + PrimaryKeyCondition(instanceID, fingerprintID string) database.Condition + // InstanceIDCondition returns an equal filter on the instance id field. + InstanceIDCondition(instanceID string) database.Condition + // FingerprintIDCondition returns an equal filter on the fingerprint id field. + FingerprintIDCondition(fingerprintID string) database.Condition +}