mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 06:42:15 +00:00
feat(relational): implement session repository (#11109)
# Which Problems Are Solved As part of going to a relation storage model, we need to implement the repository for handling sessions and their events. # How the Problems Are Solved - Adds a migration step to create the necessary tables and types in the database. - Adds the repository implementation for sessions. - Implements a projection handler for session events to handle the events written by the old command handlers. - Updates the `Session` type and `SessionRepository` interface: - added `CreatorID` - Adjusted `Delete` method to return amount of affected columns - Removed sub-repository clumns from the main interface - Added `CreatorIDCondition`, `ExpirationCondition`, `ExistsFactor` and `ExistsMetadata` - Removed `SetUserAgent` - Methods of `SessionFactor` and `SessionChallenge` have been made public - fixed enumer on `SessionFactorType` - Adds a `JSON` type to the repository package to easy can JSON columns, resp. jsonb object inside a property of a `JSONArray` object. - Adds a `NoChange` interface, which can be implemented to signal that a `Change` interface implementation is not writing any changes to the statement builder, e.g. `CTEChange` # Additional Changes - Fixes the operators enums. `TextOperations` had wrong comments and `NumberOperations` additionally also had from comparisons. - Fixes downgrade of migration step 008. # Additional Context - closes https://github.com/zitadel/zitadel/issues/10212
This commit is contained in:
@@ -22,8 +22,9 @@ import (
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
@@ -51,9 +52,10 @@ var (
|
||||
SystemClient system.SystemServiceClient
|
||||
OrgClient v2beta_org.OrganizationServiceClient
|
||||
ProjectClient v2beta_project.ProjectServiceClient
|
||||
SessionClient session.SessionServiceClient
|
||||
UserClient user.UserServiceClient
|
||||
AdminClient admin.AdminServiceClient
|
||||
MgmtClient mgmt.ManagementServiceClient
|
||||
UserClient user_v2.UserServiceClient
|
||||
AuthorizationClient authorization.AuthorizationServiceClient
|
||||
)
|
||||
|
||||
@@ -71,9 +73,10 @@ func TestMain(m *testing.M) {
|
||||
SystemClient = integration.SystemClient()
|
||||
OrgClient = Instance.Client.OrgV2beta
|
||||
ProjectClient = Instance.Client.Projectv2Beta
|
||||
SessionClient = Instance.Client.SessionV2
|
||||
UserClient = Instance.Client.UserV2
|
||||
AdminClient = Instance.Client.Admin
|
||||
MgmtClient = Instance.Client.Mgmt
|
||||
UserClient = Instance.Client.UserV2
|
||||
AuthorizationClient = Instance.Client.AuthorizationV2
|
||||
|
||||
defer func() {
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
//go:build integration
|
||||
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
zdomain "github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func TestServer_SessionReduces(t *testing.T) {
|
||||
instanceID := Instance.ID()
|
||||
sessionRepo := repository.SessionRepository()
|
||||
|
||||
email := integration.Email()
|
||||
testUser := Instance.CreateUserTypeHuman(CTX, email)
|
||||
_, err := UserClient.VerifyEmail(CTX, &user.VerifyEmailRequest{
|
||||
UserId: testUser.GetId(),
|
||||
VerificationCode: testUser.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = UserClient.SetPassword(CTX, &user.SetPasswordRequest{
|
||||
UserId: testUser.GetId(),
|
||||
NewPassword: &user.Password{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
Verification: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
totpSecret := registerTOTP(CTX, t, testUser.GetId())
|
||||
Instance.RegisterUserPasskey(CTX, testUser.GetId())
|
||||
registerOTPEmail(CTX, t, testUser.GetId())
|
||||
_, err = UserClient.SetPhone(CTX, &user.SetPhoneRequest{
|
||||
UserId: testUser.GetId(),
|
||||
Phone: integration.Phone(),
|
||||
Verification: &user.SetPhoneRequest_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
registerOTPSMS(CTX, t, testUser.GetId())
|
||||
|
||||
idpID := Instance.AddGenericOAuthProvider(CTX, integration.IDPName()).GetId()
|
||||
idpUserID := integration.ID()
|
||||
_, err = UserClient.AddIDPLink(CTX, &user.AddIDPLinkRequest{
|
||||
UserId: testUser.GetId(),
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: idpUserID,
|
||||
UserName: integration.Username(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
recoveryCodes := createRecoveryCodes(CTX, t, testUser.GetId())
|
||||
|
||||
lifetime := time.Hour
|
||||
creatorID := "tester"
|
||||
userAgent := &domain.SessionUserAgent{
|
||||
FingerprintID: gu.Ptr(integration.ID()),
|
||||
Description: gu.Ptr("description"),
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Header: http.Header{
|
||||
"User-Agent": []string{"ZITADEL-Integration-Test"},
|
||||
},
|
||||
}
|
||||
createdSession, err := SessionClient.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: nil,
|
||||
Metadata: nil,
|
||||
Challenges: nil,
|
||||
UserAgent: &session.UserAgent{
|
||||
FingerprintId: userAgent.FingerprintID,
|
||||
Ip: gu.Ptr(userAgent.IP.String()),
|
||||
Description: userAgent.Description,
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"User-Agent": {Values: []string{"ZITADEL-Integration-Test"}},
|
||||
},
|
||||
},
|
||||
Lifetime: durationpb.New(lifetime),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second)
|
||||
var updatedSession *session.SetSessionResponse
|
||||
|
||||
t.Run("create session reduces", func(t *testing.T) {
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: nil,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
metadata := []*domain.SessionMetadata{
|
||||
{
|
||||
Metadata: domain.Metadata{
|
||||
InstanceID: instanceID,
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
SessionID: createdSession.GetSessionId(),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("metadata set reduces", func(t *testing.T) {
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: nil,
|
||||
Metadata: map[string][]byte{"key1": []byte("value1")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
var lifetimeUpdated time.Time
|
||||
|
||||
t.Run("lifetime set reduces", func(t *testing.T) {
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Lifetime: durationpb.New(lifetime),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
lifetimeUpdated = updatedSession.GetDetails().GetChangeDate().AsTime()
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: "",
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: nil,
|
||||
Challenges: nil,
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
var firstFactorCheckTime time.Time
|
||||
|
||||
t.Run("checks set reduces", func(t *testing.T) {
|
||||
_, err := url.Parse(Instance.CreateIntent(CTX, idpID).GetAuthUrl())
|
||||
require.NoError(t, err)
|
||||
expiry := time.Now().Add(1 * time.Hour)
|
||||
intentID, intentToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, testUser.GetId(), expiry)
|
||||
require.NoError(t, err)
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_LoginName{
|
||||
LoginName: email,
|
||||
},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: integration.UserPassword,
|
||||
},
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: intentToken,
|
||||
},
|
||||
Totp: &session.CheckTOTP{
|
||||
Code: func() string {
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
require.NoError(t, err)
|
||||
return code
|
||||
}(),
|
||||
},
|
||||
RecoveryCode: &session.CheckRecoveryCode{
|
||||
Code: recoveryCodes[0],
|
||||
},
|
||||
},
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Instance.Domain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
OtpEmail: &session.RequestChallenges_OTPEmail{
|
||||
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{
|
||||
ReturnCode: &session.RequestChallenges_OTPEmail_ReturnCode{},
|
||||
},
|
||||
},
|
||||
OtpSms: &session.RequestChallenges_OTPSMS{
|
||||
ReturnCode: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
firstFactorCheckTime = updatedSession.GetDetails().GetChangeDate().AsTime()
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPassword{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{
|
||||
&domain.SessionChallengePasskey{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Challenge: "challenge", // placeholder
|
||||
AllowedCredentialIDs: [][]byte{Instance.WebAuthN.KeyID()},
|
||||
UserVerification: zdomain.UserVerificationRequirementRequired,
|
||||
RPID: Instance.Domain,
|
||||
},
|
||||
&domain.SessionChallengeOTPEmail{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "",
|
||||
KeyID: "",
|
||||
Crypted: nil,
|
||||
},
|
||||
Expiry: 0,
|
||||
CodeReturned: true,
|
||||
URLTemplate: "",
|
||||
TriggeredAtOrigin: "",
|
||||
},
|
||||
&domain.SessionChallengeOTPSMS{
|
||||
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Code: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "",
|
||||
KeyID: "",
|
||||
Crypted: nil,
|
||||
},
|
||||
Expiry: 0,
|
||||
CodeReturned: true,
|
||||
GeneratorID: "",
|
||||
TriggeredAtOrigin: "",
|
||||
},
|
||||
},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("checks with challenges set reduces", func(t *testing.T) {
|
||||
assertionData, err := Instance.WebAuthN.CreateAssertionResponse(updatedSession.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
OtpEmail: &session.CheckOTP{
|
||||
Code: updatedSession.GetChallenges().GetOtpEmail(),
|
||||
},
|
||||
OtpSms: &session.CheckOTP{
|
||||
Code: updatedSession.GetChallenges().GetOtpSms(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPassword{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPasskey{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UserVerified: true,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorOTPEmail{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorOTPSMS{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("password changed reduces", func(t *testing.T) {
|
||||
passwordSet, err := UserClient.SetPassword(CTX, &user.SetPasswordRequest{
|
||||
UserId: testUser.GetId(),
|
||||
NewPassword: &user.Password{
|
||||
Password: integration.UserPassword + integration.RandString(5),
|
||||
},
|
||||
Verification: &user.SetPasswordRequest_CurrentPassword{
|
||||
CurrentPassword: integration.UserPassword,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assertSessionsEqual(collect, &domain.Session{
|
||||
InstanceID: instanceID,
|
||||
ID: createdSession.GetSessionId(),
|
||||
TokenID: createdSession.GetSessionToken(),
|
||||
Lifetime: lifetime,
|
||||
Expiration: lifetimeUpdated.Add(lifetime),
|
||||
UserID: testUser.GetId(),
|
||||
CreatorID: creatorID,
|
||||
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UpdatedAt: passwordSet.GetDetails().GetChangeDate().AsTime(),
|
||||
Factors: domain.SessionFactors{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: testUser.GetId(),
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
// password factor removed
|
||||
&domain.SessionFactorIdentityProviderIntent{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorPasskey{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
UserVerified: true,
|
||||
},
|
||||
&domain.SessionFactorTOTP{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
&domain.SessionFactorOTPEmail{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorOTPSMS{
|
||||
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
|
||||
},
|
||||
&domain.SessionFactorRecoveryCode{
|
||||
LastVerifiedAt: firstFactorCheckTime,
|
||||
},
|
||||
},
|
||||
Challenges: domain.SessionChallenges{},
|
||||
Metadata: metadata,
|
||||
UserAgent: userAgent,
|
||||
}, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("terminated reduces", func(t *testing.T) {
|
||||
_, err := SessionClient.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createdSession.GetSessionId(),
|
||||
SessionToken: gu.Ptr(updatedSession.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
|
||||
))
|
||||
require.ErrorIs(collect, err, new(database.NoRowFoundError))
|
||||
assert.Nil(collect, dbSession)
|
||||
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
}
|
||||
|
||||
func assertSessionsEqual(t *assert.CollectT, expected, actual *domain.Session) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.InstanceID, actual.InstanceID)
|
||||
assert.Equal(t, expected.ID, actual.ID)
|
||||
assert.NotNil(t, actual.TokenID)
|
||||
assert.Equal(t, expected.Lifetime, actual.Lifetime)
|
||||
assert.Equal(t, expected.Expiration.UTC(), actual.Expiration.UTC())
|
||||
assert.Equal(t, expected.UserID, actual.UserID)
|
||||
assert.Equal(t, expected.CreatorID, actual.CreatorID)
|
||||
assert.NotNil(t, actual.CreatedAt)
|
||||
assert.NotNil(t, actual.UpdatedAt)
|
||||
assertSessionFactorsEqual(t, expected.Factors, actual.Factors)
|
||||
assertSessionChallengesEqual(t, expected.Challenges, actual.Challenges)
|
||||
assertSessionMetadataEqual(t, expected.Metadata, actual.Metadata)
|
||||
assert.Equal(t, expected.UserAgent, actual.UserAgent)
|
||||
}
|
||||
|
||||
func assertSessionMetadataEqual(t *assert.CollectT, expected, actual []*domain.SessionMetadata) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.InstanceID == act.InstanceID &&
|
||||
exp.SessionID == act.SessionID &&
|
||||
exp.Key == act.Key &&
|
||||
bytes.Equal(exp.Value, act.Value) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected metadata not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSessionFactorsEqual(t *assert.CollectT, expected, actual domain.SessionFactors) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.SessionFactorType() == act.SessionFactorType() {
|
||||
switch expTyped := exp.(type) {
|
||||
case *domain.SessionFactorUser:
|
||||
actTyped := act.(*domain.SessionFactorUser)
|
||||
assert.Equal(t, expTyped.UserID, actTyped.UserID)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorPassword:
|
||||
actTyped := act.(*domain.SessionFactorPassword)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorIdentityProviderIntent:
|
||||
actTyped := act.(*domain.SessionFactorIdentityProviderIntent)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorPasskey:
|
||||
actTyped := act.(*domain.SessionFactorPasskey)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
assert.Equal(t, expTyped.UserVerified, actTyped.UserVerified)
|
||||
case *domain.SessionFactorTOTP:
|
||||
actTyped := act.(*domain.SessionFactorTOTP)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorOTPEmail:
|
||||
actTyped := act.(*domain.SessionFactorOTPEmail)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorOTPSMS:
|
||||
actTyped := act.(*domain.SessionFactorOTPSMS)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
case *domain.SessionFactorRecoveryCode:
|
||||
actTyped := act.(*domain.SessionFactorRecoveryCode)
|
||||
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected factor not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSessionChallengesEqual(t *assert.CollectT, expected, actual domain.SessionChallenges) {
|
||||
t.Helper()
|
||||
assert.Len(t, actual, len(expected))
|
||||
for _, exp := range expected {
|
||||
found := false
|
||||
for _, act := range actual {
|
||||
if exp.SessionChallengeType() == act.SessionChallengeType() {
|
||||
switch expTyped := exp.(type) {
|
||||
case *domain.SessionChallengePasskey:
|
||||
actTyped := act.(*domain.SessionChallengePasskey)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.NotEmpty(t, actTyped.Challenge)
|
||||
assert.Equal(t, expTyped.AllowedCredentialIDs, actTyped.AllowedCredentialIDs)
|
||||
assert.Equal(t, expTyped.RPID, actTyped.RPID)
|
||||
assert.Equal(t, expTyped.UserVerification, actTyped.UserVerification)
|
||||
case *domain.SessionChallengeOTPEmail:
|
||||
actTyped := act.(*domain.SessionChallengeOTPEmail)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
|
||||
case *domain.SessionChallengeOTPSMS:
|
||||
actTyped := act.(*domain.SessionChallengeOTPSMS)
|
||||
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
|
||||
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(t, found, "expected challenge not found: %+v", exp)
|
||||
}
|
||||
}
|
||||
|
||||
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||
resp, err := UserClient.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
secret = resp.GetSecret()
|
||||
code, err := totp.GenerateCode(secret, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = UserClient.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: userID,
|
||||
Code: code,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return secret
|
||||
}
|
||||
|
||||
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := UserClient.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := UserClient.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createRecoveryCodes(ctx context.Context, t *testing.T, userID string) (secret []string) {
|
||||
resp, err := UserClient.GenerateRecoveryCodes(ctx, &user.GenerateRecoveryCodesRequest{
|
||||
UserId: userID,
|
||||
Count: 5,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return resp.GetRecoveryCodes()
|
||||
}
|
||||
Reference in New Issue
Block a user