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
56 changed files with 1853 additions and 775 deletions

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