Files
zitadel/backend/v3/domain/session_check_idp_intent_test.go
T
9067605b66 Merge commit from fork
* fix(crypto): use authenticated encryption for IDP intent tokens

IDP intent tokens were encrypted with AES-CFB which provides no
integrity protection, making them malleable. Switch to A256GCM via
the existing AuthAlgorithm interface (EncryptToken/DecryptToken),
the same approach already used for OIDC and session tokens.

The idpConfigEncryption key type is widened from EncryptionAlgorithm
to AuthEncryptionAlgorithm so that data-at-rest encryption (IDP
client secrets, LDAP bind passwords, SAML keys) continues to use
the CFB path unmodified.

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

* fix(proto): raise IDP intent token max length to 500 for authenticated tokens

The switch to authenticated A256GCM (JWE compact serialization) makes IDP
intent tokens longer (~270 chars) than the previous AES-CFB base64 output.
Raise the max_len/max_length validation and OpenAPI constraints on every
idp_intent_token field from 200 to 500 so the new tokens are accepted:
user v2/v2beta (idp + user_service), session v2/v2beta, and resources
user v3alpha.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-09-04 06:13:37 +02:00

858 lines
35 KiB
Go

package domain_test
import (
"encoding/base64"
"errors"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/domain"
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestIDPIntentCheckCommand_Events(t *testing.T) {
t.Parallel()
tt := []struct {
name string
command *domain.IDPIntentCheckCommand
expectedEvents []eventstore.Command
}{
{
name: "nil CheckIntent returns nil events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: nil,
IsCheckComplete: true,
},
},
{
name: "isCheckComplete false returns nil events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{},
},
},
{
name: "valid command returns events",
command: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123"},
SessionID: "session-456",
InstanceID: "instance-789",
IsCheckComplete: true,
IntentLastVerified: time.Now(),
},
expectedEvents: []eventstore.Command{
session.NewIntentCheckedEvent(t.Context(), &session.NewAggregate("session-456", "instance-789").Aggregate, time.Now()),
idpintent.NewConsumedEvent(t.Context(), &idpintent.NewAggregate("intent-123", "").Aggregate),
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Test
events, err := tc.command.Events(t.Context(), &domain.InvokeOpts{})
// Verify
assert.NoError(t, err)
require.Len(t, events, len(tc.expectedEvents))
for i, expectedType := range tc.expectedEvents {
require.IsType(t, expectedType, events[i])
switch expectedType.(type) {
case *session.IntentCheckedEvent:
actualAssertedType, ok := events[i].(*session.IntentCheckedEvent)
require.True(t, ok)
assert.Equal(t, tc.command.IntentLastVerified, actualAssertedType.CheckedAt)
case *idpintent.ConsumedEvent:
_, ok := events[i].(*idpintent.ConsumedEvent)
require.True(t, ok)
}
}
})
}
}
func TestIDPIntentCheckCommand_Validate(t *testing.T) {
t.Parallel()
getErr := errors.New("get error")
notFoundErr := database.NewNoRowFoundError(nil)
decryptionError := errors.New("decryption error")
invalidTokenEncoding := base64.RawURLEncoding.EncodeToString([]byte("invalid-token"))
validTokenEncoding := base64.RawURLEncoding.EncodeToString([]byte("token-123"))
tt := []struct {
testName string
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
idpIntentRepo func(ctrl *gomock.Controller) domain.IDPIntentRepository
userRepo func(ctrl *gomock.Controller) domain.UserRepository
encryptionAlgorithm func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm
cmd *domain.IDPIntentCheckCommand
expectedError error
expectedUser domain.User
}{
{
testName: "when CheckIntent is nil should return no error",
cmd: &domain.IDPIntentCheckCommand{},
expectedError: nil,
},
{
testName: "when session ID is not set should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "",
InstanceID: "instance-1",
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-5Y8pb4", "Errors.Missing.SessionID"),
},
{
testName: "when instance ID is not set should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "",
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-Q4YFIq", "Errors.Missing.InstanceID"),
},
{
testName: "when retrieving session fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-EhIgey", "failed fetching session"),
},
{
testName: "when session not found should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-EhIgey", "session not found"),
},
{
testName: "when session userID is empty should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: ""}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-IJcVkV", "Errors.User.UserIDMissing"),
},
{
testName: "when token verification fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: invalidTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("", decryptionError)
return cryptoAlg
},
expectedError: zerrors.ThrowPermissionDenied(decryptionError, "CRYPTO-Sf4gt", "Errors.Intent.InvalidToken"),
},
{
testName: "when retrieving intent fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-5XkWJV", "failed fetching intent"),
},
{
testName: "when intent not found should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-5XkWJV", "intent not found"),
},
{
testName: "when intent state is not succeeded should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateFailed, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour))}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-0UKHku", "Errors.Intent.NotSucceeded"),
},
{
testName: "when intent is expired should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(-time.Hour))}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired"),
},
{
testName: "when intent expiration is nil should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: nil}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-kDR1XK", "Errors.Intent.Expired"),
},
{
testName: "when retrieving user fails should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(nil, getErr)
return repo
},
expectedError: zerrors.ThrowInternal(getErr, "DOM-Vnx2G9", "failed fetching user"),
},
{
testName: "when user is not found should return not found error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(nil, notFoundErr)
return repo
},
expectedError: zerrors.ThrowNotFound(notFoundErr, "DOM-Vnx2G9", "user not found"),
},
{
testName: "when user is not human should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: ""}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{ID: "user-1", State: domain.UserStateActive, Human: nil}, nil)
return repo
},
expectedError: zerrors.ThrowInternal(nil, "DOM-FkX5lZ", "user not human"),
},
{
testName: "when intent has user and it matches session user should return no error and set fetched user",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-123"}}},
}, nil)
return repo
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "user-1"}, nil)
return repo
},
expectedError: nil,
expectedUser: domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
},
},
{
testName: "when intent has user but it does not match session user should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-123"}}},
}, nil)
return repo
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "user-2"}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-FLdnLH", "Errors.Intent.OtherUser"),
},
{
testName: "when user has no matching IDP link should return error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "", IDPID: "idp-123"}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{IdentityProviderLinks: []*domain.IdentityProviderLink{{ProviderID: "idp-456"}}},
}, nil)
return repo
},
expectedError: zerrors.ThrowPreconditionFailed(nil, "DOM-XuNkt7", "Errors.Intent.OtherUser"),
},
{
testName: "when all validations pass should return no error",
cmd: &domain.IDPIntentCheckCommand{
CheckIntent: &domain.CheckIDPIntentType{ID: "intent-123", Token: validTokenEncoding},
SessionID: "session-1",
InstanceID: "instance-1",
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "session-1")))).
Return(&domain.Session{ID: "session-1", UserID: "user-1"}, nil)
return repo
},
encryptionAlgorithm: func(ctrl *gomock.Controller) crypto.AuthEncryptionAlgorithm {
cryptoAlg := crypto.NewMockAuthEncryptionAlgorithm(ctrl)
cryptoAlg.EXPECT().
DecryptToken(gomock.Any()).
Times(1).
Return("intent-123", nil)
return cryptoAlg
},
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "intent-123")))).
Return(&domain.IDPIntent{ID: "intent-123", State: domain.IDPIntentStateSucceeded, ExpiresAt: gu.Ptr(time.Now().Add(time.Hour)), UserID: "", IDPID: "idp-123"}, nil)
return repo
},
userRepo: func(ctrl *gomock.Controller) domain.UserRepository {
repo := domainmock.NewUserRepo(ctrl)
repo.EXPECT().
Get(gomock.Any(), gomock.Any(), dbmock.QueryOptions(database.WithCondition(repo.PrimaryKeyCondition("instance-1", "user-1")))).
Return(&domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
}, nil)
return repo
},
expectedError: nil,
expectedUser: domain.User{
ID: "user-1",
State: domain.UserStateActive,
Human: &domain.HumanUser{
IdentityProviderLinks: []*domain.IdentityProviderLink{
{ProviderID: "idp-123"},
},
},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.sessionRepo != nil {
domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts)
}
if tc.userRepo != nil {
domain.WithUserRepo(tc.userRepo(ctrl))(opts)
}
if tc.idpIntentRepo != nil {
domain.WithIDPIntentRepo(tc.idpIntentRepo(ctrl))(opts)
}
if tc.encryptionAlgorithm != nil {
tc.cmd.EncAlgo = tc.encryptionAlgorithm(ctrl)
}
// Test
err := tc.cmd.Validate(ctx, opts)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
assert.Equal(t, tc.expectedUser, tc.cmd.FetchedUser)
})
}
}
func TestIDPIntentCheckCommand_Execute(t *testing.T) {
t.Parallel()
deleteErr := errors.New("delete error")
updateErr := errors.New("update error")
tt := []struct {
testName string
idpIntentRepo func(ctrl *gomock.Controller) domain.IDPIntentRepository
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
cmd *domain.IDPIntentCheckCommand
expectedError error
expectComplete bool
}{
{
testName: "when CheckIntent is nil should return no error and not mark complete",
cmd: &domain.IDPIntentCheckCommand{},
expectedError: nil,
expectComplete: false,
},
{
testName: "when intent deletion fails should return error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(0), deleteErr)
return repo
},
expectedError: zerrors.ThrowInternal(deleteErr, "DOM-j1s5Eu", "failed deleting IDP intent"),
expectComplete: false,
},
{
testName: "when intent deletion returns unexpected row count should return error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(0), nil)
return repo
},
expectedError: zerrors.ThrowInternal(domain.NewRowsReturnedMismatchError(1, 0), "DOM-3CBpdB", "unexpected number of rows deleted"),
expectComplete: false,
},
{
testName: "when session update fails should return internal error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(1), nil)
return repo
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Update(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "session-1"), repo.SetFactor(&domain.SessionFactorIdentityProviderIntent{LastVerifiedAt: time.Now()})).
Times(1).
Return(int64(0), updateErr)
return repo
},
expectedError: zerrors.ThrowInternal(updateErr, "DOM-pec0al", "failed updating session"),
},
{
testName: "when session update returns no rows should return not found error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(1), nil)
return repo
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Update(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "session-1"), repo.SetFactor(&domain.SessionFactorIdentityProviderIntent{LastVerifiedAt: time.Now()})).
Times(1).
Return(int64(0), nil)
return repo
},
expectedError: zerrors.ThrowNotFound(nil, "DOM-CopO4e", "session not found"),
},
{
testName: "when session update returns no too many rows should return internal error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(1), nil)
return repo
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Update(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "session-1"), repo.SetFactor(&domain.SessionFactorIdentityProviderIntent{LastVerifiedAt: time.Now()})).
Times(1).
Return(int64(2), nil)
return repo
},
expectedError: zerrors.ThrowInternal(domain.NewMultipleObjectsUpdatedError(1, 2), "DOM-mlbibw", "unexpected number of rows updated"),
},
{
testName: "when session update returns 1 row updated should return no error",
cmd: domain.NewIDPIntentCheckCommand(&domain.CheckIDPIntentType{ID: "intent-123"}, "session-1", "instance-1", nil),
idpIntentRepo: func(ctrl *gomock.Controller) domain.IDPIntentRepository {
repo := domainmock.NewIDPIntentRepo(ctrl)
repo.EXPECT().
Delete(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "intent-123")).
Return(int64(1), nil)
return repo
},
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
repo := domainmock.NewSessionRepo(ctrl)
repo.EXPECT().
Update(gomock.Any(), gomock.Any(), repo.PrimaryKeyCondition("instance-1", "session-1"), repo.SetFactor(&domain.SessionFactorIdentityProviderIntent{LastVerifiedAt: time.Now()})).
Times(1).
Return(int64(1), nil)
return repo
},
expectComplete: true,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Given
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(tc.cmd.InstanceID, "", "")
opts := &domain.InvokeOpts{
Invoker: domain.NewTransactionInvoker(nil),
}
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
if tc.idpIntentRepo != nil {
domain.WithIDPIntentRepo(tc.idpIntentRepo(ctrl))(opts)
}
if tc.sessionRepo != nil {
domain.WithSessionRepo(tc.sessionRepo(ctrl))(opts)
}
// Test
err := tc.cmd.Execute(ctx, opts)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
assert.Equal(t, tc.expectComplete, tc.cmd.IsCheckComplete)
if tc.expectedError == nil && tc.expectComplete {
assert.NotZero(t, tc.cmd.IntentLastVerified)
}
})
}
}