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:
Tim Möhlmann 2023-07-14 09:49:57 +03:00 committed by GitHub
parent 6fcfa63f54
commit 4589ddad4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1853 additions and 775 deletions

View File

@ -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

View File

@ -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,

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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,
}
}
}

View File

@ -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{

View File

@ -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{

View File

@ -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) {

View File

@ -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))

View File

@ -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)

View File

@ -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),
)
}

View File

@ -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$"},
}
}

View File

@ -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 {

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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"),
},

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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")
}

View File

@ -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 != "" {

View File

@ -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)
})
}
}

View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -8,6 +8,7 @@ import (
type SystemDefaults struct {
SecretGenerators SecretGenerators
PasswordHasher crypto.PasswordHashConfig
Multifactors MultifactorConfig
DomainVerification DomainVerification
Notifications Notifications

View File

@ -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
View 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
}

View 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)
})
}
}

View File

@ -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) {

View File

@ -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),
},
}
}

View File

@ -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
}

View File

@ -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 != "" {

View File

@ -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).

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -122,6 +122,8 @@ Errors:
Empty: Паролата е празна
Invalid: Паролата е невалидна
NotSet: Потребителят не е задал парола
NotChanged: Паролата не е променена
NotSupported: Хеш кодирането на паролата не се поддържа
PasswordComplexityPolicy:
NotFound: Политиката за парола не е намерена
MinLength: Паролата е твърде кратка

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -112,6 +112,8 @@ Errors:
Empty: パスワードは空です
Invalid: 無効なパスワードです
NotSet: パスワードが未設置です
NotChanged: パスワードは変更されていません
NotSupported: パスワードハッシュエンコードはサポートされていません
PasswordComplexityPolicy:
NotFound: パスワードポリシーが見つかりません
MinLength: パスワードが短すぎます

View File

@ -120,6 +120,8 @@ Errors:
Empty: Лозинката е празна
Invalid: Невалидна лозинка
NotSet: Корисникот нема поставено лозинка
NotChanged: Лозинката не е променета
NotSupported: Не е поддржано хаш-кодирањето на лозинката
PasswordComplexityPolicy:
NotFound: Политиката за комплексност на лозинката не е пронајдена
MinLength: Лозинката е прекратка

View File

@ -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

View File

@ -120,6 +120,8 @@ Errors:
Empty: 密码为空
Invalid: 密码无效
NotSet: 用户未设置密码
NotChanged: 密码未更改
NotSupported: 不支持密码哈希编码
PasswordComplexityPolicy:
NotFound: 未找到密码策略
MinLength: 密码太短

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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

View File

@ -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 [

View File

@ -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 {