mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-14 20:08:02 +00:00
07b2bac463
# Which Problems Are Solved User created through the User V2 API without any authentication method and possibly unverified email address was not able to login through the current hosted login UI. An unverified email address would result in a mail verification and not an initialization mail like it would with the management API. Also the login UI would then require the user to enter the init code, which the user never received. # How the Problems Are Solved - When verifying the email through the login UI, it will check for existing auth methods (password, IdP, passkeys). In case there are none, the user will be prompted to set a password. - When a user was created through the V2 API with a verified email and no auth method, the user will be prompted to set a password in the login UI. - Since setting a password requires a corresponding code, the code will be generated and sent when login in. # Additional Changes - Changed `RequestSetPassword` to get the codeGenerator from the eventstore instead of getting it from query. # Additional Context - closes https://github.com/zitadel/zitadel/issues/6600 - closes https://github.com/zitadel/zitadel/issues/8235 --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
175 lines
6.7 KiB
Go
175 lines
6.7 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, emailCodeGenerator crypto.Generator) (*domain.Email, error) {
|
|
if email.AggregateID == "" {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing")
|
|
}
|
|
if err := email.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingEmail, err := c.emailWriteModel(ctx, email.AggregateID, email.ResourceOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-0Pe4r", "Errors.User.Email.NotFound")
|
|
}
|
|
if existingEmail.UserState == domain.UserStateInitial {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-J8dsk", "Errors.User.NotInitialised")
|
|
}
|
|
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
|
changedEvent, hasChanged := existingEmail.NewChangedEvent(ctx, userAgg, email.EmailAddress)
|
|
|
|
// only continue if there were changes or there were no changes and the email should be set to verified
|
|
if !hasChanged && !(email.IsEmailVerified && existingEmail.IsEmailVerified != email.IsEmailVerified) {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2b7fM", "Errors.User.Email.NotChanged")
|
|
}
|
|
|
|
events := make([]eventstore.Command, 0)
|
|
if hasChanged {
|
|
events = append(events, changedEvent)
|
|
}
|
|
if email.IsEmailVerified {
|
|
events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
|
|
} else {
|
|
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, ""))
|
|
}
|
|
|
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingEmail, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToEmail(existingEmail), nil
|
|
}
|
|
|
|
func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner, optionalPassword, optionalUserAgentID string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
|
|
if userID == "" {
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
|
|
}
|
|
if code == "" {
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-çm0ds", "Errors.User.Code.Empty")
|
|
}
|
|
|
|
existingCode, err := c.emailWriteModel(ctx, userID, resourceowner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
|
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3n8ud", "Errors.User.Code.NotFound")
|
|
}
|
|
|
|
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
|
|
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, emailCodeGenerator.Alg())
|
|
if err != nil {
|
|
_, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, userAgg))
|
|
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
|
|
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid")
|
|
}
|
|
commands := []eventstore.Command{
|
|
user.NewHumanEmailVerifiedEvent(ctx, userAgg),
|
|
}
|
|
if optionalPassword != "" {
|
|
passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, optionalPassword, "", optionalUserAgentID, false, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commands = append(commands, passwordCommand)
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx, commands...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingCode, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingCode.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string, emailCodeGenerator crypto.Generator, authRequestID string) (*domain.ObjectDetails, error) {
|
|
if userID == "" {
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
|
|
}
|
|
|
|
existingEmail, err := c.emailWriteModel(ctx, userID, resourceOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
|
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-0Pe4r", "Errors.User.Email.NotFound")
|
|
}
|
|
if existingEmail.UserState == domain.UserStateInitial {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-E3fbw", "Errors.User.NotInitialised")
|
|
}
|
|
if existingEmail.IsEmailVerified {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified")
|
|
}
|
|
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
|
emailCode, _, err := domain.NewEmailCode(emailCodeGenerator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if authRequestID == "" {
|
|
authRequestID = existingEmail.AuthRequestID
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, authRequestID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingEmail, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingEmail.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) {
|
|
if userID == "" {
|
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9fs", "Errors.IDMissing")
|
|
}
|
|
existingEmail, err := c.emailWriteModel(ctx, userID, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
|
|
return zerrors.ThrowNotFound(nil, "COMMAND-6n8uH", "Errors.User.Email.NotFound")
|
|
}
|
|
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
|
|
_, err = c.eventstore.Push(ctx, user.NewHumanEmailCodeSentEvent(ctx, userAgg))
|
|
return err
|
|
}
|
|
|
|
func (c *Commands) emailWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanEmailWriteModel, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
writeModel = NewHumanEmailWriteModel(userID, resourceOwner)
|
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModel, nil
|
|
}
|