mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-04 22:13:36 +00:00
update base
This commit is contained in:
@@ -119,17 +119,16 @@ 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,
|
||||
idpUser.GetID(),
|
||||
idpUser.GetPreferredUsername(),
|
||||
userID,
|
||||
accessToken,
|
||||
idToken,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -16,6 +16,8 @@ type IDPIntentWriteModel struct {
|
||||
FailureURL *url.URL
|
||||
IDPID string
|
||||
IDPUser []byte
|
||||
IDPUserID string
|
||||
IDPUserName string
|
||||
IDPAccessToken *crypto.CryptoValue
|
||||
IDPIDToken string
|
||||
UserID string
|
||||
@@ -72,6 +74,8 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
|
||||
func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) {
|
||||
wm.UserID = e.UserID
|
||||
wm.IDPUser = e.IDPUser
|
||||
wm.IDPUserID = e.IDPUserID
|
||||
wm.IDPUserName = e.IDPUserName
|
||||
wm.IDPAccessToken = e.IDPAccessToken
|
||||
wm.IDPIDToken = e.IDPIDToken
|
||||
wm.State = domain.IDPIntentStateSucceeded
|
||||
|
||||
@@ -435,22 +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(`{"RawInfo":{"id":"id"}}`),
|
||||
"",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
"",
|
||||
)
|
||||
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",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -458,18 +457,20 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
||||
idpSession: &oauth.Session{
|
||||
idpSession: &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
},
|
||||
idpUser: &oauth.UserMapper{
|
||||
RawInfo: map[string]interface{}{
|
||||
"id": "id",
|
||||
idpUser: openid.NewUser(&oidc.UserInfo{
|
||||
Subject: "id",
|
||||
UserInfoProfile: oidc.UserInfoProfile{
|
||||
PreferredUsername: "username",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
res{
|
||||
token: "aWQ",
|
||||
|
||||
@@ -46,6 +46,12 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
||||
return es
|
||||
}
|
||||
|
||||
func expectEventstore(expects ...expect) func(*testing.T) *eventstore.Eventstore {
|
||||
return func(t *testing.T) *eventstore.Eventstore {
|
||||
return eventstoreExpect(t, expects...)
|
||||
}
|
||||
}
|
||||
|
||||
func eventPusherToEvents(eventsPushes ...eventstore.Command) []*repository.Event {
|
||||
events := make([]*repository.Event, len(eventsPushes))
|
||||
for i, event := range eventsPushes {
|
||||
@@ -125,6 +131,18 @@ func expectPushFailed(err error, events []*repository.Event, uniqueConstraints .
|
||||
}
|
||||
}
|
||||
|
||||
func expectRandomPush(events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect {
|
||||
return func(m *mock.MockRepository) {
|
||||
m.ExpectRandomPush(events, uniqueConstraints...)
|
||||
}
|
||||
}
|
||||
|
||||
func expectRandomPushFailed(err error, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect {
|
||||
return func(m *mock.MockRepository) {
|
||||
m.ExpectRandomPushFailed(err, events, uniqueConstraints...)
|
||||
}
|
||||
}
|
||||
|
||||
func expectFilter(events ...*repository.Event) expect {
|
||||
return func(m *mock.MockRepository) {
|
||||
m.ExpectFilterEvents(events...)
|
||||
|
||||
@@ -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 {
|
||||
@@ -118,7 +157,7 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co
|
||||
return token, s.sessionWriteModel.commands, nil
|
||||
}
|
||||
|
||||
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, sessionDomain string, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
sessionID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,8 +167,8 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionWriteModel.Start(ctx, sessionDomain)
|
||||
cmd := c.NewSessionCommands(cmds, sessionWriteModel)
|
||||
cmd.sessionWriteModel.Start(ctx)
|
||||
return c.updateSession(ctx, cmd, metadata)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ type PasskeyChallengeModel struct {
|
||||
Challenge string
|
||||
AllowedCrentialIDs [][]byte
|
||||
UserVerification domain.UserVerificationRequirement
|
||||
RPID string
|
||||
}
|
||||
|
||||
func (p *PasskeyChallengeModel) WebAuthNLogin(human *domain.Human, credentialAssertionData []byte) (*domain.WebAuthNLogin, error) {
|
||||
@@ -28,6 +29,7 @@ func (p *PasskeyChallengeModel) WebAuthNLogin(human *domain.Human, credentialAss
|
||||
Challenge: p.Challenge,
|
||||
AllowedCredentialIDs: p.AllowedCrentialIDs,
|
||||
UserVerification: p.UserVerification,
|
||||
RPID: p.RPID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -38,8 +40,10 @@ type SessionWriteModel struct {
|
||||
UserID string
|
||||
UserCheckedAt time.Time
|
||||
PasswordCheckedAt time.Time
|
||||
IntentCheckedAt time.Time
|
||||
PasskeyCheckedAt time.Time
|
||||
Metadata map[string][]byte
|
||||
Domain string
|
||||
State domain.SessionState
|
||||
|
||||
PasskeyChallenge *PasskeyChallengeModel
|
||||
@@ -68,6 +72,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 +96,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
session.AddedType,
|
||||
session.UserCheckedType,
|
||||
session.PasswordCheckedType,
|
||||
session.IntentCheckedType,
|
||||
session.PasskeyChallengedType,
|
||||
session.PasskeyCheckedType,
|
||||
session.TokenSetType,
|
||||
@@ -105,6 +112,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceAdded(e *session.AddedEvent) {
|
||||
wm.Domain = e.Domain
|
||||
wm.State = domain.SessionStateActive
|
||||
}
|
||||
|
||||
@@ -117,11 +125,16 @@ 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,
|
||||
AllowedCrentialIDs: e.AllowedCrentialIDs,
|
||||
UserVerification: e.UserVerification,
|
||||
RPID: wm.Domain,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +151,10 @@ func (wm *SessionWriteModel) reduceTerminate() {
|
||||
wm.State = domain.SessionStateTerminated
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) Start(ctx context.Context) {
|
||||
wm.commands = append(wm.commands, session.NewAddedEvent(ctx, wm.aggregate))
|
||||
func (wm *SessionWriteModel) Start(ctx context.Context, domain string) {
|
||||
wm.commands = append(wm.commands, session.NewAddedEvent(ctx, wm.aggregate, domain))
|
||||
// set the domain so checks can use it
|
||||
wm.Domain = domain
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
|
||||
@@ -153,6 +168,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))
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (c *Commands) CreatePasskeyChallenge(userVerification domain.UserVerificati
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, humanPasskeys.tokens...)
|
||||
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, cmd.sessionWriteModel.Domain, humanPasskeys.tokens...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestSessionCommands_getHumanPasskeys(t *testing.T) {
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
|
||||
context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
|
||||
), "111", "challenge"),
|
||||
), "111", "challenge", "rpID"),
|
||||
)),
|
||||
),
|
||||
sessionWriteModel: &SessionWriteModel{
|
||||
@@ -112,6 +112,7 @@ func TestSessionCommands_getHumanPasskeys(t *testing.T) {
|
||||
WebAuthNTokenID: "111",
|
||||
State: domain.MFAStateNotReady,
|
||||
Challenge: "challenge",
|
||||
RPID: "rpID",
|
||||
}},
|
||||
},
|
||||
err: nil,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -146,6 +147,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
checks []SessionCommand
|
||||
domain string
|
||||
metadata map[string][]byte
|
||||
}
|
||||
type res struct {
|
||||
@@ -193,7 +195,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
@@ -217,6 +219,39 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"empty session with domain",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenCreator: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
},
|
||||
args{
|
||||
ctx: authz.NewMockContext("", "org1", ""),
|
||||
domain: "domain.tld",
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
|
||||
ID: "sessionID",
|
||||
NewToken: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
// the rest is tested in the Test_updateSession
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -226,7 +261,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
sessionTokenCreator: tt.fields.tokenCreator,
|
||||
}
|
||||
got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata)
|
||||
got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.domain, tt.args.metadata)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
})
|
||||
@@ -275,7 +310,7 @@ func TestCommands_UpdateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
@@ -300,7 +335,7 @@ func TestCommands_UpdateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
@@ -341,6 +376,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 +532,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) {
|
||||
@@ -537,7 +773,7 @@ func TestCommands_TerminateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
@@ -562,7 +798,7 @@ func TestCommands_TerminateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID")),
|
||||
@@ -591,7 +827,7 @@ func TestCommands_TerminateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
@@ -622,7 +858,7 @@ func TestCommands_TerminateSession(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
|
||||
eventFromEventPusher(
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
|
||||
@@ -140,6 +140,7 @@ func writeModelToWebAuthN(wm *HumanWebAuthNWriteModel) *domain.WebAuthNToken {
|
||||
SignCount: wm.SignCount,
|
||||
WebAuthNTokenName: wm.WebAuthNTokenName,
|
||||
State: wm.State,
|
||||
RPID: wm.RPID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ package command
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -43,7 +45,32 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
|
||||
}
|
||||
human, err := c.getHuman(ctx, userID, resourceowner)
|
||||
prep, err := c.createHumanTOTP(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, prep.cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.OTP{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: prep.userAgg.ID,
|
||||
},
|
||||
SecretString: prep.key.Secret(),
|
||||
Url: prep.key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type preparedTOTP struct {
|
||||
wm *HumanOTPWriteModel
|
||||
userAgg *eventstore.Aggregate
|
||||
key *otp.Key
|
||||
cmds []eventstore.Command
|
||||
}
|
||||
|
||||
func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner string) (*preparedTOTP, error) {
|
||||
human, err := c.getHuman(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound")
|
||||
@@ -59,7 +86,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
|
||||
}
|
||||
|
||||
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner)
|
||||
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,16 +107,13 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.OTP{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: human.AggregateID,
|
||||
return &preparedTOTP{
|
||||
wm: otpWriteModel,
|
||||
userAgg: userAgg,
|
||||
key: key,
|
||||
cmds: []eventstore.Command{
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
},
|
||||
SecretString: key.Secret(),
|
||||
Url: key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,17 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
@@ -231,6 +237,476 @@ func TestCommandSide_AddHumanOTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_createHumanOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "user not existing, not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-MM9fs", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "org not existing, not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-55M9f", "Errors.Org.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "org iam policy not existing, not found error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&user.NewAggregate("org1", "org1").Aggregate,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "otp already exists, already exists error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&user.NewAggregate("org1", "org1").Aggregate,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("a"),
|
||||
}),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPVerifiedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"agent1")),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady"),
|
||||
},
|
||||
{
|
||||
name: "issuer not in context",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&user.NewAggregate("org1", "org1").Aggregate,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInternal(nil, "TOTP-ieY3o", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&user.NewAggregate("org1", "org1").Aggregate,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithRequestedDomain(context.Background(), "zitadel.com"),
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
multifactors: domain.MultifactorConfigs{
|
||||
OTP: domain.OTPConfig{
|
||||
CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := c.createHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want {
|
||||
require.NotNil(t, got)
|
||||
assert.NotNil(t, got.wm)
|
||||
assert.NotNil(t, got.userAgg)
|
||||
require.NotNil(t, got.key)
|
||||
assert.NotEmpty(t, got.key.URL())
|
||||
assert.NotEmpty(t, got.key.Secret())
|
||||
assert.Len(t, got.cmds, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_HumanCheckMFAOTPSetup(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
|
||||
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
code, err := totp.GenerateCode(key.Secret(), time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
code string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"),
|
||||
},
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "otp not existing error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPRemovedEvent(ctx, userAgg),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotExisting"),
|
||||
},
|
||||
{
|
||||
name: "otp already ready error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPVerifiedEvent(context.Background(),
|
||||
userAgg,
|
||||
"agent1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady"),
|
||||
},
|
||||
{
|
||||
name: "wrong code",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
code: "wrong",
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
[]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanOTPVerifiedEvent(ctx,
|
||||
userAgg,
|
||||
"agent1",
|
||||
),
|
||||
)},
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
code: code,
|
||||
userID: "user1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
),
|
||||
expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanOTPVerifiedEvent(ctx,
|
||||
userAgg,
|
||||
"agent1",
|
||||
),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
code: code,
|
||||
userID: "user1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
multifactors: domain.MultifactorConfigs{
|
||||
OTP: domain.OTPConfig{
|
||||
CryptoMFA: cryptoAlg,
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := c.HumanCheckMFAOTPSetup(ctx, tt.args.userID, tt.args.code, "agent1", tt.args.resourceOwner)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "org1", got.ResourceOwner)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_RemoveHumanOTP(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
|
||||
@@ -26,6 +26,9 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordStrin
|
||||
if !existingPassword.UserState.Exists() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0fs", "Errors.User.NotFound")
|
||||
}
|
||||
if err = c.checkPermission(ctx, domain.PermissionUserWrite, existingPassword.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password := &domain.Password{
|
||||
SecretString: passwordString,
|
||||
ChangeRequired: oneTime,
|
||||
@@ -46,28 +49,28 @@ func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordStrin
|
||||
return writeModelToObjectDetails(&existingPassword.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string, passwordVerificationCode crypto.Generator) (err error) {
|
||||
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing")
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing")
|
||||
}
|
||||
if passwordString == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Mf0sd", "Errors.User.Password.Empty")
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Mf0sd", "Errors.User.Password.Empty")
|
||||
}
|
||||
existingCode, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
|
||||
}
|
||||
|
||||
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, passwordVerificationCode)
|
||||
err = crypto.VerifyCodeWithAlgorithm(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.userEncryption)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password := &domain.Password{
|
||||
@@ -77,10 +80,13 @@ func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID,
|
||||
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
|
||||
passwordEvent, err := c.changePassword(ctx, userAgentID, password, userAgg, existingCode)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, passwordEvent)
|
||||
return err
|
||||
err = c.pushAppendAndReduce(ctx, existingCode, passwordEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,6 +23,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -72,6 +74,49 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing permission, error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
oneTime: true,
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change password onetime, ok",
|
||||
fields: fields{
|
||||
@@ -129,6 +174,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -200,6 +246,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -220,6 +267,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
}
|
||||
got, err := r.SetPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.oneTime)
|
||||
if tt.res.err == nil {
|
||||
@@ -235,19 +283,19 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_SetPassword(t *testing.T) {
|
||||
func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
resourceOwner string
|
||||
password string
|
||||
agentID string
|
||||
secretGenerator crypto.Generator
|
||||
ctx context.Context
|
||||
userID string
|
||||
code string
|
||||
resourceOwner string
|
||||
password string
|
||||
agentID string
|
||||
}
|
||||
type res struct {
|
||||
want *domain.ObjectDetails
|
||||
@@ -377,14 +425,14 @@ func TestCommandSide_SetPassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
code: "test",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
code: "test",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
@@ -460,14 +508,14 @@ func TestCommandSide_SetPassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
code: "a",
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
code: "a",
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
@@ -481,14 +529,18 @@ func TestCommandSide_SetPassword(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
}
|
||||
err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID, tt.args.secretGenerator)
|
||||
got, err := r.SetPasswordWithVerifyCode(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.code, tt.args.password, tt.args.agentID)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.res.err != nil && !tt.res.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
if tt.res.err == nil {
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ func (c *Commands) getHumanU2FTokens(ctx context.Context, userID, resourceowner
|
||||
return readModelToU2FTokens(tokenReadModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getHumanPasswordlessTokens(ctx context.Context, userID, resourceowner string) ([]*domain.WebAuthNToken, error) {
|
||||
tokenReadModel := NewHumanPasswordlessTokensReadModel(userID, resourceowner)
|
||||
func (c *Commands) getHumanPasswordlessTokens(ctx context.Context, userID, resourceOwner string) ([]*domain.WebAuthNToken, error) {
|
||||
tokenReadModel := NewHumanPasswordlessTokensReadModel(userID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,12 +82,12 @@ func (c *Commands) HumanAddU2FSetup(ctx context.Context, userID, resourceowner s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addWebAuthN, userAgg, webAuthN, err := c.addHumanWebAuthN(ctx, userID, resourceowner, isLoginUI, u2fTokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementDiscouraged)
|
||||
addWebAuthN, userAgg, webAuthN, err := c.addHumanWebAuthN(ctx, userID, resourceowner, "", u2fTokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementDiscouraged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events, err := c.eventstore.Push(ctx, usr_repo.NewHumanU2FAddedEvent(ctx, userAgg, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
|
||||
events, err := c.eventstore.Push(ctx, usr_repo.NewHumanU2FAddedEvent(ctx, userAgg, addWebAuthN.WebauthNTokenID, webAuthN.Challenge, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,12 +108,12 @@ func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resour
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addWebAuthN, userAgg, webAuthN, err := c.addHumanWebAuthN(ctx, userID, resourceowner, isLoginUI, passwordlessTokens, authenticatorPlatform, domain.UserVerificationRequirementRequired)
|
||||
addWebAuthN, userAgg, webAuthN, err := c.addHumanWebAuthN(ctx, userID, resourceowner, "", passwordlessTokens, authenticatorPlatform, domain.UserVerificationRequirementRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events, err := c.eventstore.Push(ctx, usr_repo.NewHumanPasswordlessAddedEvent(ctx, userAgg, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
|
||||
events, err := c.eventstore.Push(ctx, usr_repo.NewHumanPasswordlessAddedEvent(ctx, userAgg, addWebAuthN.WebauthNTokenID, webAuthN.Challenge, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID
|
||||
return c.HumanAddPasswordlessSetup(ctx, userID, resourceowner, true, preferredPlatformType)
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner string, isLoginUI bool, tokens []*domain.WebAuthNToken, authenticatorPlatform domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner, rpID string, tokens []*domain.WebAuthNToken, authenticatorPlatform domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
if userID == "" {
|
||||
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner s
|
||||
if accountName == "" {
|
||||
accountName = string(user.EmailAddress)
|
||||
}
|
||||
webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, userVerification, isLoginUI, tokens...)
|
||||
webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, userVerification, rpID, tokens...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -343,7 +343,7 @@ func (c *Commands) beginWebAuthNLogin(ctx context.Context, userID, resourceOwner
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, human, userVerification, tokens...)
|
||||
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, human, userVerification, "", tokens...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type HumanWebAuthNWriteModel struct {
|
||||
AAGUID []byte
|
||||
SignCount uint32
|
||||
WebAuthNTokenName string
|
||||
RPID string
|
||||
|
||||
State domain.MFAState
|
||||
}
|
||||
@@ -113,6 +114,7 @@ func (wm *HumanWebAuthNWriteModel) Reduce() error {
|
||||
func (wm *HumanWebAuthNWriteModel) appendAddedEvent(e *user.HumanWebAuthNAddedEvent) {
|
||||
wm.WebauthNTokenID = e.WebAuthNTokenID
|
||||
wm.Challenge = e.Challenge
|
||||
wm.RPID = e.RPID
|
||||
wm.State = domain.MFAStateNotReady
|
||||
}
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@ func (wm *UserIDPLinkWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *user.UserIDPLinkAddedEvent:
|
||||
if e.IDPConfigID != wm.IDPConfigID && e.ExternalUserID != wm.ExternalUserID {
|
||||
if e.IDPConfigID != wm.IDPConfigID || e.ExternalUserID != wm.ExternalUserID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *user.UserIDPLinkRemovedEvent:
|
||||
if e.IDPConfigID != wm.IDPConfigID && e.ExternalUserID != wm.ExternalUserID {
|
||||
if e.IDPConfigID != wm.IDPConfigID || e.ExternalUserID != wm.ExternalUserID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
case *user.UserIDPLinkCascadeRemovedEvent:
|
||||
if e.IDPConfigID != wm.IDPConfigID && e.ExternalUserID != wm.ExternalUserID {
|
||||
if e.IDPConfigID != wm.IDPConfigID || e.ExternalUserID != wm.ExternalUserID {
|
||||
continue
|
||||
}
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
|
||||
@@ -15,23 +15,23 @@ import (
|
||||
)
|
||||
|
||||
// RegisterUserPasskey creates a passkey registration for the current authenticated user.
|
||||
// UserID, ussualy taken from the request is compaired against the user ID in the context.
|
||||
func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
// UserID, usually taken from the request is compared against the user ID in the context.
|
||||
func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner, rpID string, authenticator domain.AuthenticatorAttachment) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, authenticator)
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, rpID, authenticator)
|
||||
}
|
||||
|
||||
// RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id.
|
||||
// The resource is protected by the code, identified by the codeID.
|
||||
func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code, rpID string, alg crypto.EncryptionAlgorithm) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, authenticator, event)
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, rpID, authenticator, event)
|
||||
}
|
||||
|
||||
type eventCallback func(context.Context, *eventstore.Aggregate) eventstore.Command
|
||||
@@ -40,7 +40,7 @@ type eventCallback func(context.Context, *eventstore.Aggregate) eventstore.Comma
|
||||
// A code can only be used once.
|
||||
// Upon success an event callback is returned, which must be called after
|
||||
// all other events for the current request are created.
|
||||
// This prevent consuming a code when another error occurred after verification.
|
||||
// This prevents consuming a code when another error occurred after verification.
|
||||
func (c *Commands) verifyUserPasskeyCode(ctx context.Context, userID, resourceOwner, codeID, code string, alg crypto.EncryptionAlgorithm) (eventCallback, error) {
|
||||
wm := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
@@ -63,25 +63,25 @@ func (c *Commands) verifyUserPasskeyCodeFailed(ctx context.Context, wm *HumanPas
|
||||
logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed")
|
||||
}
|
||||
|
||||
func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator)
|
||||
func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner, rpID string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, rpID, authenticator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.pushUserPasskey(ctx, wm, userAgg, webAuthN, events...)
|
||||
}
|
||||
|
||||
func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner, rpID string, authenticator domain.AuthenticatorAttachment) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
passwordlessTokens, err := c.getHumanPasswordlessTokens(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired)
|
||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, rpID, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired)
|
||||
}
|
||||
|
||||
func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
cmds := make([]eventstore.Command, len(events)+1)
|
||||
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge, webAuthN.RPID)
|
||||
for i, event := range events {
|
||||
cmds[i+1] = event(ctx, userAgg)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (c *Commands) AddUserPasskeyCodeURLTemplate(ctx context.Context, userID, re
|
||||
}
|
||||
|
||||
// AddUserPasskeyCodeReturn generates and returns a Passkey code.
|
||||
// No email will be send to the user.
|
||||
// No email will be sent to the user.
|
||||
func (c *Commands) AddUserPasskeyCodeReturn(ctx context.Context, userID, resourceOwner string, alg crypto.EncryptionAlgorithm) (*domain.PasskeyCodeDetails, error) {
|
||||
return c.addUserPasskeyCode(ctx, userID, resourceOwner, alg, "", true)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
rpID string
|
||||
authenticator domain.AuthenticatorAttachment
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -121,7 +122,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
_, err := c.RegisterUserPasskey(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator)
|
||||
_, err := c.RegisterUserPasskey(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.rpID, tt.args.authenticator)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful case can't be tested due to random challenge.
|
||||
})
|
||||
@@ -148,6 +149,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
rpID string
|
||||
authenticator domain.AuthenticatorAttachment
|
||||
codeID string
|
||||
code string
|
||||
@@ -222,7 +224,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
_, err := c.RegisterUserPasskeyWithCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator, tt.args.codeID, tt.args.code, alg)
|
||||
_, err := c.RegisterUserPasskeyWithCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator, tt.args.codeID, tt.args.code, tt.args.rpID, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful case can't be tested due to random challenge.
|
||||
})
|
||||
@@ -376,7 +378,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
|
||||
ctx, &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
|
||||
), "111", "challenge"),
|
||||
), "111", "challenge", "rpID"),
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -394,7 +396,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
userAgg, "123", challenge, "rpID",
|
||||
),
|
||||
)})
|
||||
},
|
||||
@@ -406,7 +408,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
userAgg, "123", challenge, "rpID",
|
||||
),
|
||||
)})
|
||||
},
|
||||
@@ -418,7 +420,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
return expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
userAgg, "123", challenge, "rpID",
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
@@ -440,7 +442,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
webauthnConfig: webauthnConfig,
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
}
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", domain.AuthenticatorAttachmentCrossPlattform)
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", "rpID", domain.AuthenticatorAttachmentCrossPlattform)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge))
|
||||
|
||||
71
internal/command/user_v2_password.go
Normal file
71
internal/command/user_v2_password.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
// RequestPasswordReset generates a code
|
||||
// and triggers a notification e-mail with the default confirmation URL format.
|
||||
func (c *Commands) RequestPasswordReset(ctx context.Context, userID string) (*domain.ObjectDetails, *string, error) {
|
||||
return c.requestPasswordReset(ctx, userID, false, "", domain.NotificationTypeEmail)
|
||||
}
|
||||
|
||||
// RequestPasswordResetURLTemplate generates a code
|
||||
// and triggers a notification e-mail with the confirmation URL rendered from the passed urlTmpl.
|
||||
// urlTmpl must be a valid [tmpl.Template].
|
||||
func (c *Commands) RequestPasswordResetURLTemplate(ctx context.Context, userID, urlTmpl string, notificationType domain.NotificationType) (*domain.ObjectDetails, *string, error) {
|
||||
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTmpl, userID, "code", "orgID"); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return c.requestPasswordReset(ctx, userID, false, urlTmpl, notificationType)
|
||||
}
|
||||
|
||||
// RequestPasswordResetReturnCode generates a code and does not send a notification email.
|
||||
// The generated plain text code will be returned.
|
||||
func (c *Commands) RequestPasswordResetReturnCode(ctx context.Context, userID string) (*domain.ObjectDetails, *string, error) {
|
||||
return c.requestPasswordReset(ctx, userID, true, "", 0)
|
||||
}
|
||||
|
||||
// requestPasswordReset creates a code for a password change.
|
||||
// returnCode controls if the plain text version of the code will be set in the return object.
|
||||
// When the plain text code is returned, no notification e-mail will be sent to the user.
|
||||
// urlTmpl allows changing the target URL that is used by the e-mail and should be a validated Go template, if used.
|
||||
func (c *Commands) requestPasswordReset(ctx context.Context, userID string, returnCode bool, urlTmpl string, notificationType domain.NotificationType) (_ *domain.ObjectDetails, plainCode *string, err error) {
|
||||
if userID == "" {
|
||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SAFdda", "Errors.User.IDMissing")
|
||||
}
|
||||
model, err := c.getHumanWriteModelByID(ctx, userID, "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !model.UserState.Exists() {
|
||||
return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-SAF4f", "Errors.User.NotFound")
|
||||
}
|
||||
if model.UserState == domain.UserStateInitial {
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfe4g", "Errors.User.NotInitialised")
|
||||
}
|
||||
if authz.GetCtxData(ctx).UserID != userID {
|
||||
if err = c.checkPermission(ctx, domain.PermissionUserWrite, model.ResourceOwner, userID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
code, err := c.newCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cmd := user.NewHumanPasswordCodeAddedEventV2(ctx, UserAggregateFromWriteModel(&model.WriteModel), code.Crypted, code.Expiry, notificationType, urlTmpl, returnCode)
|
||||
|
||||
if returnCode {
|
||||
plainCode = &code.Plain
|
||||
}
|
||||
if err = c.pushAppendAndReduce(ctx, model, cmd); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&model.WriteModel), plainCode, nil
|
||||
}
|
||||
611
internal/command/user_v2_password_test.go
Normal file
611
internal/command/user_v2_password_test.go
Normal file
@@ -0,0 +1,611 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
func TestCommands_RequestPasswordReset(t *testing.T) {
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "missing userID",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SAFdda", "Errors.User.IDMissing"),
|
||||
},
|
||||
{
|
||||
name: "user not existing",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-SAF4f", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "user not initialized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfe4g", "Errors.User.NotInitialised"),
|
||||
},
|
||||
{
|
||||
name: "missing permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
}
|
||||
_, _, err := c.RequestPasswordReset(tt.args.ctx, tt.args.userID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful cases are tested in TestCommands_requestPasswordReset
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_RequestPasswordResetReturnCode(t *testing.T) {
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "missing userID",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SAFdda", "Errors.User.IDMissing"),
|
||||
},
|
||||
{
|
||||
name: "user not existing",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-SAF4f", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "user not initialized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfe4g", "Errors.User.NotInitialised"),
|
||||
},
|
||||
{
|
||||
name: "missing permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
}
|
||||
_, _, err := c.RequestPasswordResetReturnCode(tt.args.ctx, tt.args.userID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful cases are tested in TestCommands_requestPasswordReset
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_RequestPasswordResetURLTemplate(t *testing.T) {
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
urlTmpl string
|
||||
notificationType domain.NotificationType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid template",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
|
||||
{
|
||||
name: "missing userID",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SAFdda", "Errors.User.IDMissing"),
|
||||
},
|
||||
{
|
||||
name: "user not existing",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-SAF4f", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "user not initialized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfe4g", "Errors.User.NotInitialised"),
|
||||
},
|
||||
{
|
||||
name: "missing permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
}
|
||||
_, _, err := c.RequestPasswordResetURLTemplate(tt.args.ctx, tt.args.userID, tt.args.urlTmpl, tt.args.notificationType)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful cases are tested in TestCommands_requestPasswordReset
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_requestPasswordReset(t *testing.T) {
|
||||
type fields struct {
|
||||
checkPermission domain.PermissionCheck
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
newCode cryptoCodeFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID string
|
||||
returnCode bool
|
||||
urlTmpl string
|
||||
notificationType domain.NotificationType
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
code *string
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "missing userID",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-SAFdda", "Errors.User.IDMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user not existing",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowNotFound(nil, "COMMAND-SAF4f", "Errors.User.NotFound"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user not initialized",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{CryptoType: crypto.TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("code")}, 10*time.Second),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfe4g", "Errors.User.NotInitialised"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "code generated",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
user.NewHumanPasswordCodeAddedEventV2(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
10*time.Minute,
|
||||
domain.NotificationTypeEmail,
|
||||
"",
|
||||
false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockCode("code", 10*time.Minute),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
},
|
||||
res: res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
code: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "code generated template",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
user.NewHumanPasswordCodeAddedEventV2(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
10*time.Minute,
|
||||
domain.NotificationTypeEmail,
|
||||
"https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockCode("code", 10*time.Minute),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
urlTmpl: "https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
},
|
||||
res: res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
code: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "code generated template sms",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
user.NewHumanPasswordCodeAddedEventV2(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
10*time.Minute,
|
||||
domain.NotificationTypeSms,
|
||||
"https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
false,
|
||||
)),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockCode("code", 10*time.Minute),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
urlTmpl: "https://example.com/password/changey?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
notificationType: domain.NotificationTypeSms,
|
||||
},
|
||||
res: res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
code: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "code generated returned",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstname", "lastname", "nickname", "displayname",
|
||||
language.English, domain.GenderUnspecified, "email", false),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
user.NewHumanPasswordCodeAddedEventV2(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("code"),
|
||||
},
|
||||
10*time.Minute,
|
||||
domain.NotificationTypeEmail,
|
||||
"",
|
||||
true,
|
||||
)),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
newCode: mockCode("code", 10*time.Minute),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "userID",
|
||||
returnCode: true,
|
||||
},
|
||||
res: res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
code: gu.Ptr("code"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
newCode: tt.fields.newCode,
|
||||
}
|
||||
got, gotPlainCode, err := c.requestPasswordReset(tt.args.ctx, tt.args.userID, tt.args.returnCode, tt.args.urlTmpl, tt.args.notificationType)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.details, got)
|
||||
assert.Equal(t, tt.res.code, gotPlainCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
33
internal/command/user_v2_totp.go
Normal file
33
internal/command/user_v2_totp.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
func (c *Commands) AddUserTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) {
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prep, err := c.createHumanTOTP(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.pushAppendAndReduce(ctx, prep.wm, prep.cmds...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.TOTP{
|
||||
ObjectDetails: writeModelToObjectDetails(&prep.wm.WriteModel),
|
||||
Secret: prep.key.Secret(),
|
||||
URI: prep.key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) CheckUserTOTP(ctx context.Context, userID, code, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HumanCheckMFAOTPSetup(ctx, userID, code, "", resourceOwner)
|
||||
}
|
||||
263
internal/command/user_v2_totp_test.go
Normal file
263
internal/command/user_v2_totp_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
func TestCommands_AddUserTOTP(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceowner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "wrong user",
|
||||
args: args{
|
||||
userID: "foo",
|
||||
resourceowner: "org1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "create otp error",
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceowner: "org1",
|
||||
},
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-MM9fs", "Errors.User.NotFound"),
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceowner: "org1",
|
||||
},
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(ctx,
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(ctx,
|
||||
userAgg,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(ctx,
|
||||
userAgg,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectRandomPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, nil),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceowner: "org1",
|
||||
},
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(ctx,
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(ctx,
|
||||
userAgg,
|
||||
"org",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(ctx,
|
||||
userAgg,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectRandomPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, nil),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
multifactors: domain.MultifactorConfigs{
|
||||
OTP: domain.OTPConfig{
|
||||
Issuer: "zitadel.com",
|
||||
CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := c.AddUserTOTP(ctx, tt.args.userID, tt.args.resourceowner)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "org1", got.ResourceOwner)
|
||||
assert.NotEmpty(t, got.Secret)
|
||||
assert.NotEmpty(t, got.URI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_CheckUserTOTP(t *testing.T) {
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
|
||||
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
key, secret, err := domain.NewOTPKey("example.com", "user1", cryptoAlg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
code, err := totp.GenerateCode(key.Secret(), time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
code string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "wrong user id",
|
||||
args: args{
|
||||
userID: "foo",
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||
),
|
||||
),
|
||||
expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanOTPVerifiedEvent(ctx, userAgg, ""),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
resourceOwner: "org1",
|
||||
code: code,
|
||||
userID: "user1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
multifactors: domain.MultifactorConfigs{
|
||||
OTP: domain.OTPConfig{
|
||||
CryptoMFA: cryptoAlg,
|
||||
},
|
||||
},
|
||||
}
|
||||
got, err := c.CheckUserTOTP(ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, "org1", got.ResourceOwner)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,31 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner, rpID string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.registerUserU2F(ctx, userID, resourceOwner)
|
||||
return c.registerUserU2F(ctx, userID, resourceOwner, rpID)
|
||||
}
|
||||
|
||||
func (c *Commands) registerUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
wm, userAgg, webAuthN, err := c.createUserU2F(ctx, userID, resourceOwner)
|
||||
func (c *Commands) registerUserU2F(ctx context.Context, userID, resourceOwner, rpID string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
wm, userAgg, webAuthN, err := c.createUserU2F(ctx, userID, resourceOwner, rpID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.pushUserU2F(ctx, wm, userAgg, webAuthN)
|
||||
}
|
||||
|
||||
func (c *Commands) createUserU2F(ctx context.Context, userID, resourceOwner string) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
func (c *Commands) createUserU2F(ctx context.Context, userID, resourceOwner, rpID string) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
tokens, err := c.getHumanU2FTokens(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, tokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementRequired)
|
||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, rpID, tokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementRequired)
|
||||
}
|
||||
|
||||
func (c *Commands) pushUserU2F(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken) (*domain.WebAuthNRegistrationDetails, error) {
|
||||
cmd := user.NewHumanU2FAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||
cmd := user.NewHumanU2FAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge, webAuthN.RPID)
|
||||
err := c.pushAppendAndReduce(ctx, wm, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -37,6 +37,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) {
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
rpID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -114,7 +115,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) {
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
_, err := c.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner)
|
||||
_, err := c.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.rpID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful case can't be tested due to random challenge.
|
||||
})
|
||||
@@ -160,7 +161,7 @@ func TestCommands_pushUserU2F(t *testing.T) {
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
|
||||
ctx, &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
|
||||
), "111", "challenge"),
|
||||
), "111", "challenge", "rpID"),
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ func TestCommands_pushUserU2F(t *testing.T) {
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanU2FAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
userAgg, "123", challenge, "rpID",
|
||||
),
|
||||
)})
|
||||
},
|
||||
@@ -185,7 +186,7 @@ func TestCommands_pushUserU2F(t *testing.T) {
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanU2FAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
userAgg, "123", challenge, "rpID",
|
||||
),
|
||||
)})
|
||||
},
|
||||
@@ -198,7 +199,7 @@ func TestCommands_pushUserU2F(t *testing.T) {
|
||||
webauthnConfig: webauthnConfig,
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
}
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", domain.AuthenticatorAttachmentCrossPlattform)
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", "rpID", domain.AuthenticatorAttachmentCrossPlattform)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge))
|
||||
|
||||
Reference in New Issue
Block a user