feat: session checks with intent (#6031)

* feat: session checks with intent

* feat: session checks with intent

* fix: integration tests for intent session

* fix: integration tests for intent session

* fix merge

* fix: integration tests for intent session

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2023-06-21 16:06:18 +02:00
committed by GitHub
parent c12d94f7d4
commit 1b5d6ce89e
29 changed files with 727 additions and 153 deletions

View File

@@ -119,7 +119,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
if err != nil {
return "", err
}
cmd, err := idpintent.NewSucceededEvent(
cmd := idpintent.NewSucceededEvent(
ctx,
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
idpInfo,
@@ -129,9 +129,6 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
accessToken,
idToken,
)
if err != nil {
return "", err
}
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
return "", err

View File

@@ -435,24 +435,21 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
eventstore: eventstoreExpect(t,
expectPush(
eventPusherToEvents(
func() eventstore.Command {
event, _ := idpintent.NewSucceededEvent(
context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate,
[]byte(`{"sub":"id","preferred_username":"username"}`),
"id",
"username",
"",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("accessToken"),
},
"idToken",
)
return event
}(),
idpintent.NewSucceededEvent(
context.Background(),
&idpintent.NewAggregate("id", "ro").Aggregate,
[]byte(`{"sub":"id","preferred_username":"username"}`),
"id",
"username",
"",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("accessToken"),
},
"idToken",
),
),
),
),

View File

@@ -23,8 +23,10 @@ type SessionCommands struct {
sessionWriteModel *SessionWriteModel
passwordWriteModel *HumanPasswordWriteModel
intentWriteModel *IDPIntentWriteModel
eventstore *eventstore.Eventstore
userPasswordAlg crypto.HashAlgorithm
intentAlg crypto.EncryptionAlgorithm
createToken func(sessionID string) (id string, token string, err error)
now func() time.Time
}
@@ -35,6 +37,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
sessionWriteModel: session,
eventstore: c.eventstore,
userPasswordAlg: c.userPasswordAlg,
intentAlg: c.idpConfigEncryption,
createToken: c.sessionTokenCreator,
now: time.Now,
}
@@ -80,6 +83,42 @@ func CheckPassword(password string) SessionCommand {
}
}
// CheckIntent defines a check for a succeeded intent to be executed for a session update
func CheckIntent(intentID, token string) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) error {
if cmd.sessionWriteModel.UserID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfw3r", "Errors.User.UserIDMissing")
}
if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
return err
}
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "")
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
if err != nil {
return err
}
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
}
if cmd.intentWriteModel.UserID != "" {
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
}
} else {
linkWriteModel := NewUserIDPLinkWriteModel(cmd.sessionWriteModel.UserID, cmd.intentWriteModel.IDPID, cmd.intentWriteModel.IDPUserID, cmd.intentWriteModel.ResourceOwner)
err := cmd.eventstore.FilterToQueryReducer(ctx, linkWriteModel)
if err != nil {
return err
}
if linkWriteModel.State != domain.UserIDPLinkStateActive {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
}
}
cmd.sessionWriteModel.IntentChecked(ctx, cmd.now())
return nil
}
}
// Exec will execute the commands specified and returns an error on the first occurrence
func (s *SessionCommands) Exec(ctx context.Context) error {
for _, cmd := range s.cmds {

View File

@@ -38,6 +38,7 @@ type SessionWriteModel struct {
UserID string
UserCheckedAt time.Time
PasswordCheckedAt time.Time
IntentCheckedAt time.Time
PasskeyCheckedAt time.Time
Metadata map[string][]byte
State domain.SessionState
@@ -68,6 +69,8 @@ func (wm *SessionWriteModel) Reduce() error {
wm.reduceUserChecked(e)
case *session.PasswordCheckedEvent:
wm.reducePasswordChecked(e)
case *session.IntentCheckedEvent:
wm.reduceIntentChecked(e)
case *session.PasskeyChallengedEvent:
wm.reducePasskeyChallenged(e)
case *session.PasskeyCheckedEvent:
@@ -90,6 +93,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
session.AddedType,
session.UserCheckedType,
session.PasswordCheckedType,
session.IntentCheckedType,
session.PasskeyChallengedType,
session.PasskeyCheckedType,
session.TokenSetType,
@@ -117,6 +121,10 @@ func (wm *SessionWriteModel) reducePasswordChecked(e *session.PasswordCheckedEve
wm.PasswordCheckedAt = e.CheckedAt
}
func (wm *SessionWriteModel) reduceIntentChecked(e *session.IntentCheckedEvent) {
wm.IntentCheckedAt = e.CheckedAt
}
func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) {
wm.PasskeyChallenge = &PasskeyChallengeModel{
Challenge: e.Challenge,
@@ -153,6 +161,10 @@ func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time
wm.commands = append(wm.commands, session.NewPasswordCheckedEvent(ctx, wm.aggregate, checkedAt))
}
func (wm *SessionWriteModel) IntentChecked(ctx context.Context, checkedAt time.Time) {
wm.commands = append(wm.commands, session.NewIntentCheckedEvent(ctx, wm.aggregate, checkedAt))
}
func (wm *SessionWriteModel) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) {
wm.commands = append(wm.commands, session.NewPasskeyChallengedEvent(ctx, wm.aggregate, challenge, allowedCrentialIDs, userVerification))
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -341,6 +342,19 @@ func TestCommands_UpdateSession(t *testing.T) {
}
func TestCommands_updateSession(t *testing.T) {
decryption := func(err error) crypto.EncryptionAlgorithm {
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
mCrypto.EXPECT().EncryptionKeyID().Return("id")
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn(
func(code []byte, keyID string) (string, error) {
if err != nil {
return "", err
}
return string(code), nil
})
return mCrypto
}
testNow := time.Now()
type fields struct {
eventstore *eventstore.Eventstore
@@ -484,6 +498,194 @@ func TestCommands_updateSession(t *testing.T) {
},
},
},
{
"set user, intent not successful",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
cmds: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
),
),
),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"),
},
},
{
"set user, intent not for user",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
cmds: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate,
nil,
"idpUserID",
"idpUserName",
"userID2",
nil,
"",
),
),
),
),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser"),
},
},
{
"set user, intent incorrect token",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
cmds: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent2", "aW50ZW50"),
},
eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
err: caos_errs.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"),
},
},
{
"set user, intent, metadata and token",
fields{
eventstore: eventstoreExpect(t,
expectPush(
eventPusherToEvents(
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"userID", testNow),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
testNow),
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
map[string][]byte{"key": []byte("value")}),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID"),
),
),
),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
cmds: []SessionCommand{
CheckUser("userID"),
CheckIntent("intent", "aW50ZW50"),
},
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"userID",
nil,
"",
),
),
),
),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
want: &SessionChanged{
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "org1",
},
ID: "sessionID",
NewToken: "token",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {