feat(login): add OTP (email and sms) (#6353)

* feat: login with otp

* fix(i18n): japanese translation

* add missing files

* fix provider change

* add event types translations to en

* add tests

* resourceOwner

* remove unused handler

* fix: secret generators and add comments

* add setup step

* rename

* linting

* fix setup

* improve otp handling

* fix autocomplete

* translations for login and notifications

* translations for event types

* changes from review

* check selected mfa type
This commit is contained in:
Livio Spring
2023-08-15 14:47:05 +02:00
committed by GitHub
parent faa9ed4de9
commit 7c494fd219
76 changed files with 3203 additions and 88 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -106,6 +107,14 @@ func (u *userNotifier) reducers() []handler.AggregateReducer {
Event: user.HumanPasswordChangedType,
Reduce: u.reducePasswordChanged,
},
{
Event: user.HumanOTPSMSCodeAddedType,
Reduce: u.reduceOTPSMSCodeAdded,
},
{
Event: user.HumanOTPEmailCodeAddedType,
Reduce: u.reduceOTPEmailCodeAdded,
},
},
},
}
@@ -332,6 +341,136 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler
return crdb.NewNoOpStatement(e), nil
}
func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanOTPSMSCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType)
}
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.HumanOTPSMSCodeAddedType, user.HumanOTPSMSCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
if err != nil {
return nil, err
}
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
if err != nil {
return nil, err
}
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifySMSOTPMessageType)
if err != nil {
return nil, err
}
ctx, origin, err := u.queries.Origin(ctx)
if err != nil {
return nil, err
}
notify := types.SendSMSTwilio(
ctx,
translator,
notifyUser,
u.queries.GetTwilioConfig,
u.queries.GetFileSystemProvider,
u.queries.GetLogProvider,
colors,
u.assetsPrefix(ctx),
e,
u.metricSuccessfulDeliveriesSMS,
u.metricFailedDeliveriesSMS,
)
err = notify.SendOTPSMSCode(authz.GetInstance(ctx).RequestedDomain(), origin, code, e.Expiry)
if err != nil {
return nil, err
}
err = u.commands.HumanOTPSMSCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanOTPEmailCodeAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType)
}
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.HumanOTPEmailCodeAddedType, user.HumanOTPEmailCodeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
if err != nil {
return nil, err
}
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
if err != nil {
return nil, err
}
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailOTPMessageType)
if err != nil {
return nil, err
}
ctx, origin, err := u.queries.Origin(ctx)
if err != nil {
return nil, err
}
var authRequestID string
if e.AuthRequestInfo != nil {
authRequestID = e.AuthRequestInfo.ID
}
notify := types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
u.queries.GetSMTPConfig,
u.queries.GetFileSystemProvider,
u.queries.GetLogProvider,
colors,
u.assetsPrefix(ctx),
e,
u.metricSuccessfulDeliveriesEmail,
u.metricFailedDeliveriesEmail,
)
err = notify.SendOTPEmailCode(notifyUser, authz.GetInstance(ctx).RequestedDomain(), origin, code, authRequestID, e.Expiry)
if err != nil {
return nil, err
}
err = u.commands.HumanOTPEmailCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
if err != nil {
return nil, err
}
return crdb.NewNoOpStatement(e), nil
}
func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.DomainClaimedEvent)
if !ok {
@@ -583,7 +722,7 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
e,
u.metricSuccessfulDeliveriesSMS,
u.metricFailedDeliveriesSMS,
).SendPhoneVerificationCode(notifyUser, origin, code)
).SendPhoneVerificationCode(notifyUser, origin, code, authz.GetInstance(ctx).RequestedDomain())
if err != nil {
return nil, err
}

View File

@@ -40,7 +40,10 @@ VerifyEmailOTP:
Text: Моля, използвай бутона 'Удостовери' или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
ButtonText: Удостовери
VerifySMSOTP:
Text: Моля, посети {{ .VerifyURL }} или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
Text: >-
{{.OTP}} е вашата еднократна парола за {{ .Domain }}. Използвайте го в рамките на следващия {{.Expiry}}.
@{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Домейнът е заявен
PreHeader: Промяна на имейл/потребителско име

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Bitte nutze den 'Authentifizieren'-Button oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
ButtonText: Authentifizieren
VerifySMSOTP:
Text: Bitte besuche {{ .VerifyURL }} oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
Text: >-
{{.OTP}} ist Ihr Einmalpasswort für {{ .Domain }}. Verwenden Sie es innerhalb der nächsten {{.Expiry}}.
@{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domain wurde beansprucht
PreHeader: Email / Username ändern

View File

@@ -31,10 +31,13 @@ VerifyEmailOTP:
PreHeader: Verify One-Time Password
Subject: Verify One-Time Password
Greeting: Hello {{.DisplayName}},
Text: Please use the "Authenticate" button or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
Text: Please use the one-time password {{.OTP}} to authenticate at ZITADEL within the next five minutes or click the "Authenticate" button.
ButtonText: Authenticate
VerifySMSOTP:
Text: Please visit {{ .VerifyURL }} or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
Text: >-
{{.OTP}} is your one-time-password for {{ .Domain }}. Use it within the next {{.Expiry}}.
@{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domain has been claimed
PreHeader: Change email / username

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Por favor, utiliza el botón 'Autenticar' o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
ButtonText: Autenticar
VerifySMSOTP:
Text: Por favor, visita {{ .VerifyURL }} o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
Text: >-
{{.OTP}} es su contraseña de un solo uso para {{ .Domain }}. Úselo dentro de los próximos {{.Expiry}}.
@{{.Dominio}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Se ha reclamado un dominio
PreHeader: Cambiar dirección de correo electrónico / nombre de usuario

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Utilise le bouton 'Authentifier' ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
ButtonText: Authentifier
VerifySMSOTP:
Text: Visite {{ .VerifyURL }} ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
Text: >-
{{.OTP}} est votre mot de passe à usage unique pour {{ .Domain }}. Utilisez-le dans le prochain {{.Expiry}}.
@{{.Domaine}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Le domaine a été réclamé
PreHeader: Modifier l'email / le nom d'utilisateur

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Per favore, utilizza il pulsante 'Autentica' o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
ButtonText: Autentica
VerifySMSOTP:
Text: Per favore, visita {{ .VerifyURL }} o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
Text: >-
{{.OTP}} è la tua password monouso per {{ .Domain }}. Usalo entro il prossimo {{.Expiry}}.
@{{.Dominio}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Il dominio è stato rivendicato
PreHeader: Cambiare email / nome utente

View File

@@ -31,10 +31,13 @@ VerifyEmailOTP:
PreHeader: ワンタイムパスワードを確認する
Subject: ワンタイムパスワードを確認する
Greeting: こんにちは、{{.DisplayName}}さん
Text: '認証'ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
Text: 認証ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
ButtonText: 認証
VerifySMSOTP:
Text: {{ .VerifyURL }} を訪れるか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
Text: >-
{{.OTP}} は、{{ .Domain }} のワンタイムパスワードです。次の {{.Expiry}} 以内に使用してください。
@{{.ドメイン}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - ドメインの登録
PreHeader: メールアドレス・ユーザー名の変更

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Ве молам, користи го копчето 'Автентицирај' или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
ButtonText: Автентицирај
VerifySMSOTP:
Text: Ве молам, посети го {{ .VerifyURL }} или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
Text: >-
{{.OTP}} е вашата еднократна лозинка за {{ .Домен }}. Користете го во следниот {{.Истек}}.
@{{.Домен}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Доменот е преземен
PreHeader: Промена на е-пошта / корисничко име

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Proszę, użyj przycisku 'Uwierzytelnij' lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
ButtonText: Uwierzytelnij
VerifySMSOTP:
Text: Proszę, odwiedź {{ .VerifyURL }} lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
Text: >-
{{.OTP}} to Twoje jednorazowe hasło do domeny {{ .Domain }}. Użyj go w ciągu najbliższych {{.Expiry}}.
@{{.Domena}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domena została zarejestrowana
PreHeader: Zmiana adresu e-mail / nazwy użytkownika

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: Por favor, usa o botão 'Autenticar' ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
ButtonText: Autenticar
VerifySMSOTP:
Text: Por favor, visita {{ .VerifyURL }} ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
Text: >-
{{.OTP}} é sua senha única para {{ .Domain }}. Use-o nos próximos {{.Expiry}}.
@{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - Domínio foi reivindicado
PreHeader: Alterar e-mail / nome de usuário

View File

@@ -34,7 +34,10 @@ VerifyEmailOTP:
Text: 请使用 '验证' 按钮,或复制一次性密码 {{.OTP}} 并将其粘贴到验证屏幕中,以在接下来的五分钟内在 ZITADEL 中进行验证。
ButtonText: 验证
VerifySMSOTP:
Text: 请访问 {{ .VerifyURL }} 或复制一次性密码 {{.OTP}} 并将其粘贴到身份验证屏幕以在接下来的五分钟内在ZITADEL进行身份验证。
Text: >-
{{.OTP}} 是您的 {{ .Domain }} 的一次性密码。在下一个 {{.Expiry}} 内使用它。
@{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: ZITADEL - 域名所有权验证
PreHeader: 更改电子邮件/用户名

View File

@@ -0,0 +1,29 @@
package types
import (
"time"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendOTPSMSCode(requestedDomain, origin, code string, expiry time.Duration) error {
args := otpArgs(code, origin, requestedDomain, expiry)
return notify("", args, domain.VerifySMSOTPMessageType, false)
}
func (notify Notify) SendOTPEmailCode(user *query.NotifyUser, requestedDomain, origin, code, authRequestID string, expiry time.Duration) error {
url := login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail)
args := otpArgs(code, origin, requestedDomain, expiry)
return notify(url, args, domain.VerifyEmailOTPMessageType, false)
}
func otpArgs(code, origin, requestedDomain string, expiry time.Duration) map[string]interface{} {
args := make(map[string]interface{})
args["OTP"] = code
args["Origin"] = origin
args["Domain"] = requestedDomain
args["Expiry"] = expiry
return args
}

View File

@@ -5,8 +5,9 @@ import (
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error {
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code, requestedDomain string) error {
args := make(map[string]interface{})
args["Code"] = code
args["Domain"] = requestedDomain
return notify("", args, domain.VerifyPhoneMessageType, true)
}