feat(api): add and remove OTP (SMS and email) (#6295)

* refactor: rename otp to totp

* feat: add otp sms and email

* implement tests
This commit is contained in:
Livio Spring
2023-08-02 18:57:53 +02:00
committed by GitHub
parent ca13e70c92
commit a1942ecdaa
44 changed files with 2253 additions and 215 deletions

View File

@@ -11,12 +11,11 @@ import (
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error {
encryptedSecret, err := crypto.Encrypt([]byte(key), c.multifactors.OTP.CryptoMFA)
if err != nil {
return err
@@ -25,7 +24,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
return err
}
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner)
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
@@ -41,7 +40,7 @@ func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, reso
return err
}
func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) {
func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
@@ -49,21 +48,19 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string
if err != nil {
return nil, err
}
_, err = c.eventstore.Push(ctx, prep.cmds...)
err = c.pushAppendAndReduce(ctx, prep.wm, prep.cmds...)
if err != nil {
return nil, err
}
return &domain.OTP{
ObjectRoot: models.ObjectRoot{
AggregateID: prep.userAgg.ID,
},
SecretString: prep.key.Secret(),
Url: prep.key.URL(),
return &domain.TOTP{
ObjectDetails: writeModelToObjectDetails(&prep.wm.WriteModel),
Secret: prep.key.Secret(),
URI: prep.key.URL(),
}, nil
}
type preparedTOTP struct {
wm *HumanOTPWriteModel
wm *HumanTOTPWriteModel
userAgg *eventstore.Aggregate
key *otp.Key
cmds []eventstore.Command
@@ -72,21 +69,21 @@ type preparedTOTP struct {
func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner string) (*preparedTOTP, error) {
human, err := c.getHuman(ctx, userID, resourceOwner)
if err != nil {
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-MM9fs", "Errors.User.NotFound")
}
org, err := c.getOrg(ctx, human.ResourceOwner)
if err != nil {
logging.Log("COMMAND-Cm0ds").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-55M9f", "Errors.Org.NotFound")
}
orgPolicy, err := c.getOrgDomainPolicy(ctx, org.AggregateID)
if err != nil {
logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound")
}
otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@@ -103,7 +100,7 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
if issuer == "" {
issuer = authz.GetInstance(ctx).RequestedDomain()
}
key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
key, secret, err := domain.NewTOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
@@ -117,12 +114,12 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
}, nil
}
func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return nil, err
}
@@ -132,7 +129,7 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
if existingOTP.State == domain.MFAStateReady {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-qx4ls", "Errors.Users.MFA.OTP.AlreadyReady")
}
if err := domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
if err := domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA); err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
@@ -148,11 +145,11 @@ func (c *Commands) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, user
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
@@ -160,22 +157,22 @@ func (c *Commands) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceo
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
err = domain.VerifyMFAOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
err = domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
if err == nil {
_, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
return err
}
_, pushErr := c.eventstore.Push(ctx, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
logging.Log("COMMAND-9fj7s").OnError(pushErr).Error("error create password check failed event")
logging.OnError(pushErr).Error("error create password check failed event")
return err
}
func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpWriteModelByID(ctx, userID, resourceOwner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@@ -194,11 +191,128 @@ func (c *Commands) HumanRemoveOTP(ctx context.Context, userID, resourceOwner str
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) otpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPWriteModel, err error) {
func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing")
}
if err := authz.UserIDInCTX(ctx, userID); err != nil {
return nil, err
}
otpWriteModel, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if otpWriteModel.otpAdded {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady")
}
if !otpWriteModel.phoneVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Q54j2", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPSMSAddedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
}
func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3br2", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
return nil, err
}
}
if !existingOTP.otpAdded {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Sr3h3", "Errors.User.MFA.OTP.NotExisting")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPSMSRemovedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing")
}
otpWriteModel, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if otpWriteModel.otpAdded {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady")
}
if !otpWriteModel.emailVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-KLJ2d", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel)
if err = c.pushAppendAndReduce(ctx, otpWriteModel, user.NewHumanOTPEmailAddedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
}
func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-S2h11", "Errors.User.UserIDMissing")
}
existingOTP, err := c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
return nil, err
}
}
if !existingOTP.otpAdded {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-b312D", "Errors.User.MFA.OTP.NotExisting")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
if err = c.pushAppendAndReduce(ctx, existingOTP, user.NewHumanOTPEmailRemovedEvent(ctx, userAgg)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPWriteModel(userID, resourceOwner)
writeModel = NewHumanTOTPWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) otpSMSWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPSMSWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPSMSWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPEmailWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err