mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-12 03:28:32 +00:00
feat: integrate passwap for human user password hashing (#6196)
* feat: use passwap for human user passwords * fix tests * passwap config * add the event mapper * cleanup query side and api * solve linting errors * regression test * try to fix linter errors again * pass systemdefaults into externalConfigChange migration * fix: user password set in auth view * pin passwap v0.2.0 * v2: validate hashed password hash based on prefix * resolve remaining comments * add error tag and translation for unsupported hash encoding * fix unit test --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
6fcfa63f54
commit
4589ddad4a
@ -356,6 +356,51 @@ SystemDefaults:
|
||||
PasswordSaltCost: 14
|
||||
MachineKeySize: 2048
|
||||
ApplicationKeySize: 2048
|
||||
PasswordHasher:
|
||||
# Set hasher configuration for user passwords.
|
||||
# Passwords previously hashed with a different algorithm
|
||||
# or cost are automatically re-hashed using this config,
|
||||
# upon password validation or update.
|
||||
Hasher:
|
||||
Algorithm: "bcrypt"
|
||||
Cost: 14
|
||||
|
||||
# Other supported Hasher configs:
|
||||
|
||||
# Hasher:
|
||||
# Algorithm: "argon2i"
|
||||
# Time: 3
|
||||
# Memory: 32768
|
||||
# Threads: 4
|
||||
|
||||
# Hasher:
|
||||
# Algorithm: "argon2id"
|
||||
# Time: 1
|
||||
# Memory: 65536
|
||||
# Threads: 4
|
||||
|
||||
# Hasher:
|
||||
# Algorithm: "scrypt"
|
||||
# Cost: 15
|
||||
|
||||
# Verifiers enable the possibility of verifying
|
||||
# passwords that are previously hashed using another
|
||||
# algorithm then the Hasher.
|
||||
# This can be used when migrating from one algorithm to another,
|
||||
# or when importing users with hashed passwords.
|
||||
# There is no need to enable a Verifier of the same algorithm
|
||||
# as the Hasher.
|
||||
#
|
||||
# The format of the encoded hash strings must comply
|
||||
# with https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
# https://passlib.readthedocs.io/en/stable/modular_crypt_format.html
|
||||
#
|
||||
# Supported verifiers: (uncomment to enable)
|
||||
# Verifiers:
|
||||
# - "argon2" # verifier for both argon2i and argon2id.
|
||||
# - "bcrypt"
|
||||
# - "md5"
|
||||
# - "scrypt"
|
||||
Multifactors:
|
||||
OTP:
|
||||
# If this is empty, the issuer is the requested domain
|
||||
|
@ -17,6 +17,7 @@ type externalConfigChange struct {
|
||||
currentExternalDomain string
|
||||
currentExternalSecure bool
|
||||
currentExternalPort uint16
|
||||
defaults systemdefaults.SystemDefaults
|
||||
}
|
||||
|
||||
func (mig *externalConfigChange) SetLastExecution(lastRun map[string]interface{}) {
|
||||
@ -35,7 +36,7 @@ func (mig *externalConfigChange) Check() bool {
|
||||
func (mig *externalConfigChange) Execute(ctx context.Context) error {
|
||||
cmd, err := command.StartCommands(
|
||||
mig.es,
|
||||
systemdefaults.SystemDefaults{},
|
||||
mig.defaults,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
|
@ -104,6 +104,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
||||
ExternalDomain: config.ExternalDomain,
|
||||
ExternalPort: config.ExternalPort,
|
||||
ExternalSecure: config.ExternalSecure,
|
||||
defaults: config.SystemDefaults,
|
||||
},
|
||||
&projectionTables{
|
||||
es: eventstoreClient,
|
||||
|
1
go.mod
1
go.mod
@ -62,6 +62,7 @@ require (
|
||||
github.com/ttacon/libphonenumber v1.2.1
|
||||
github.com/zitadel/logging v0.3.4
|
||||
github.com/zitadel/oidc/v2 v2.6.3
|
||||
github.com/zitadel/passwap v0.2.0
|
||||
github.com/zitadel/saml v0.0.11
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
|
||||
|
2
go.sum
2
go.sum
@ -911,6 +911,8 @@ github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM
|
||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
||||
github.com/zitadel/oidc/v2 v2.6.3 h1:YY87cAcdI+3voZqcRU2RGz3Pxky/2KsjDmYDVb6EgWw=
|
||||
github.com/zitadel/oidc/v2 v2.6.3/go.mod h1:2LrbdKYLSgKxXBfct56ev4e186J7TXotlZxb6tExOO4=
|
||||
github.com/zitadel/passwap v0.2.0 h1:rkYrax9hfRIpVdXJ7pS8JHkQOhuQTdZQxEhsY0dFFrU=
|
||||
github.com/zitadel/passwap v0.2.0/go.mod h1:KRTL4LL8ugJIn2xLoQYZf5t4kDyr7w41uq3XqvUlO6w=
|
||||
github.com/zitadel/saml v0.0.11 h1:kObucnBrcu1PHCO7RGT0iVeuJL/5I50gUgr40S41nMs=
|
||||
github.com/zitadel/saml v0.0.11/go.mod h1:YGWAvPZRv4DbEZ78Ht/2P0AWzGn+6WGhFf90PMXl0Po=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
|
@ -577,15 +577,14 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
|
||||
}
|
||||
if withPasswords {
|
||||
ctx, pwspan := tracing.NewSpan(ctx)
|
||||
hashedPassword, hashAlgorithm, err := s.query.GetHumanPassword(ctx, org, user.ID)
|
||||
encodedHash, err := s.query.GetHumanPassword(ctx, org, user.ID)
|
||||
pwspan.EndWithError(err)
|
||||
if err != nil && !caos_errors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
if err == nil && hashedPassword != nil {
|
||||
if err == nil && encodedHash != "" {
|
||||
dataUser.User.HashedPassword = &management_pb.ImportHumanUserRequest_HashedPassword{
|
||||
Value: string(hashedPassword),
|
||||
Algorithm: hashAlgorithm,
|
||||
Value: encodedHash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,9 +121,7 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
|
||||
human.Password.ChangeRequired = req.PasswordChangeRequired
|
||||
}
|
||||
|
||||
if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" {
|
||||
human.HashedPassword = domain.NewHashedPassword(req.HashedPassword.Value, req.HashedPassword.Algorithm)
|
||||
}
|
||||
human.HashedPassword = req.GetHashedPassword().GetValue()
|
||||
links = make([]*domain.UserIDPLink, len(req.Idps))
|
||||
for i, idp := range req.Idps {
|
||||
links[i] = &domain.UserIDPLink{
|
||||
|
@ -48,10 +48,6 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
|
||||
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
|
||||
for i, metadataEntry := range req.Metadata {
|
||||
@ -85,7 +81,7 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
|
||||
Gender: genderToDomain(req.GetProfile().GetGender()),
|
||||
Phone: command.Phone{}, // TODO: add as soon as possible
|
||||
Password: req.GetPassword().GetPassword(),
|
||||
BcryptedPassword: bcryptedPassword,
|
||||
EncodedPasswordHash: req.GetHashedPassword().GetHash(),
|
||||
PasswordChangeRequired: passwordChangeRequired,
|
||||
Passwordless: false,
|
||||
Register: false,
|
||||
@ -109,17 +105,6 @@ func genderToDomain(gender user.Gender) domain.Gender {
|
||||
}
|
||||
}
|
||||
|
||||
func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
|
||||
if hashed == nil {
|
||||
return "", nil
|
||||
}
|
||||
// we currently only handle bcrypt
|
||||
if hashed.GetAlgorithm() != "bcrypt" {
|
||||
return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
|
||||
}
|
||||
return hashed.GetHash(), nil
|
||||
}
|
||||
|
||||
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
|
||||
orgID := authz.GetCtxData(ctx).OrgID
|
||||
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &domain.UserIDPLink{
|
||||
|
@ -392,6 +392,79 @@ func TestServer_AddHumanUser(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hashed password",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddHumanUserRequest{
|
||||
Organisation: &object.Organisation{
|
||||
Org: &object.Organisation_OrgId{
|
||||
OrgId: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
Profile: &user.SetHumanProfile{
|
||||
FirstName: "Donald",
|
||||
LastName: "Duck",
|
||||
NickName: gu.Ptr("Dukkie"),
|
||||
DisplayName: gu.Ptr("Donald Duck"),
|
||||
PreferredLanguage: gu.Ptr("en"),
|
||||
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||
},
|
||||
Email: &user.SetHumanEmail{},
|
||||
Metadata: []*user.SetMetadataEntry{
|
||||
{
|
||||
Key: "somekey",
|
||||
Value: []byte("somevalue"),
|
||||
},
|
||||
},
|
||||
PasswordType: &user.AddHumanUserRequest_HashedPassword{
|
||||
HashedPassword: &user.HashedPassword{
|
||||
Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.AddHumanUserResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported hashed password",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddHumanUserRequest{
|
||||
Organisation: &object.Organisation{
|
||||
Org: &object.Organisation_OrgId{
|
||||
OrgId: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
Profile: &user.SetHumanProfile{
|
||||
FirstName: "Donald",
|
||||
LastName: "Duck",
|
||||
NickName: gu.Ptr("Dukkie"),
|
||||
DisplayName: gu.Ptr("Donald Duck"),
|
||||
PreferredLanguage: gu.Ptr("en"),
|
||||
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||
},
|
||||
Email: &user.SetHumanEmail{},
|
||||
Metadata: []*user.SetMetadataEntry{
|
||||
{
|
||||
Key: "somekey",
|
||||
Value: []byte("somevalue"),
|
||||
},
|
||||
},
|
||||
PasswordType: &user.AddHumanUserRequest_HashedPassword{
|
||||
HashedPassword: &user.HashedPassword{
|
||||
Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -25,74 +24,6 @@ import (
|
||||
|
||||
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration", "google.protobuf.Struct"}
|
||||
|
||||
func Test_hashedPasswordToCommand(t *testing.T) {
|
||||
type args struct {
|
||||
hashed *user.HashedPassword
|
||||
}
|
||||
type res struct {
|
||||
want string
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"not hashed",
|
||||
args{
|
||||
hashed: nil,
|
||||
},
|
||||
res{
|
||||
"",
|
||||
nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"hashed, not bcrypt",
|
||||
args{
|
||||
hashed: &user.HashedPassword{
|
||||
Hash: "hash",
|
||||
Algorithm: "custom",
|
||||
},
|
||||
},
|
||||
res{
|
||||
"",
|
||||
func(err error) bool {
|
||||
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"hashed, bcrypt",
|
||||
args{
|
||||
hashed: &user.HashedPassword{
|
||||
Hash: "hash",
|
||||
Algorithm: "bcrypt",
|
||||
},
|
||||
},
|
||||
res{
|
||||
"hash",
|
||||
nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := hashedPasswordToCommand(tt.args.hashed)
|
||||
if tt.res.err == nil {
|
||||
require.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_intentToIDPInformationPb(t *testing.T) {
|
||||
decryption := func(err error) crypto.EncryptionAlgorithm {
|
||||
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
|
@ -49,7 +49,8 @@ type Commands struct {
|
||||
smtpEncryption crypto.EncryptionAlgorithm
|
||||
smsEncryption crypto.EncryptionAlgorithm
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
codeAlg crypto.HashAlgorithm
|
||||
machineKeySize int
|
||||
applicationKeySize int
|
||||
domainVerificationAlg crypto.EncryptionAlgorithm
|
||||
@ -140,7 +141,11 @@ func StartCommands(
|
||||
oidcsession.RegisterEventMappers(repo.eventstore)
|
||||
milestone.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.userPasswordHasher, err = defaults.PasswordHasher.PasswordHasher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
||||
repo.applicationKeySize = int(defaults.SecretGenerators.ApplicationKeySize)
|
||||
|
||||
|
@ -335,7 +335,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
} else if setup.Org.Human != nil {
|
||||
setup.Org.Human.ID = userID
|
||||
validations = append(validations,
|
||||
c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordAlg, c.userEncryption, true),
|
||||
c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordHasher, c.userEncryption, true),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,13 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/zitadel/passwap"
|
||||
"github.com/zitadel/passwap/verifier"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@ -293,3 +296,38 @@ func newMockPermissionCheckNotAllowed() domain.PermissionCheck {
|
||||
return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")
|
||||
}
|
||||
}
|
||||
|
||||
type plainHasher struct {
|
||||
x string // arbitrary info that triggers update when different from encoding
|
||||
}
|
||||
|
||||
func (h plainHasher) Hash(password string) (string, error) {
|
||||
return strings.Join([]string{"", "plain", h.x, password}, "$"), nil
|
||||
}
|
||||
|
||||
func (h plainHasher) Verify(encoded, password string) (verifier.Result, error) {
|
||||
nodes := strings.Split(encoded, "$")
|
||||
if len(nodes) != 4 || nodes[1] != "plain" {
|
||||
return verifier.Skip, nil
|
||||
}
|
||||
if nodes[3] != password {
|
||||
return verifier.Fail, nil
|
||||
}
|
||||
if nodes[2] != h.x {
|
||||
return verifier.NeedUpdate, nil
|
||||
}
|
||||
return verifier.OK, nil
|
||||
}
|
||||
|
||||
// mockPasswordHasher creates a swapper for plain (cleartext) password used in tests.
|
||||
// x can be set to arbitrary info which triggers updates when different from the
|
||||
// setting in the encoded hashes. (normally cost parameters)
|
||||
//
|
||||
// With `x` set to "foo", the following encoded string would be produced by Hash:
|
||||
// $plain$foo$password
|
||||
func mockPasswordHasher(x string) *crypto.PasswordHasher {
|
||||
return &crypto.PasswordHasher{
|
||||
Swapper: passwap.NewSwapper(plainHasher{x: x}),
|
||||
Prefixes: []string{"$plain$"},
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID strin
|
||||
var pat *PersonalAccessToken
|
||||
if o.Human != nil {
|
||||
o.Human.ID = userID
|
||||
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordAlg, c.userEncryption, true))
|
||||
validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordHasher, c.userEncryption, true))
|
||||
} else if o.Machine != nil {
|
||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
||||
if o.Machine.Pat != nil {
|
||||
|
@ -244,7 +244,7 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID,
|
||||
|
||||
projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel)
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
|
||||
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.codeAlg)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err == nil {
|
||||
_, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
|
||||
|
@ -326,7 +326,7 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID,
|
||||
|
||||
projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel)
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
|
||||
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.codeAlg)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err == nil {
|
||||
_, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
|
||||
|
@ -1,6 +1,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@ -13,30 +14,34 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type SessionCommand func(ctx context.Context, cmd *SessionCommands) error
|
||||
|
||||
type SessionCommands struct {
|
||||
cmds []SessionCommand
|
||||
sessionCommands []SessionCommand
|
||||
|
||||
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
|
||||
eventCommands []eventstore.Command
|
||||
|
||||
hasher *crypto.PasswordHasher
|
||||
intentAlg crypto.EncryptionAlgorithm
|
||||
createToken func(sessionID string) (id string, token string, err error)
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
|
||||
return &SessionCommands{
|
||||
cmds: cmds,
|
||||
sessionCommands: cmds,
|
||||
sessionWriteModel: session,
|
||||
eventstore: c.eventstore,
|
||||
userPasswordAlg: c.userPasswordAlg,
|
||||
hasher: c.userPasswordHasher,
|
||||
intentAlg: c.idpConfigEncryption,
|
||||
createToken: c.sessionTokenCreator,
|
||||
now: time.Now,
|
||||
@ -49,7 +54,7 @@ func CheckUser(id string) SessionCommand {
|
||||
if cmd.sessionWriteModel.UserID != "" && id != "" && cmd.sessionWriteModel.UserID != id {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "", "user change not possible")
|
||||
}
|
||||
return cmd.sessionWriteModel.UserChecked(ctx, id, cmd.now())
|
||||
return cmd.UserChecked(ctx, id, cmd.now())
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,17 +73,21 @@ func CheckPassword(password string) SessionCommand {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
if cmd.passwordWriteModel.Secret == nil {
|
||||
if cmd.passwordWriteModel.EncodedHash == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-WEf3t", "Errors.User.Password.NotSet")
|
||||
}
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(cmd.passwordWriteModel.Secret, []byte(password), cmd.userPasswordAlg)
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
|
||||
updated, err := cmd.hasher.Verify(cmd.passwordWriteModel.EncodedHash, password)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err != nil {
|
||||
//TODO: maybe we want to reset the session in the future https://github.com/zitadel/zitadel/issues/5807
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-SAF3g", "Errors.User.Password.Invalid")
|
||||
}
|
||||
cmd.sessionWriteModel.PasswordChecked(ctx, cmd.now())
|
||||
if updated != "" {
|
||||
cmd.eventCommands = append(cmd.eventCommands, user.NewHumanPasswordHashUpdatedEvent(ctx, UserAggregateFromWriteModel(&cmd.passwordWriteModel.WriteModel), updated))
|
||||
}
|
||||
|
||||
cmd.PasswordChecked(ctx, cmd.now())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -114,14 +123,14 @@ func CheckIntent(intentID, token string) SessionCommand {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
|
||||
}
|
||||
}
|
||||
cmd.sessionWriteModel.IntentChecked(ctx, cmd.now())
|
||||
cmd.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 {
|
||||
for _, cmd := range s.sessionCommands {
|
||||
if err := cmd(ctx, s); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -129,6 +138,66 @@ func (s *SessionCommands) Exec(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionCommands) Start(ctx context.Context, domain string) {
|
||||
s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, domain))
|
||||
// set the domain so checks can use it
|
||||
s.sessionWriteModel.Domain = domain
|
||||
}
|
||||
|
||||
func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
|
||||
s.eventCommands = append(s.eventCommands, session.NewUserCheckedEvent(ctx, s.sessionWriteModel.aggregate, userID, checkedAt))
|
||||
// set the userID so other checks can use it
|
||||
s.sessionWriteModel.UserID = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Time) {
|
||||
s.eventCommands = append(s.eventCommands, session.NewPasswordCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
|
||||
}
|
||||
|
||||
func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) {
|
||||
s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
|
||||
}
|
||||
|
||||
func (s *SessionCommands) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) {
|
||||
s.eventCommands = append(s.eventCommands, session.NewPasskeyChallengedEvent(ctx, s.sessionWriteModel.aggregate, challenge, allowedCrentialIDs, userVerification))
|
||||
}
|
||||
|
||||
func (s *SessionCommands) PasskeyChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32) {
|
||||
s.eventCommands = append(s.eventCommands,
|
||||
session.NewPasskeyCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt),
|
||||
usr_repo.NewHumanPasswordlessSignCountChangedEvent(ctx, s.sessionWriteModel.aggregate, tokenID, signCount),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *SessionCommands) SetToken(ctx context.Context, tokenID string) {
|
||||
s.eventCommands = append(s.eventCommands, session.NewTokenSetEvent(ctx, s.sessionWriteModel.aggregate, tokenID))
|
||||
}
|
||||
|
||||
func (s *SessionCommands) ChangeMetadata(ctx context.Context, metadata map[string][]byte) {
|
||||
var changed bool
|
||||
for key, value := range metadata {
|
||||
currentValue, exists := s.sessionWriteModel.Metadata[key]
|
||||
|
||||
if len(value) != 0 {
|
||||
// if a value is provided, and it's not equal, change it
|
||||
if !bytes.Equal(currentValue, value) {
|
||||
s.sessionWriteModel.Metadata[key] = value
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
// if there's no / an empty value, we only need to remove it on existing entries
|
||||
if exists {
|
||||
delete(s.sessionWriteModel.Metadata, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
s.eventCommands = append(s.eventCommands, session.NewMetadataSetEvent(ctx, s.sessionWriteModel.aggregate, s.sessionWriteModel.Metadata))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteModel, error) {
|
||||
if s.sessionWriteModel.UserID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing")
|
||||
@ -145,7 +214,7 @@ func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteMo
|
||||
}
|
||||
|
||||
func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Command, error) {
|
||||
if len(s.sessionWriteModel.commands) == 0 {
|
||||
if len(s.eventCommands) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
@ -153,8 +222,8 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
s.sessionWriteModel.SetToken(ctx, tokenID)
|
||||
return token, s.sessionWriteModel.commands, nil
|
||||
s.SetToken(ctx, tokenID)
|
||||
return token, s.eventCommands, nil
|
||||
}
|
||||
|
||||
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, sessionDomain string, metadata map[string][]byte) (set *SessionChanged, err error) {
|
||||
@ -167,8 +236,8 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, ses
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionWriteModel.Start(ctx, sessionDomain)
|
||||
cmd := c.NewSessionCommands(cmds, sessionWriteModel)
|
||||
cmd.Start(ctx, sessionDomain)
|
||||
return c.updateSession(ctx, cmd, metadata)
|
||||
}
|
||||
|
||||
@ -217,7 +286,7 @@ func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, m
|
||||
// TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807
|
||||
return nil, err
|
||||
}
|
||||
checks.sessionWriteModel.ChangeMetadata(ctx, metadata)
|
||||
checks.ChangeMetadata(ctx, metadata)
|
||||
sessionToken, cmds, err := checks.commands(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1,15 +1,12 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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/session"
|
||||
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
type PasskeyChallengeModel struct {
|
||||
@ -48,7 +45,6 @@ type SessionWriteModel struct {
|
||||
|
||||
PasskeyChallenge *PasskeyChallengeModel
|
||||
|
||||
commands []eventstore.Command
|
||||
aggregate *eventstore.Aggregate
|
||||
}
|
||||
|
||||
@ -151,66 +147,6 @@ func (wm *SessionWriteModel) reduceTerminate() {
|
||||
wm.State = domain.SessionStateTerminated
|
||||
}
|
||||
|
||||
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 {
|
||||
wm.commands = append(wm.commands, session.NewUserCheckedEvent(ctx, wm.aggregate, userID, checkedAt))
|
||||
// set the userID so other checks can use it
|
||||
wm.UserID = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time.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))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) PasskeyChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32) {
|
||||
wm.commands = append(wm.commands,
|
||||
session.NewPasskeyCheckedEvent(ctx, wm.aggregate, checkedAt),
|
||||
usr_repo.NewHumanPasswordlessSignCountChangedEvent(ctx, wm.aggregate, tokenID, signCount),
|
||||
)
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) SetToken(ctx context.Context, tokenID string) {
|
||||
wm.commands = append(wm.commands, session.NewTokenSetEvent(ctx, wm.aggregate, tokenID))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) ChangeMetadata(ctx context.Context, metadata map[string][]byte) {
|
||||
var changed bool
|
||||
for key, value := range metadata {
|
||||
currentValue, exists := wm.Metadata[key]
|
||||
|
||||
if len(value) != 0 {
|
||||
// if a value is provided, and it's not equal, change it
|
||||
if !bytes.Equal(currentValue, value) {
|
||||
wm.Metadata[key] = value
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
// if there's no / an empty value, we only need to remove it on existing entries
|
||||
if exists {
|
||||
delete(wm.Metadata, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
wm.commands = append(wm.commands, session.NewMetadataSetEvent(ctx, wm.aggregate, wm.Metadata))
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticationTime returns the time the user authenticated using the latest time of all checks
|
||||
func (wm *SessionWriteModel) AuthenticationTime() time.Time {
|
||||
var authTime time.Time
|
||||
|
@ -51,7 +51,7 @@ func (c *Commands) CreatePasskeyChallenge(userVerification domain.UserVerificati
|
||||
return caos_errs.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
|
||||
}
|
||||
|
||||
cmd.sessionWriteModel.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification)
|
||||
cmd.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,7 @@ func (c *Commands) CheckPasskey(credentialAssertionData json.Marshaler) SessionC
|
||||
if token == nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
|
||||
}
|
||||
cmd.sessionWriteModel.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount)
|
||||
cmd.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,6 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) {
|
||||
|
||||
func TestCommands_CreateSession(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
tokenCreator func(sessionID string) (string, string, error)
|
||||
}
|
||||
@ -158,6 +157,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
expect []expect
|
||||
res res
|
||||
}{
|
||||
{
|
||||
@ -168,6 +168,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
[]expect{},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "generator failed"),
|
||||
},
|
||||
@ -176,13 +177,13 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
"eventstore failed",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
[]expect{
|
||||
expectFilterError(caos_errs.ThrowInternal(nil, "id", "filter failed")),
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
|
||||
},
|
||||
@ -191,17 +192,6 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
"empty session",
|
||||
fields{
|
||||
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
tokenCreator: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
@ -211,6 +201,17 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
args{
|
||||
ctx: authz.NewMockContext("", "org1", ""),
|
||||
},
|
||||
[]expect{
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
|
||||
@ -223,17 +224,6 @@ 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",
|
||||
@ -244,6 +234,17 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
ctx: authz.NewMockContext("", "org1", ""),
|
||||
domain: "domain.tld",
|
||||
},
|
||||
[]expect{
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
|
||||
@ -257,7 +258,7 @@ func TestCommands_CreateSession(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
eventstore: eventstoreExpect(t, tt.expect...),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
sessionTokenCreator: tt.fields.tokenCreator,
|
||||
}
|
||||
@ -432,7 +433,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
func(ctx context.Context, cmd *SessionCommands) error {
|
||||
return caos_errs.ThrowInternal(nil, "id", "check failed")
|
||||
},
|
||||
@ -452,7 +453,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{},
|
||||
sessionCommands: []SessionCommand{},
|
||||
},
|
||||
},
|
||||
res{
|
||||
@ -487,7 +488,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckPassword("password"),
|
||||
},
|
||||
@ -499,12 +500,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
}, false, ""),
|
||||
"$plain$x$password", false, ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -513,7 +509,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
hasher: mockPasswordHasher("x"),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
@ -541,7 +537,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
@ -580,7 +576,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
@ -629,7 +625,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent2", "aW50ZW50"),
|
||||
},
|
||||
@ -674,7 +670,7 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
sessionCommands: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
|
@ -49,8 +49,8 @@ type AddHuman struct {
|
||||
Phone Phone
|
||||
// Password is optional
|
||||
Password string
|
||||
// BcryptedPassword is optional
|
||||
BcryptedPassword string
|
||||
// EncodedPasswordHash is optional
|
||||
EncodedPasswordHash string
|
||||
// PasswordChangeRequired is used if the `Password`-field is set
|
||||
PasswordChangeRequired bool
|
||||
Passwordless bool
|
||||
@ -74,7 +74,7 @@ type AddLink struct {
|
||||
IDPExternalID string
|
||||
}
|
||||
|
||||
func (h *AddHuman) Validate() (err error) {
|
||||
func (h *AddHuman) Validate(hasher *crypto.PasswordHasher) (err error) {
|
||||
if err := h.Email.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -101,6 +101,11 @@ func (h *AddHuman) Validate() (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if h.EncodedPasswordHash != "" {
|
||||
if !hasher.EncodingSupported(h.EncodedPasswordHash) {
|
||||
return errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -127,7 +132,7 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad
|
||||
c.AddHumanCommand(
|
||||
human,
|
||||
resourceOwner,
|
||||
c.userPasswordAlg,
|
||||
c.userPasswordHasher,
|
||||
c.userEncryption,
|
||||
allowInitMail,
|
||||
))
|
||||
@ -151,12 +156,13 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad
|
||||
type humanCreationCommand interface {
|
||||
eventstore.Command
|
||||
AddPhoneData(phoneNumber domain.PhoneNumber)
|
||||
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
||||
AddPasswordData(encoded string, changeRequired bool)
|
||||
}
|
||||
|
||||
func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
|
||||
//nolint:gocognit
|
||||
func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, hasher *crypto.PasswordHasher, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
|
||||
return func() (_ preparation.CreateCommands, err error) {
|
||||
if err := human.Validate(); err != nil {
|
||||
if err := human.Validate(hasher); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -210,7 +216,7 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg cr
|
||||
createCmd.AddPhoneData(human.Phone.Number)
|
||||
}
|
||||
|
||||
if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
|
||||
if err := addHumanCommandPassword(ctx, filter, createCmd, human, hasher); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -316,13 +322,13 @@ func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparatio
|
||||
return nil
|
||||
}
|
||||
|
||||
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
|
||||
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, hasher *crypto.PasswordHasher) (err error) {
|
||||
if human.Password != "" {
|
||||
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
||||
secret, err := hasher.Hash(human.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -330,8 +336,8 @@ func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQue
|
||||
return nil
|
||||
}
|
||||
|
||||
if human.BcryptedPassword != "" {
|
||||
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
||||
if human.EncodedPasswordHash != "" {
|
||||
createCmd.AddPasswordData(human.EncodedPasswordHash, human.PasswordChangeRequired)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -578,7 +584,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
|
||||
|
||||
human.EnsureDisplayName()
|
||||
if human.Password != nil {
|
||||
if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, human.Password.ChangeRequired); err != nil {
|
||||
if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@ -677,10 +683,10 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h
|
||||
human.StreetAddress)
|
||||
}
|
||||
if human.Password != nil {
|
||||
addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired)
|
||||
addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
|
||||
}
|
||||
if human.HashedPassword != nil {
|
||||
addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false)
|
||||
if human.HashedPassword != "" {
|
||||
addEvent.AddPasswordData(human.HashedPassword, false)
|
||||
}
|
||||
return addEvent
|
||||
}
|
||||
@ -711,10 +717,10 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega
|
||||
human.StreetAddress)
|
||||
}
|
||||
if human.Password != nil {
|
||||
addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired)
|
||||
addEvent.AddPasswordData(human.Password.EncodedSecret, human.Password.ChangeRequired)
|
||||
}
|
||||
if human.HashedPassword != nil {
|
||||
addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false)
|
||||
if human.HashedPassword != "" {
|
||||
addEvent.AddPasswordData(human.HashedPassword, false)
|
||||
}
|
||||
return addEvent
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
@ -49,7 +50,7 @@ func (c *Commands) ResendInitialMail(ctx context.Context, userID string, email d
|
||||
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string, initCodeGenerator crypto.Generator) error {
|
||||
func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, password string, initCodeGenerator crypto.Generator) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing")
|
||||
}
|
||||
@ -72,26 +73,22 @@ func (c *Commands) HumanVerifyInitCode(ctx context.Context, userID, resourceOwne
|
||||
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInitializedCheckFailedEvent push failed")
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-11v6G", "Errors.User.Code.Invalid")
|
||||
}
|
||||
events := []eventstore.Command{
|
||||
commands := []eventstore.Command{
|
||||
user.NewHumanInitializedCheckSucceededEvent(ctx, userAgg),
|
||||
}
|
||||
if !existingCode.IsEmailVerified {
|
||||
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
commands = append(commands, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
||||
}
|
||||
if passwordString != "" {
|
||||
if password != "" {
|
||||
passwordWriteModel := NewHumanPasswordWriteModel(userID, existingCode.ResourceOwner)
|
||||
passwordWriteModel.UserState = domain.UserStateActive
|
||||
password := &domain.Password{
|
||||
SecretString: passwordString,
|
||||
ChangeRequired: false,
|
||||
}
|
||||
passwordEvent, err := c.changePassword(ctx, "", password, userAgg, passwordWriteModel)
|
||||
passwordCommand, err := c.setPasswordCommand(ctx, passwordWriteModel, password, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
events = append(events, passwordEvent)
|
||||
commands = append(commands, passwordCommand)
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, events...)
|
||||
_, err = c.eventstore.Push(ctx, commands...)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@ -305,8 +304,8 @@ func TestCommandSide_ResendInitialMail(t *testing.T) {
|
||||
|
||||
func TestCommandSide_VerifyInitCode(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -584,18 +583,13 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -615,8 +609,8 @@ func TestCommandSide_VerifyInitCode(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
}
|
||||
err := r.HumanVerifyInitCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.code, tt.args.password, tt.args.secretGenerator)
|
||||
if tt.res.err == nil {
|
||||
|
@ -2,8 +2,10 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/passwap"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -13,80 +15,75 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (c *Commands) SetPassword(ctx context.Context, orgID, userID, passwordString string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) {
|
||||
func (c *Commands) SetPassword(ctx context.Context, orgID, userID, password string, oneTime bool) (objectDetails *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.IDMissing")
|
||||
}
|
||||
existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
wm, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existingPassword.UserState.Exists() {
|
||||
if !wm.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 {
|
||||
if err = c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password := &domain.Password{
|
||||
SecretString: passwordString,
|
||||
ChangeRequired: oneTime,
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
|
||||
passwordEvent, err := c.changePassword(ctx, "", password, userAgg, existingPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, passwordEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingPassword, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingPassword.WriteModel), nil
|
||||
return c.setPassword(ctx, wm, password, oneTime)
|
||||
}
|
||||
|
||||
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
func (c *Commands) SetPasswordWithVerifyCode(ctx context.Context, orgID, userID, code, password, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M9fs", "Errors.IDMissing")
|
||||
}
|
||||
if passwordString == "" {
|
||||
if password == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Mf0sd", "Errors.User.Password.Empty")
|
||||
}
|
||||
existingCode, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
wm, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
|
||||
if wm.Code == nil || wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
|
||||
}
|
||||
|
||||
err = crypto.VerifyCodeWithAlgorithm(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, c.userEncryption)
|
||||
err = crypto.VerifyCodeWithAlgorithm(wm.CodeCreationDate, wm.CodeExpiry, wm.Code, code, c.userEncryption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password := &domain.Password{
|
||||
SecretString: passwordString,
|
||||
ChangeRequired: false,
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
|
||||
passwordEvent, err := c.changePassword(ctx, userAgentID, password, userAgg, existingCode)
|
||||
return c.setPassword(ctx, wm, password, false)
|
||||
}
|
||||
|
||||
func (c *Commands) setPassword(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (objectDetails *domain.ObjectDetails, err error) {
|
||||
command, err := c.setPasswordCommand(ctx, wm, password, changeRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, existingCode, passwordEvent)
|
||||
err = c.pushAppendAndReduce(ctx, wm, command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) setPasswordCommand(ctx context.Context, wm *HumanPasswordWriteModel, password string, changeRequired bool) (_ eventstore.Command, err error) {
|
||||
if err = c.canUpdatePassword(ctx, password, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, span := tracing.NewNamedSpan(ctx, "passwap.Hash")
|
||||
encoded, err := c.userPasswordHasher.Hash(password)
|
||||
span.EndWithError(err)
|
||||
if err = convertPasswapErr(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), encoded, changeRequired, ""), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (objectDetails *domain.ObjectDetails, err error) {
|
||||
@ -99,59 +96,50 @@ func (c *Commands) ChangePassword(ctx context.Context, orgID, userID, oldPasswor
|
||||
if oldPassword == "" || newPassword == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Empty")
|
||||
}
|
||||
existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
wm, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingPassword.Secret == nil {
|
||||
if wm.EncodedHash == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Fds3s", "Errors.User.Password.Empty")
|
||||
}
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(existingPassword.Secret, []byte(oldPassword), c.userPasswordAlg)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0fs", "Errors.User.Password.Invalid")
|
||||
}
|
||||
password := &domain.Password{
|
||||
SecretString: newPassword,
|
||||
ChangeRequired: false,
|
||||
if err = c.canUpdatePassword(ctx, newPassword, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
|
||||
command, err := c.changePassword(ctx, userAgentID, password, userAgg, existingPassword)
|
||||
ctx, spanPasswap := tracing.NewNamedSpan(ctx, "passwap.VerifyAndUpdate")
|
||||
updated, err := c.userPasswordHasher.VerifyAndUpdate(wm.EncodedHash, oldPassword, newPassword)
|
||||
spanPasswap.EndWithError(err)
|
||||
if err = convertPasswapErr(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, wm,
|
||||
user.NewHumanPasswordChangedEvent(ctx, UserAggregateFromWriteModel(&wm.WriteModel), updated, false, userAgentID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(existingPassword, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingPassword.WriteModel), nil
|
||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) changePassword(ctx context.Context, userAgentID string, password *domain.Password, userAgg *eventstore.Aggregate, existingPassword *HumanPasswordWriteModel) (event eventstore.Command, err error) {
|
||||
func (c *Commands) canUpdatePassword(ctx context.Context, newPassword string, wm *HumanPasswordWriteModel) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
|
||||
if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
|
||||
return caos_errs.ThrowNotFound(nil, "COMMAND-G8dh3", "Errors.User.Password.NotFound")
|
||||
}
|
||||
if existingPassword.UserState == domain.UserStateInitial {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
|
||||
if wm.UserState == domain.UserStateInitial {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M9dse", "Errors.User.NotInitialised")
|
||||
}
|
||||
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, userAgg.ResourceOwner)
|
||||
policy, err := c.getOrgPasswordComplexityPolicy(ctx, wm.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if err := password.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg); err != nil {
|
||||
return nil, err
|
||||
|
||||
if err := policy.Check(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
return user.NewHumanPasswordChangedEvent(ctx, userAgg, password.SecretCrypto, password.ChangeRequired, userAgentID), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator) (objectDetails *domain.ObjectDetails, err error) {
|
||||
@ -238,37 +226,44 @@ func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, passwo
|
||||
return caos_errs.ThrowPreconditionFailed(err, "COMMAND-Dft32", "Errors.Org.LoginPolicy.UsernamePasswordNotAllowed")
|
||||
}
|
||||
|
||||
existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
wm, err := c.passwordWriteModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
|
||||
if wm.UserState == domain.UserStateUnspecified || wm.UserState == domain.UserStateDeleted {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
if existingPassword.Secret == nil {
|
||||
if wm.EncodedHash == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet")
|
||||
}
|
||||
|
||||
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
err = crypto.CompareHash(existingPassword.Secret, []byte(password), c.userPasswordAlg)
|
||||
userAgg := UserAggregateFromWriteModel(&wm.WriteModel)
|
||||
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
|
||||
updated, err := c.userPasswordHasher.Verify(wm.EncodedHash, password)
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
err = convertPasswapErr(err)
|
||||
|
||||
commands := make([]eventstore.Command, 0, 2)
|
||||
if err == nil {
|
||||
_, err = c.eventstore.Push(ctx, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
commands = append(commands, user.NewHumanPasswordCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
if updated != "" {
|
||||
commands = append(commands, user.NewHumanPasswordHashUpdatedEvent(ctx, userAgg, updated))
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, commands...)
|
||||
return err
|
||||
}
|
||||
events := make([]eventstore.Command, 0)
|
||||
events = append(events, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
|
||||
commands = append(commands, user.NewHumanPasswordCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
|
||||
if lockoutPolicy != nil && lockoutPolicy.MaxPasswordAttempts > 0 {
|
||||
if existingPassword.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
|
||||
events = append(events, user.NewUserLockedEvent(ctx, userAgg))
|
||||
if wm.PasswordCheckFailedCount+1 >= lockoutPolicy.MaxPasswordAttempts {
|
||||
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
|
||||
}
|
||||
|
||||
}
|
||||
_, err = c.eventstore.Push(ctx, events...)
|
||||
logging.Log("COMMAND-9fj7s").OnError(err).Error("error create password check failed event")
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-452ad", "Errors.User.Password.Invalid")
|
||||
_, pushErr := c.eventstore.Push(ctx, commands...)
|
||||
logging.OnError(pushErr).Error("error create password check failed event")
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {
|
||||
@ -282,3 +277,16 @@ func (c *Commands) passwordWriteModel(ctx context.Context, userID, resourceOwner
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func convertPasswapErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, passwap.ErrPasswordMismatch) {
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-3M0fs", "Errors.User.Password.Invalid")
|
||||
}
|
||||
if errors.Is(err, passwap.ErrPasswordNoChange) {
|
||||
return caos_errs.ThrowPreconditionFailed(err, "COMMAND-Aesh5", "Errors.User.Password.NotChanged")
|
||||
}
|
||||
return caos_errs.ThrowInternal(err, "COMMAND-CahN2", "Errors.Internal")
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
type HumanPasswordWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
Secret *crypto.CryptoValue
|
||||
EncodedHash string
|
||||
SecretChangeRequired bool
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
@ -37,11 +37,11 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanAddedEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanRegisteredEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanInitialCodeAddedEvent:
|
||||
@ -49,7 +49,7 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
|
||||
case *user.HumanInitializedCheckSucceededEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanPasswordChangedEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.Code = nil
|
||||
wm.PasswordCheckFailedCount = 0
|
||||
@ -69,6 +69,8 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
|
||||
wm.PasswordCheckFailedCount = 0
|
||||
case *user.UserRemovedEvent:
|
||||
wm.UserState = domain.UserStateDeleted
|
||||
case *user.HumanPasswordHashUpdatedEvent:
|
||||
wm.EncodedHash = e.EncodedHash
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
@ -98,7 +100,8 @@ func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
user.UserV1PasswordCodeAddedType,
|
||||
user.UserV1EmailVerifiedType,
|
||||
user.UserV1PasswordCheckFailedType,
|
||||
user.UserV1PasswordCheckSucceededType).
|
||||
user.UserV1PasswordCheckSucceededType,
|
||||
user.UserV1PasswordHashUpdatedType).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
|
@ -3,11 +3,13 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zitadel/passwap"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@ -21,9 +23,9 @@ import (
|
||||
|
||||
func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
checkPermission domain.PermissionCheck
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -101,8 +103,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -160,12 +162,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
true,
|
||||
"",
|
||||
),
|
||||
@ -173,8 +170,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -232,12 +229,7 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"",
|
||||
),
|
||||
@ -245,8 +237,8 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -265,9 +257,9 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
checkPermission: tt.fields.checkPermission,
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
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 {
|
||||
@ -285,9 +277,9 @@ func TestCommandSide_SetOneTimePassword(t *testing.T) {
|
||||
|
||||
func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -494,12 +486,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"",
|
||||
),
|
||||
@ -507,8 +494,8 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -527,9 +514,9 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
userEncryption: tt.fields.userEncryption,
|
||||
}
|
||||
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 {
|
||||
@ -547,8 +534,7 @@ func TestCommandSide_SetPasswordWithVerifyCode(t *testing.T) {
|
||||
|
||||
func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -566,67 +552,54 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
expect []expect
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
name: "userid missing, invalid argument error",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
oldPassword: "password",
|
||||
newPassword: "password1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
expect: []expect{},
|
||||
res: res{
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old password missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
name: "old password missing, invalid argument error",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
newPassword: "password1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
expect: []expect{},
|
||||
res: res{
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new password missing, invalid argument error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
),
|
||||
},
|
||||
name: "new password missing, invalid argument error",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
oldPassword: "password",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
expect: []expect{},
|
||||
res: res{
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user not existing, precondition error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
name: "user not existing, precondition error",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
@ -634,6 +607,9 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
oldPassword: "password",
|
||||
newPassword: "password1",
|
||||
},
|
||||
expect: []expect{
|
||||
expectFilter(),
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
@ -641,26 +617,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
{
|
||||
name: "existing password empty, precondition 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -669,49 +626,32 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
newPassword: "password1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
expect: []expect{
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"email@test.ch",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "password not matching, precondition error",
|
||||
name: "password not matching, invalid argument 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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -720,6 +660,47 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
newPassword: "password1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
expect: []expect{
|
||||
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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
res: res{
|
||||
err: caos_errs.IsErrorInvalidArgument,
|
||||
},
|
||||
@ -727,71 +708,7 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
{
|
||||
name: "change password, ok",
|
||||
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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password1"),
|
||||
},
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -800,6 +717,59 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
oldPassword: "password",
|
||||
newPassword: "password1",
|
||||
},
|
||||
expect: []expect{
|
||||
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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$x$password1",
|
||||
false,
|
||||
"",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
@ -810,8 +780,8 @@ func TestCommandSide_ChangePassword(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
eventstore: eventstoreExpect(t, tt.expect...),
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
}
|
||||
got, err := r.ChangePassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.oldPassword, tt.args.newPassword, tt.args.agentID)
|
||||
if tt.res.err == nil {
|
||||
@ -1123,8 +1093,8 @@ func TestCommandSide_PasswordCodeSent(t *testing.T) {
|
||||
|
||||
func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -1320,7 +1290,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1383,12 +1353,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
@ -1406,7 +1371,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1474,12 +1439,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
@ -1502,7 +1462,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1572,12 +1532,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
},
|
||||
"$plain$x$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
@ -1595,7 +1550,195 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
authReq: &domain.AuthRequest{
|
||||
ID: "request1",
|
||||
AgentID: "agent1",
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "check password, ok, updated hash",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
domain.PasswordlessTypeNotAllowed,
|
||||
"",
|
||||
time.Hour*1,
|
||||
time.Hour*2,
|
||||
time.Hour*3,
|
||||
time.Hour*4,
|
||||
time.Hour*5,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordChangedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$v$password",
|
||||
false,
|
||||
"")),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordCheckSucceededEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&user.AuthRequestInfo{
|
||||
ID: "request1",
|
||||
UserAgentID: "agent1",
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordHashUpdatedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$x$password",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
password: "password",
|
||||
authReq: &domain.AuthRequest{
|
||||
ID: "request1",
|
||||
AgentID: "agent1",
|
||||
},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "regression test old version event",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewLoginPolicyAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
domain.PasswordlessTypeNotAllowed,
|
||||
"",
|
||||
time.Hour*1,
|
||||
time.Hour*2,
|
||||
time.Hour*3,
|
||||
time.Hour*4,
|
||||
time.Hour*5,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
&user.HumanPasswordChangedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
user.HumanPasswordChangedType,
|
||||
),
|
||||
Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "plain",
|
||||
KeyID: "",
|
||||
Crypted: []byte("$plain$v$password"),
|
||||
},
|
||||
ChangeRequired: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordCheckSucceededEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
&user.AuthRequestInfo{
|
||||
ID: "request1",
|
||||
UserAgentID: "agent1",
|
||||
},
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordHashUpdatedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"$plain$x$password",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1613,8 +1756,8 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
}
|
||||
err := r.HumanCheckPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.userID, tt.args.password, tt.args.authReq, tt.args.lockoutPolicy)
|
||||
if tt.res.err == nil {
|
||||
@ -1626,3 +1769,41 @@ func TestCommandSide_CheckPassword(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertPasswapErr(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{nil},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "mismatch",
|
||||
args: args{passwap.ErrPasswordMismatch},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(passwap.ErrPasswordMismatch, "COMMAND-3M0fs", "Errors.User.Password.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "no change",
|
||||
args: args{passwap.ErrPasswordNoChange},
|
||||
wantErr: caos_errs.ThrowPreconditionFailed(passwap.ErrPasswordNoChange, "COMMAND-Aesh5", "Errors.User.Password.NotChanged"),
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
args: args{io.ErrClosedPipe},
|
||||
wantErr: caos_errs.ThrowInternal(io.ErrClosedPipe, "COMMAND-CahN2", "Errors.Internal"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := convertPasswapErr(tt.args.err)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -26,11 +26,11 @@ import (
|
||||
|
||||
func TestCommandSide_AddHuman(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
newCode cryptoCodeFunc
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
newCode cryptoCodeFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -104,7 +104,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
newAddHumanEvent("$plain$x$password", true, ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -307,7 +307,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
newAddHumanEvent("$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -325,10 +325,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("userinit", time.Hour),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("userinit", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -383,7 +383,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
newAddHumanEvent("$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||
@ -403,10 +403,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -462,7 +462,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
newAddHumanEvent("$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||
@ -482,10 +482,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -542,7 +542,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
newAddHumanEvent("$plain$x$password", true, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -552,9 +552,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -611,7 +611,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
newAddHumanEvent("$plain$x$password", true, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -621,9 +621,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -680,7 +680,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
newAddHumanEvent("$plain$x$password", true, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -690,9 +690,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", false)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -743,9 +743,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -822,12 +822,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
"email@test.ch",
|
||||
true,
|
||||
)
|
||||
event.AddPasswordData(&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
}, true)
|
||||
event.AddPasswordData("$plain$x$password", true)
|
||||
return event
|
||||
}(),
|
||||
),
|
||||
@ -839,9 +834,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username@test.ch", "org1", false)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -899,7 +894,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, "+41711234567"),
|
||||
newAddHumanEvent("$plain$x$password", false, "+41711234567"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(
|
||||
@ -921,10 +916,10 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("phonecode", time.Hour),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newCode: mockCode("phonecode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1107,11 +1102,11 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
userEncryption: tt.fields.codeAlg,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
newCode: tt.fields.newCode,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
userEncryption: tt.fields.codeAlg,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
newCode: tt.fields.newCode,
|
||||
}
|
||||
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
|
||||
if tt.res.err == nil {
|
||||
@ -1133,9 +1128,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
|
||||
func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -1319,7 +1314,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", true, ""),
|
||||
newAddHumanEvent("$plain$x$password", true, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -1337,8 +1332,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1410,7 +1405,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
newAddHumanEvent("$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -1420,8 +1415,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1519,8 +1514,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1603,7 +1598,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
newAddHumanEvent("$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -1626,8 +1621,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1713,7 +1708,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, "+41711234567"),
|
||||
newAddHumanEvent("$plain$x$password", false, "+41711234567"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -1741,8 +1736,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1820,7 +1815,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, "+41711234567"),
|
||||
newAddHumanEvent("$plain$x$password", false, "+41711234567"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -1842,8 +1837,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -1973,8 +1968,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -2025,9 +2020,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
}
|
||||
gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless, tt.args.links, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
|
||||
if tt.res.err == nil {
|
||||
@ -2046,9 +2041,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
|
||||
func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
userPasswordHasher *crypto.PasswordHasher
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -2521,7 +2516,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("email@test.ch", "password", false, ""),
|
||||
newRegisterHumanEvent("email@test.ch", "$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -2539,8 +2534,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("email@test.ch", "org1", false)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -2632,7 +2627,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, ""),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -2650,8 +2645,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", false)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -2744,7 +2739,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, ""),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -2762,8 +2757,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -2856,7 +2851,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, ""),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
@ -2866,8 +2861,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -2962,7 +2957,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, "+41711234567"),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, "+41711234567"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -2990,8 +2985,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -3090,7 +3085,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, "+41711234567"),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, "+41711234567"),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanInitialCodeAddedEvent(context.Background(),
|
||||
@ -3112,8 +3107,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -3245,7 +3240,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newRegisterHumanEvent("username", "password", false, ""),
|
||||
newRegisterHumanEvent("username", "$plain$x$password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewUserIDPLinkAddedEvent(context.Background(),
|
||||
@ -3264,8 +3259,8 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUserIDPLinkUniqueConstraint("idpID", "externalID")),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordHasher: mockPasswordHasher("x"),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@ -3316,9 +3311,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||
}
|
||||
got, err := r.RegisterHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.link, tt.args.orgMemberRoles, tt.args.secretGenerator, tt.args.secretGenerator, tt.args.secretGenerator)
|
||||
if tt.res.err == nil {
|
||||
@ -3657,13 +3652,7 @@ func newAddHumanEvent(password string, changeRequired bool, phone string) *user.
|
||||
true,
|
||||
)
|
||||
if password != "" {
|
||||
event.AddPasswordData(&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte(password),
|
||||
},
|
||||
changeRequired)
|
||||
event.AddPasswordData(password, changeRequired)
|
||||
}
|
||||
if phone != "" {
|
||||
event.AddPhoneData(domain.PhoneNumber(phone))
|
||||
@ -3685,13 +3674,7 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
|
||||
true,
|
||||
)
|
||||
if password != "" {
|
||||
event.AddPasswordData(&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte(password),
|
||||
},
|
||||
changeRequired)
|
||||
event.AddPasswordData(password, changeRequired)
|
||||
}
|
||||
if phone != "" {
|
||||
event.AddPhoneData(domain.PhoneNumber(phone))
|
||||
@ -3706,7 +3689,7 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
type args struct {
|
||||
human *AddHuman
|
||||
orgID string
|
||||
passwordAlg crypto.HashAlgorithm
|
||||
hasher *crypto.PasswordHasher
|
||||
filter preparation.FilterToQueryReducer
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
allowInitMail bool
|
||||
@ -3763,6 +3746,24 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsupported password hash encoding",
|
||||
args: args{
|
||||
human: &AddHuman{
|
||||
Email: Email{Address: "support@zitadel.com", Verified: true},
|
||||
PreferredLanguage: language.English,
|
||||
FirstName: "gigi",
|
||||
LastName: "giraffe",
|
||||
EncodedPasswordHash: "$foo$x$password",
|
||||
Username: "username",
|
||||
},
|
||||
orgID: "ro",
|
||||
hasher: mockPasswordHasher("x"),
|
||||
},
|
||||
want: Want{
|
||||
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.User.Password.NotSupported"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid password",
|
||||
fields: fields{
|
||||
@ -3828,9 +3829,9 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
Password: "password",
|
||||
Username: "username",
|
||||
},
|
||||
orgID: "ro",
|
||||
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
orgID: "ro",
|
||||
hasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
filter: NewMultiFilter().Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{}, nil
|
||||
@ -3879,12 +3880,79 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
"support@zitadel.com",
|
||||
true,
|
||||
)
|
||||
event.AddPasswordData(&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: "hash",
|
||||
KeyID: "",
|
||||
Crypted: []byte("password"),
|
||||
}, false)
|
||||
event.AddPasswordData("$plain$x$password", false)
|
||||
return event
|
||||
}(),
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hashed password",
|
||||
fields: fields{
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||
},
|
||||
args: args{
|
||||
human: &AddHuman{
|
||||
Email: Email{Address: "support@zitadel.com", Verified: true},
|
||||
PreferredLanguage: language.English,
|
||||
FirstName: "gigi",
|
||||
LastName: "giraffe",
|
||||
EncodedPasswordHash: "$plain$x$password",
|
||||
Username: "username",
|
||||
},
|
||||
orgID: "ro",
|
||||
hasher: mockPasswordHasher("x"),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
filter: NewMultiFilter().Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewDomainPolicyAddedEvent(
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
}, nil
|
||||
}).
|
||||
Append(
|
||||
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
|
||||
return []eventstore.Event{
|
||||
org.NewPasswordComplexityPolicyAddedEvent(
|
||||
ctx,
|
||||
&org.NewAggregate("id").Aggregate,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
}, nil
|
||||
}).
|
||||
Filter(),
|
||||
},
|
||||
want: Want{
|
||||
Commands: []eventstore.Command{
|
||||
func() *user.HumanAddedEvent {
|
||||
event := user.NewHumanAddedEvent(
|
||||
context.Background(),
|
||||
&agg.Aggregate,
|
||||
"username",
|
||||
"gigi",
|
||||
"giraffe",
|
||||
"",
|
||||
"gigi giraffe",
|
||||
language.English,
|
||||
0,
|
||||
"support@zitadel.com",
|
||||
true,
|
||||
)
|
||||
event.AddPasswordData("$plain$x$password", false)
|
||||
return event
|
||||
}(),
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(), &agg.Aggregate),
|
||||
@ -3897,7 +3965,7 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
c := &Commands{
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
|
||||
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.hasher, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation {
|
||||
|
||||
func (c *Commands) VerifyMachineSecret(ctx context.Context, userID string, resourceOwner string, secret string) (*domain.ObjectDetails, error) {
|
||||
agg := user.NewAggregate(userID, resourceOwner)
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.userPasswordAlg))
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareVerifyMachineSecret(agg, secret, c.codeAlg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -529,8 +529,8 @@ func TestCommandSide_VerifyMachineSecret(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
userPasswordAlg: crypto.NewBCrypt(14),
|
||||
eventstore: tt.fields.eventstore,
|
||||
codeAlg: crypto.NewBCrypt(14),
|
||||
}
|
||||
got, err := r.VerifyMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secret)
|
||||
if tt.res.err == nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
type SystemDefaults struct {
|
||||
SecretGenerators SecretGenerators
|
||||
PasswordHasher crypto.PasswordHashConfig
|
||||
Multifactors MultifactorConfig
|
||||
DomainVerification DomainVerification
|
||||
Notifications Notifications
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
const (
|
||||
TypeEncryption CryptoType = iota
|
||||
TypeHash
|
||||
TypeHash // Depcrecated: use [passwap.Swapper] instead
|
||||
)
|
||||
|
||||
type Crypto interface {
|
||||
@ -26,6 +26,7 @@ type EncryptionAlgorithm interface {
|
||||
DecryptString(hashed []byte, keyID string) (string, error)
|
||||
}
|
||||
|
||||
// Depcrecated: use [passwap.Swapper] instead
|
||||
type HashAlgorithm interface {
|
||||
Crypto
|
||||
Hash(value []byte) ([]byte, error)
|
||||
|
209
internal/crypto/passwap.go
Normal file
209
internal/crypto/passwap.go
Normal file
@ -0,0 +1,209 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/zitadel/passwap"
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
"github.com/zitadel/passwap/verifier"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
type PasswordHasher struct {
|
||||
*passwap.Swapper
|
||||
Prefixes []string
|
||||
}
|
||||
|
||||
func (h *PasswordHasher) EncodingSupported(encodedHash string) bool {
|
||||
for _, prefix := range h.Prefixes {
|
||||
if strings.HasPrefix(encodedHash, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type HashName string
|
||||
|
||||
const (
|
||||
HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier
|
||||
HashNameArgon2i HashName = "argon2i" // hash only
|
||||
HashNameArgon2id HashName = "argon2id" // hash only
|
||||
HashNameBcrypt HashName = "bcrypt" // hash and verify
|
||||
HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNameScrypt HashName = "scrypt" // hash and verify
|
||||
)
|
||||
|
||||
type PasswordHashConfig struct {
|
||||
Verifiers []HashName
|
||||
Hasher HasherConfig
|
||||
}
|
||||
|
||||
func (c *PasswordHashConfig) PasswordHasher() (*PasswordHasher, error) {
|
||||
verifiers, vPrefixes, err := c.buildVerifiers()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(err, "CRYPT-sahW9", "password hash config invalid")
|
||||
}
|
||||
hasher, hPrefixes, err := c.Hasher.buildHasher()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(err, "CRYPT-Que4r", "password hash config invalid")
|
||||
}
|
||||
return &PasswordHasher{
|
||||
Swapper: passwap.NewSwapper(hasher, verifiers...),
|
||||
Prefixes: append(hPrefixes, vPrefixes...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type prefixVerifier struct {
|
||||
prefixes []string
|
||||
verifier verifier.Verifier
|
||||
}
|
||||
|
||||
// map HashNames to Verifier instances.
|
||||
var knowVerifiers = map[HashName]prefixVerifier{
|
||||
HashNameArgon2: {
|
||||
// only argon2i and argon2id are suppored.
|
||||
// The Prefix constant also covers argon2d.
|
||||
prefixes: []string{argon2.Prefix},
|
||||
verifier: argon2.Verifier,
|
||||
},
|
||||
HashNameBcrypt: {
|
||||
prefixes: []string{bcrypt.Prefix},
|
||||
verifier: bcrypt.Verifier,
|
||||
},
|
||||
HashNameMd5: {
|
||||
prefixes: []string{md5.Prefix},
|
||||
verifier: md5.Verifier,
|
||||
},
|
||||
HashNameScrypt: {
|
||||
prefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
verifier: scrypt.Verifier,
|
||||
},
|
||||
}
|
||||
|
||||
func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
|
||||
verifiers = make([]verifier.Verifier, len(c.Verifiers))
|
||||
prefixes = make([]string, 0, len(c.Verifiers)+1)
|
||||
for i, name := range c.Verifiers {
|
||||
v, ok := knowVerifiers[name]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("invalid verifier %q", name)
|
||||
}
|
||||
verifiers[i] = v.verifier
|
||||
prefixes = append(prefixes, v.prefixes...)
|
||||
}
|
||||
return verifiers, prefixes, nil
|
||||
}
|
||||
|
||||
type HasherConfig struct {
|
||||
Algorithm HashName
|
||||
Params map[string]any `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, err error) {
|
||||
switch c.Algorithm {
|
||||
case HashNameArgon2i:
|
||||
return c.argon2i()
|
||||
case HashNameArgon2id:
|
||||
return c.argon2id()
|
||||
case HashNameBcrypt:
|
||||
return c.bcrypt()
|
||||
case HashNameScrypt:
|
||||
return c.scrypt()
|
||||
case "":
|
||||
return nil, nil, fmt.Errorf("missing hasher algorithm")
|
||||
case HashNameArgon2, HashNameMd5:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HasherConfig) decodeParams(dst any) error {
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
ErrorUnused: true,
|
||||
ErrorUnset: true,
|
||||
Result: dst,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(c.Params)
|
||||
}
|
||||
|
||||
// argon2Params decodes [HasherConfig.Params] into a [argon2.Params] used as defaults.
|
||||
// p is passed a copy and therfore will not be modified.
|
||||
func (c *HasherConfig) argon2Params(p argon2.Params) (argon2.Params, error) {
|
||||
var dst struct {
|
||||
Time uint32 `mapstructure:"Time"`
|
||||
Memory uint32 `mapstructure:"Memory"`
|
||||
Threads uint8 `mapstructure:"Threads"`
|
||||
}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return argon2.Params{}, fmt.Errorf("decode argon2i params: %w", err)
|
||||
}
|
||||
p.Time = dst.Time
|
||||
p.Memory = dst.Memory
|
||||
p.Threads = dst.Threads
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) argon2i() (passwap.Hasher, []string, error) {
|
||||
p, err := c.argon2Params(argon2.RecommendedIParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return argon2.NewArgon2i(p), []string{argon2.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) argon2id() (passwap.Hasher, []string, error) {
|
||||
p, err := c.argon2Params(argon2.RecommendedIDParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return argon2.NewArgon2id(p), []string{argon2.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) bcryptCost() (int, error) {
|
||||
var dst = struct {
|
||||
Cost int `mapstructure:"Cost"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return 0, fmt.Errorf("decode bcrypt params: %w", err)
|
||||
}
|
||||
return dst.Cost, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) bcrypt() (passwap.Hasher, []string, error) {
|
||||
cost, err := c.bcryptCost()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return bcrypt.New(cost), []string{bcrypt.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) scryptParams() (scrypt.Params, error) {
|
||||
var dst = struct {
|
||||
Cost int `mapstructure:"Cost"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return scrypt.Params{}, fmt.Errorf("decode scrypt params: %w", err)
|
||||
}
|
||||
p := scrypt.RecommendedParams // copy
|
||||
p.N = 1 << dst.Cost
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) scrypt() (passwap.Hasher, []string, error) {
|
||||
p, err := c.scryptParams()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return scrypt.New(p), []string{scrypt.Prefix, scrypt.Prefix_Linux}, nil
|
||||
}
|
486
internal/crypto/passwap_test.go
Normal file
486
internal/crypto/passwap_test.go
Normal file
@ -0,0 +1,486 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
)
|
||||
|
||||
func TestPasswordHasher_EncodingSupported(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encodedHash string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty string, false",
|
||||
encodedHash: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "scrypt, false",
|
||||
encodedHash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "bcrypt, true",
|
||||
encodedHash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "argo2i, true",
|
||||
encodedHash: "$argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl1pKL2MsYy9VgU/E",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "argo2id, true",
|
||||
encodedHash: "$argon2d$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$CB0Du96aj3fQVcVSqb0LIA6Z6fpStjzjVkaC3RlpK9A",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &PasswordHasher{
|
||||
Prefixes: []string{bcrypt.Prefix, argon2.Prefix},
|
||||
}
|
||||
got := h.EncodingSupported(tt.encodedHash)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
type fields struct {
|
||||
Verifiers []HashName
|
||||
Hasher HasherConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantPrefixes []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "invalid verifier",
|
||||
fields: fields{
|
||||
Verifiers: []HashName{
|
||||
HashNameArgon2,
|
||||
HashNameBcrypt,
|
||||
HashNameMd5,
|
||||
HashNameScrypt,
|
||||
"foobar",
|
||||
},
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid hasher",
|
||||
fields: fields{
|
||||
Verifiers: []HashName{
|
||||
HashNameArgon2,
|
||||
HashNameBcrypt,
|
||||
HashNameMd5,
|
||||
HashNameScrypt,
|
||||
},
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: "foobar",
|
||||
Params: map[string]any{
|
||||
"cost": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing algorithm",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid md5",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameMd5,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid argon2",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2i, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2i,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2i, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2i,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "argon2id, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2id,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2id, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2id,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "bcrypt, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"foo": 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bcrypt, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 3,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "scrypt, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameScrypt,
|
||||
Params: map[string]any{
|
||||
"cost": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "scrypt, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameScrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 3,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &PasswordHashConfig{
|
||||
Verifiers: tt.fields.Verifiers,
|
||||
Hasher: tt.fields.Hasher,
|
||||
}
|
||||
got, err := c.PasswordHasher()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if tt.wantPrefixes != nil {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.wantPrefixes, got.Prefixes)
|
||||
encoded, err := got.Hash("password")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_decodeParams(t *testing.T) {
|
||||
type dst struct {
|
||||
A int
|
||||
B uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
params map[string]any
|
||||
want dst
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unused",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unset",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": "2",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
},
|
||||
want: dst{
|
||||
A: 1,
|
||||
B: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.params,
|
||||
}
|
||||
var got dst
|
||||
err := c.decodeParams(&got)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_argon2Params(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
type args struct {
|
||||
p argon2.Params
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want argon2.Params
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
p: argon2.RecommendedIDParams,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"time": 2,
|
||||
"memory": 256,
|
||||
"threads": 8,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
p: argon2.RecommendedIDParams,
|
||||
},
|
||||
want: argon2.Params{
|
||||
Time: 2,
|
||||
Memory: 256,
|
||||
Threads: 8,
|
||||
KeyLen: 32,
|
||||
SaltLen: 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.argon2Params(tt.args.p)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_bcryptCost(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"cost": 12,
|
||||
},
|
||||
},
|
||||
want: 12,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.bcryptCost()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_scryptParams(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want scrypt.Params
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"cost": 2,
|
||||
},
|
||||
},
|
||||
want: scrypt.Params{
|
||||
N: 4,
|
||||
R: 8,
|
||||
P: 1,
|
||||
KeyLen: 32,
|
||||
SaltLen: 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.scryptParams()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ type Human struct {
|
||||
Username string
|
||||
State UserState
|
||||
*Password
|
||||
*HashedPassword
|
||||
HashedPassword string
|
||||
*Profile
|
||||
*Email
|
||||
*Phone
|
||||
@ -103,10 +103,10 @@ func (u *Human) EnsureDisplayName() {
|
||||
u.DisplayName = u.Username
|
||||
}
|
||||
|
||||
func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm, onetime bool) error {
|
||||
func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, hasher *crypto.PasswordHasher, onetime bool) error {
|
||||
if u.Password != nil {
|
||||
u.Password.ChangeRequired = onetime
|
||||
return u.Password.HashPasswordIfExisting(policy, passwordAlg)
|
||||
return u.Password.HashPasswordIfExisting(policy, hasher)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -115,7 +115,7 @@ func (u *Human) IsInitialState(passwordless, externalIDPs bool) bool {
|
||||
if externalIDPs {
|
||||
return false
|
||||
}
|
||||
return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && (u.HashedPassword == nil || u.HashedPassword.SecretString == "")
|
||||
return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && u.HashedPassword == ""
|
||||
}
|
||||
|
||||
func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {
|
||||
|
@ -1,24 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type HashedPassword struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
SecretString string
|
||||
SecretCrypto *crypto.CryptoValue
|
||||
}
|
||||
|
||||
func NewHashedPassword(password, algorithm string) *HashedPassword {
|
||||
return &HashedPassword{
|
||||
SecretString: password,
|
||||
SecretCrypto: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeHash,
|
||||
Algorithm: algorithm,
|
||||
Crypted: []byte(password),
|
||||
},
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
SecretString string
|
||||
SecretCrypto *crypto.CryptoValue
|
||||
EncodedSecret string
|
||||
ChangeRequired bool
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ type PasswordCode struct {
|
||||
NotificationType NotificationType
|
||||
}
|
||||
|
||||
func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwordAlg crypto.HashAlgorithm) error {
|
||||
func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, hasher *crypto.PasswordHasher) error {
|
||||
if p.SecretString == "" {
|
||||
return nil
|
||||
}
|
||||
@ -40,11 +40,11 @@ func (p *Password) HashPasswordIfExisting(policy *PasswordComplexityPolicy, pass
|
||||
if err := policy.Check(p.SecretString); err != nil {
|
||||
return err
|
||||
}
|
||||
secret, err := crypto.Hash([]byte(p.SecretString), passwordAlg)
|
||||
encoded, err := hasher.Hash(p.SecretString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.SecretCrypto = secret
|
||||
p.EncodedSecret = encoded
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
type HumanPasswordReadModel struct {
|
||||
*eventstore.ReadModel
|
||||
|
||||
Secret *crypto.CryptoValue
|
||||
EncodedHash string
|
||||
SecretChangeRequired bool
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
@ -26,26 +26,21 @@ type HumanPasswordReadModel struct {
|
||||
UserState domain.UserState
|
||||
}
|
||||
|
||||
func (q *Queries) GetHumanPassword(ctx context.Context, orgID, userID string) (passwordHash []byte, algorithm string, err error) {
|
||||
func (q *Queries) GetHumanPassword(ctx context.Context, orgID, userID string) (encodedHash string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if userID == "" {
|
||||
return nil, "", errors.ThrowInvalidArgument(nil, "QUERY-4Mfsf", "Errors.User.UserIDMissing")
|
||||
return "", errors.ThrowInvalidArgument(nil, "QUERY-4Mfsf", "Errors.User.UserIDMissing")
|
||||
}
|
||||
existingPassword, err := q.passwordReadModel(ctx, userID, orgID)
|
||||
if err != nil {
|
||||
return nil, "", errors.ThrowInternal(nil, "QUERY-p1k1n2i", "Errors.User.NotFound")
|
||||
return "", errors.ThrowInternal(nil, "QUERY-p1k1n2i", "Errors.User.NotFound")
|
||||
}
|
||||
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
|
||||
return nil, "", errors.ThrowPreconditionFailed(nil, "QUERY-3n77z", "Errors.User.NotFound")
|
||||
return "", errors.ThrowPreconditionFailed(nil, "QUERY-3n77z", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
if existingPassword.Secret != nil && existingPassword.Secret.Crypted != nil {
|
||||
return existingPassword.Secret.Crypted, existingPassword.Secret.Algorithm, nil
|
||||
}
|
||||
|
||||
return nil, "", nil
|
||||
return existingPassword.EncodedHash, nil
|
||||
}
|
||||
|
||||
func (q *Queries) passwordReadModel(ctx context.Context, userID, resourceOwner string) (readModel *HumanPasswordReadModel, err error) {
|
||||
@ -77,11 +72,11 @@ func (wm *HumanPasswordReadModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanAddedEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanRegisteredEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanInitialCodeAddedEvent:
|
||||
@ -89,7 +84,7 @@ func (wm *HumanPasswordReadModel) Reduce() error {
|
||||
case *user.HumanInitializedCheckSucceededEvent:
|
||||
wm.UserState = domain.UserStateActive
|
||||
case *user.HumanPasswordChangedEvent:
|
||||
wm.Secret = e.Secret
|
||||
wm.EncodedHash = user.SecretOrEncodedHash(e.Secret, e.EncodedHash)
|
||||
wm.SecretChangeRequired = e.ChangeRequired
|
||||
wm.Code = nil
|
||||
wm.PasswordCheckFailedCount = 0
|
||||
@ -109,6 +104,8 @@ func (wm *HumanPasswordReadModel) Reduce() error {
|
||||
wm.PasswordCheckFailedCount = 0
|
||||
case *user.UserRemovedEvent:
|
||||
wm.UserState = domain.UserStateDeleted
|
||||
case *user.HumanPasswordHashUpdatedEvent:
|
||||
wm.EncodedHash = e.EncodedHash
|
||||
}
|
||||
}
|
||||
return wm.ReadModel.Reduce()
|
||||
@ -139,7 +136,8 @@ func (wm *HumanPasswordReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
user.UserV1PasswordCodeAddedType,
|
||||
user.UserV1EmailVerifiedType,
|
||||
user.UserV1PasswordCheckFailedType,
|
||||
user.UserV1PasswordCheckSucceededType).
|
||||
user.UserV1PasswordCheckSucceededType,
|
||||
user.UserV1PasswordHashUpdatedType).
|
||||
Builder()
|
||||
|
||||
if wm.ResourceOwner != "" {
|
||||
|
@ -17,6 +17,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
RegisterFilterEventMapper(AggregateType, UserV1PasswordCodeSentType, HumanPasswordCodeSentEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1PasswordCheckFailedType, HumanPasswordCheckFailedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1PasswordHashUpdatedType, eventstore.GenericEventMapper[HumanPasswordHashUpdatedEvent]).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1EmailChangedType, HumanEmailChangedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1EmailVerifiedType, HumanEmailVerifiedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, UserV1EmailVerificationFailedType, HumanEmailVerificationFailedEventMapper).
|
||||
|
@ -48,7 +48,10 @@ type HumanAddedEvent struct {
|
||||
Region string `json:"region,omitempty"`
|
||||
StreetAddress string `json:"streetAddress,omitempty"`
|
||||
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
@ -81,10 +84,10 @@ func (e *HumanAddedEvent) AddPhoneData(
|
||||
}
|
||||
|
||||
func (e *HumanAddedEvent) AddPasswordData(
|
||||
secret *crypto.CryptoValue,
|
||||
encoded string,
|
||||
changeRequired bool,
|
||||
) {
|
||||
e.Secret = secret
|
||||
e.EncodedHash = encoded
|
||||
e.ChangeRequired = changeRequired
|
||||
}
|
||||
|
||||
@ -149,8 +152,12 @@ type HumanRegisteredEvent struct {
|
||||
PostalCode string `json:"postalCode,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
StreetAddress string `json:"streetAddress,omitempty"`
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"` // legacy
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
func (e *HumanRegisteredEvent) Data() interface{} {
|
||||
@ -182,10 +189,10 @@ func (e *HumanRegisteredEvent) AddPhoneData(
|
||||
}
|
||||
|
||||
func (e *HumanRegisteredEvent) AddPasswordData(
|
||||
secret *crypto.CryptoValue,
|
||||
encoded string,
|
||||
changeRequired bool,
|
||||
) {
|
||||
e.Secret = secret
|
||||
e.EncodedHash = encoded
|
||||
e.ChangeRequired = changeRequired
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,10 @@ const (
|
||||
type HumanPasswordChangedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
// New events only use EncodedHash. However, the secret field
|
||||
// is preserved to handle events older than the switch to Passwap.
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired"`
|
||||
UserAgentID string `json:"userAgentID,omitempty"`
|
||||
}
|
||||
@ -42,7 +45,7 @@ func (e *HumanPasswordChangedEvent) UniqueConstraints() []*eventstore.EventUniqu
|
||||
func NewHumanPasswordChangedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
secret *crypto.CryptoValue,
|
||||
encodeHash string,
|
||||
changeRequired bool,
|
||||
userAgentID string,
|
||||
) *HumanPasswordChangedEvent {
|
||||
@ -52,7 +55,7 @@ func NewHumanPasswordChangedEvent(
|
||||
aggregate,
|
||||
HumanPasswordChangedType,
|
||||
),
|
||||
Secret: secret,
|
||||
EncodedHash: encodeHash,
|
||||
ChangeRequired: changeRequired,
|
||||
UserAgentID: userAgentID,
|
||||
}
|
||||
@ -268,3 +271,44 @@ func HumanPasswordCheckFailedEventMapper(event *repository.Event) (eventstore.Ev
|
||||
|
||||
return humanAdded, nil
|
||||
}
|
||||
|
||||
type HumanPasswordHashUpdatedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
}
|
||||
|
||||
func (e *HumanPasswordHashUpdatedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *HumanPasswordHashUpdatedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *HumanPasswordHashUpdatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||
e.BaseEvent = *base
|
||||
}
|
||||
|
||||
func NewHumanPasswordHashUpdatedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
encoded string,
|
||||
) *HumanPasswordHashUpdatedEvent {
|
||||
return &HumanPasswordHashUpdatedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
HumanPasswordCheckFailedType,
|
||||
),
|
||||
EncodedHash: encoded,
|
||||
}
|
||||
}
|
||||
|
||||
// SecretOrEncodedHash returns the legacy *crypto.CryptoValue if it is not nil.
|
||||
// orherwise it will returns the encoded hash string.
|
||||
func SecretOrEncodedHash(secret *crypto.CryptoValue, encoded string) string {
|
||||
if secret != nil {
|
||||
return string(secret.Crypted)
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ const (
|
||||
UserV1PasswordCodeSentType = userV1PasswordEventTypePrefix + "code.sent"
|
||||
UserV1PasswordCheckSucceededType = userV1PasswordEventTypePrefix + "check.succeeded"
|
||||
UserV1PasswordCheckFailedType = userV1PasswordEventTypePrefix + "check.failed"
|
||||
UserV1PasswordHashUpdatedType = userV1PasswordEventTypePrefix + "hash.updated"
|
||||
|
||||
userV1EmailEventTypePrefix = userEventTypePrefix + "email."
|
||||
UserV1EmailChangedType = userV1EmailEventTypePrefix + "changed"
|
||||
|
@ -122,6 +122,8 @@ Errors:
|
||||
Empty: Паролата е празна
|
||||
Invalid: Паролата е невалидна
|
||||
NotSet: Потребителят не е задал парола
|
||||
NotChanged: Паролата не е променена
|
||||
NotSupported: Хеш кодирането на паролата не се поддържа
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Политиката за парола не е намерена
|
||||
MinLength: Паролата е твърде кратка
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: Passwort ist leer
|
||||
Invalid: Passwort ungültig
|
||||
NotSet: Benutzer hat kein Passwort gesetzt
|
||||
NotChanged: Passwort nicht geändert
|
||||
NotSupported: Passwort-Hash-Kodierung wird nicht unterstützt
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Passwort Policy konnte nicht gefunden werden
|
||||
MinLength: Passwort ist zu kurz
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: Password is empty
|
||||
Invalid: Password is invalid
|
||||
NotSet: User has not set a password
|
||||
NotChanged: Password not changed
|
||||
NotSupported: Password hash encoding not supported
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Password policy not found
|
||||
MinLength: Password is too short
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: La contraseña está vacía
|
||||
Invalid: La contraseña no es válida
|
||||
NotSet: El usuario no ha establecido una contraseña
|
||||
NotChanged: Contraseña no modificada
|
||||
NotSupported: No se admite la codificación hash de contraseña
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Política de contraseñas no encontrada
|
||||
MinLength: La contraseña es demasiado corta
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: Le mot de passe est vide
|
||||
Invalid: Le mot de passe n'est pas valide
|
||||
NotSet: L'utilisateur n'a pas défini de mot de passe
|
||||
NotChanged: Mot de passe non modifié
|
||||
NotSupported: Encodage de hachage de mot de passe non pris en charge
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Politique de mot de passe non trouvée
|
||||
MinLength: Le mot de passe est trop court
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: La password è vuota
|
||||
Invalid: La password non è valida
|
||||
NotSet: L'utente non ha impostato una password
|
||||
NotChanged: Password non modificata
|
||||
NotSupported: Codifica hash password non supportata
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Impostazioni di complessità password non trovati
|
||||
MinLength: La password è troppo corta
|
||||
|
@ -112,6 +112,8 @@ Errors:
|
||||
Empty: パスワードは空です
|
||||
Invalid: 無効なパスワードです
|
||||
NotSet: パスワードが未設置です
|
||||
NotChanged: パスワードは変更されていません
|
||||
NotSupported: パスワードハッシュエンコードはサポートされていません
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: パスワードポリシーが見つかりません
|
||||
MinLength: パスワードが短すぎます
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: Лозинката е празна
|
||||
Invalid: Невалидна лозинка
|
||||
NotSet: Корисникот нема поставено лозинка
|
||||
NotChanged: Лозинката не е променета
|
||||
NotSupported: Не е поддржано хаш-кодирањето на лозинката
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Политиката за комплексност на лозинката не е пронајдена
|
||||
MinLength: Лозинката е прекратка
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: Hasło jest puste
|
||||
Invalid: Hasło jest nieprawidłowe
|
||||
NotSet: Użytkownik nie ustawił hasła
|
||||
NotChanged: Hasło nie zostało zmienione
|
||||
NotSupported: Kodowanie skrótu hasła nie jest obsługiwane
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Polityka hasła nie znaleziona
|
||||
MinLength: Hasło jest zbyt krótkie
|
||||
|
@ -120,6 +120,8 @@ Errors:
|
||||
Empty: 密码为空
|
||||
Invalid: 密码无效
|
||||
NotSet: 用户未设置密码
|
||||
NotChanged: 密码未更改
|
||||
NotSupported: 不支持密码哈希编码
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: 未找到密码策略
|
||||
MinLength: 密码太短
|
||||
|
@ -15,6 +15,7 @@ type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Secret *crypto.CryptoValue `json:"secret,omitempty"`
|
||||
EncodedHash string `json:"encodedHash,omitempty"`
|
||||
ChangeRequired bool `json:"changeRequired,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,6 @@ func (u *NotifyUser) setPasswordData(event *models.Event) error {
|
||||
logging.Log("MODEL-dfhw6").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-BHFD2", "could not unmarshal data")
|
||||
}
|
||||
u.PasswordSet = password.Secret != nil
|
||||
u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
|
||||
return nil
|
||||
}
|
||||
|
@ -380,7 +380,7 @@ func (u *UserView) setPasswordData(event *models.Event) error {
|
||||
logging.Log("MODEL-sdw4r").WithError(err).Error("could not unmarshal event data")
|
||||
return errors.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.PasswordSet = password.Secret != nil
|
||||
u.PasswordSet = password.Secret != nil || password.EncodedHash != ""
|
||||
u.PasswordInitRequired = !u.PasswordSet
|
||||
u.PasswordChangeRequired = password.ChangeRequired
|
||||
u.PasswordChanged = event.CreationDate
|
||||
|
@ -7406,8 +7406,14 @@ message ImportHumanUserRequest {
|
||||
description: "Use this to import hashed passwords from another system."
|
||||
}
|
||||
};
|
||||
string value = 1;
|
||||
string algorithm = 2;
|
||||
string value = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm\"";
|
||||
description: "Encoded hash of a password in Modular Crypt Format: https://passlib.readthedocs.io/en/stable/modular_crypt_format.html"
|
||||
}
|
||||
];
|
||||
reserved 2; // was algortithm, which is actually obtained from the encoded hash
|
||||
reserved "algortithm";
|
||||
}
|
||||
message IDP {
|
||||
string config_id = 1 [
|
||||
|
@ -32,17 +32,7 @@ message HashedPassword {
|
||||
max_length: 200;
|
||||
}
|
||||
];
|
||||
string algorithm = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200, const: "bcrypt"},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"bcrypt\"";
|
||||
description: "\"algorithm used for the hash. currently only bcrypt is supported\"";
|
||||
min_length: 1,
|
||||
max_length: 200;
|
||||
}
|
||||
];
|
||||
bool change_required = 3;
|
||||
bool change_required = 2;
|
||||
}
|
||||
|
||||
message SendPasswordResetLink {
|
||||
|
Loading…
x
Reference in New Issue
Block a user