mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +00:00
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:
@@ -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
|
||||
}
|
||||
|
@@ -40,7 +40,10 @@ VerifyEmailOTP:
|
||||
Text: Моля, използвай бутона 'Удостовери' или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
|
||||
ButtonText: Удостовери
|
||||
VerifySMSOTP:
|
||||
Text: Моля, посети {{ .VerifyURL }} или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
|
||||
Text: >-
|
||||
{{.OTP}} е вашата еднократна парола за {{ .Domain }}. Използвайте го в рамките на следващия {{.Expiry}}.
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Домейнът е заявен
|
||||
PreHeader: Промяна на имейл/потребителско име
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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: メールアドレス・ユーザー名の変更
|
||||
|
@@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Ве молам, користи го копчето 'Автентицирај' или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
|
||||
ButtonText: Автентицирај
|
||||
VerifySMSOTP:
|
||||
Text: Ве молам, посети го {{ .VerifyURL }} или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
|
||||
Text: >-
|
||||
{{.OTP}} е вашата еднократна лозинка за {{ .Домен }}. Користете го во следниот {{.Истек}}.
|
||||
|
||||
@{{.Домен}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Доменот е преземен
|
||||
PreHeader: Промена на е-пошта / корисничко име
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: 请使用 '验证' 按钮,或复制一次性密码 {{.OTP}} 并将其粘贴到验证屏幕中,以在接下来的五分钟内在 ZITADEL 中进行验证。
|
||||
ButtonText: 验证
|
||||
VerifySMSOTP:
|
||||
Text: 请访问 {{ .VerifyURL }} 或复制一次性密码 {{.OTP}} 并将其粘贴到身份验证屏幕,以在接下来的五分钟内在ZITADEL进行身份验证。
|
||||
Text: >-
|
||||
{{.OTP}} 是您的 {{ .Domain }} 的一次性密码。在下一个 {{.Expiry}} 内使用它。
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - 域名所有权验证
|
||||
PreHeader: 更改电子邮件/用户名
|
||||
|
29
internal/notification/types/otp.go
Normal file
29
internal/notification/types/otp.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user