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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 3203 additions and 88 deletions

26
cmd/setup/12.go Normal file
View File

@ -0,0 +1,26 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed 12/12_add_otp_columns.sql
addOTPColumns string
)
type AddOTPColumns struct {
dbClient *database.DB
}
func (mig *AddOTPColumns) Execute(ctx context.Context) error {
_, err := mig.dbClient.ExecContext(ctx, addOTPColumns)
return err
}
func (mig *AddOTPColumns) String() string {
return "12_auth_users_otp_columns"
}

View File

@ -0,0 +1,2 @@
ALTER TABLE auth.users2 ADD COLUMN otp_sms_added BOOL DEFAULT false;
ALTER TABLE auth.users2 ADD COLUMN otp_email_added BOOL DEFAULT false;

View File

@ -67,6 +67,7 @@ type Steps struct {
s9EventstoreIndexes2 *EventstoreIndexesNew
CorrectCreationDate *CorrectCreationDate
AddEventCreatedAt *AddEventCreatedAt
s12AddOTPColumns *AddOTPColumns
}
type encryptionKeyConfig struct {

View File

@ -94,6 +94,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.CorrectCreationDate.dbClient = dbClient
steps.AddEventCreatedAt.dbClient = dbClient
steps.AddEventCreatedAt.step10 = steps.CorrectCreationDate
steps.s12AddOTPColumns = &AddOTPColumns{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@ -134,6 +135,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
logging.OnError(err).Fatal("unable to migrate step 10")
err = migration.Migrate(ctx, eventstoreClient, steps.AddEventCreatedAt)
logging.OnError(err).Fatal("unable to migrate step 11")
err = migration.Migrate(ctx, eventstoreClient, steps.s12AddOTPColumns)
logging.OnError(err).Fatal("unable to migrate step 12")
for _, repeatableStep := range repeatableSteps {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)

View File

@ -32,8 +32,8 @@ export class DialogAddSecretGeneratorComponent {
expiry: [exp, [requiredValidator]],
length: [data.config?.length ?? 6, [requiredValidator]],
includeDigits: [data.config?.includeDigits ?? true, [requiredValidator]],
includeLowerLetters: [data.config?.includeSymbols ?? true, [requiredValidator]],
includeSymbols: [data.config?.includeLowerLetters ?? true, [requiredValidator]],
includeSymbols: [data.config?.includeSymbols ?? true, [requiredValidator]],
includeLowerLetters: [data.config?.includeLowerLetters ?? true, [requiredValidator]],
includeUpperLetters: [data.config?.includeUpperLetters ?? true, [requiredValidator]],
});
}

View File

@ -62,11 +62,7 @@ func (s *Server) VerifyMyPhone(ctx context.Context, req *auth_pb.VerifyMyPhoneRe
func (s *Server) ResendMyPhoneVerification(ctx context.Context, _ *auth_pb.ResendMyPhoneVerificationRequest) (*auth_pb.ResendMyPhoneVerificationResponse, error) {
ctxData := authz.GetCtxData(ctx)
phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg)
if err != nil {
return nil, err
}
objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner, phoneCodeGenerator)
objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}

View File

@ -549,11 +549,7 @@ func (s *Server) RemoveHumanPhone(ctx context.Context, req *mgmt_pb.RemoveHumanP
}
func (s *Server) ResendHumanPhoneVerification(ctx context.Context, req *mgmt_pb.ResendHumanPhoneVerificationRequest) (*mgmt_pb.ResendHumanPhoneVerificationResponse, error) {
phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg)
if err != nil {
return nil, err
}
objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, phoneCodeGenerator)
objectDetails, err := s.command.CreateHumanPhoneVerificationCode(ctx, req.UserId, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}

View File

@ -261,7 +261,9 @@ func CodeChallengeToOIDC(challenge *domain.OIDCCodeChallenge) *oidc.CodeChalleng
func AMRFromMFAType(mfaType domain.MFAType) string {
switch mfaType {
case domain.MFATypeTOTP:
case domain.MFATypeTOTP,
domain.MFATypeOTPSMS,
domain.MFATypeOTPEmail:
return OTP
case domain.MFATypeU2F,
domain.MFATypeU2FUserVerification:

View File

@ -149,6 +149,8 @@ type authMethod string
const (
authMethodPassword authMethod = "password"
authMethodOTP authMethod = "OTP"
authMethodOTPSMS authMethod = "OTP SMS"
authMethodOTPEmail authMethod = "OTP Email"
authMethodU2F authMethod = "U2F"
authMethodPasswordless authMethod = "passwordless"
)

View File

@ -171,6 +171,14 @@ func setContext(ctx context.Context, resourceOwner string) context.Context {
return authz.SetCtxData(ctx, data)
}
func setUserContext(ctx context.Context, userID, resourceOwner string) context.Context {
data := authz.CtxData{
UserID: userID,
OrgID: resourceOwner,
}
return authz.SetCtxData(ctx, data)
}
func (l *Login) baseURL(ctx context.Context) string {
return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure) + HandlerPrefix
}

View File

@ -0,0 +1,125 @@
package login
import (
"net/http"
"github.com/zitadel/zitadel/internal/domain"
)
const (
tmplMFASMSInit = "mfainitsms"
)
type smsInitData struct {
userData
Edit bool
MFAType domain.MFAType
Phone string
}
type smsInitFormData struct {
Edit bool `schema:"edit"`
Resend bool `schema:"resend"`
Phone string `schema:"phone"`
NewPhone string `schema:"newPhone"`
Code string `schema:"code"`
}
// handleRegisterOTPSMS checks if the user has a verified phone number and will directly add OTP SMS as 2FA.
// It will also add a successful OTP SMS check to the auth request.
// If there's no verified phone number, the potential last phone number will be used to render the registration page
func (l *Login) handleRegisterOTPSMS(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
user, err := l.query.GetNotifyUserByID(r.Context(), true, authReq.UserID, false)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if user.VerifiedPhone == "" {
data := new(smsInitData)
data.Phone = user.LastPhone
data.Edit = user.LastPhone == ""
l.renderRegisterSMS(w, r, authReq, data, nil)
return
}
_, err = l.command.AddHumanOTPSMSWithCheckSucceeded(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeOTPSMS,
}
l.renderMFAInitDone(w, r, authReq, done)
}
func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *smsInitData, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data.baseData = l.getBaseData(r, authReq, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.profileData = l.getProfileData(authReq)
data.MFAType = domain.MFATypeOTPSMS
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplMFASMSInit], data, nil)
}
// handleRegisterSMSCheck handles form submissions of the SMS registration.
// The user can be either in edit mode, where a phone number can be entered / changed.
// If a phone was set, the user can either switch to edit mode, have a resend of the code or verify the code by entering it.
// On successful code verification, the phone will be added to the user as well as his MFA
// and a successful OTP SMS check will be added to the auth request.
func (l *Login) handleRegisterSMSCheck(w http.ResponseWriter, r *http.Request) {
formData := new(smsInitFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
ctx := setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID)
// save the current state
data := &smsInitData{Phone: formData.Phone}
if formData.Edit {
data.Edit = true
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
if formData.Resend {
_, err = l.command.CreateHumanPhoneVerificationCode(ctx, authReq.UserID, authReq.UserOrgID)
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
// if the user is currently in edit mode,
// he can either change the phone number
// or just return to the code verification again
if formData.Code == "" {
data.Phone = formData.NewPhone
if formData.NewPhone != formData.Phone {
_, err = l.command.ChangeUserPhone(ctx, authReq.UserID, authReq.UserOrgID, formData.NewPhone, l.userCodeAlg)
if err != nil {
// stay in edit more
data.Edit = true
}
}
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
_, err = l.command.VerifyUserPhone(ctx, authReq.UserID, authReq.UserOrgID, formData.Code, l.userCodeAlg)
if err != nil {
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
_, err = l.command.AddHumanOTPSMSWithCheckSucceeded(ctx, authReq.UserID, authReq.UserOrgID, authReq)
if err != nil {
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeOTPSMS,
}
l.renderMFAInitDone(w, r, authReq, done)
}

View File

@ -83,6 +83,12 @@ func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authRe
case domain.MFATypeTOTP:
l.handleTOTPCreation(w, r, authReq, data)
return
case domain.MFATypeOTPSMS:
l.handleRegisterOTPSMS(w, r, authReq)
return
case domain.MFATypeOTPEmail:
l.handleRegisterOTPEmail(w, r, authReq)
return
case domain.MFATypeU2F:
l.renderRegisterU2F(w, r, authReq, nil)
return
@ -103,3 +109,17 @@ func (l *Login) handleTOTPCreation(w http.ResponseWriter, r *http.Request, authR
}
l.renderMFAInitVerify(w, r, authReq, data, nil)
}
// handleRegisterOTPEmail will directly add OTP Email as 2FA.
// It will also add a successful OTP Email check to the auth request.
func (l *Login) handleRegisterOTPEmail(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
_, err := l.command.AddHumanOTPEmailWithCheckSucceeded(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeOTPEmail,
}
l.renderMFAInitDone(w, r, authReq, done)
}

View File

@ -84,6 +84,12 @@ func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request,
data.SelectedMFAProvider = domain.MFATypeTOTP
data.Title = translator.LocalizeWithoutArgs("VerifyMFAOTP.Title")
data.Description = translator.LocalizeWithoutArgs("VerifyMFAOTP.Description")
case domain.MFATypeOTPSMS:
l.handleOTPVerification(w, r, authReq, verificationStep.MFAProviders, domain.MFATypeOTPSMS, nil)
return
case domain.MFATypeOTPEmail:
l.handleOTPVerification(w, r, authReq, verificationStep.MFAProviders, domain.MFATypeOTPEmail, nil)
return
default:
l.renderError(w, r, authReq, err)
return

View File

@ -0,0 +1,126 @@
package login
import (
"context"
"fmt"
"net/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
)
const (
tmplOTPVerification = "otpverification"
querySelectedProvider = "selectedProvider"
)
type mfaOTPData struct {
userData
MFAProviders []domain.MFAType
SelectedProvider domain.MFAType
}
type mfaOTPFormData struct {
Resend bool `schema:"resend"`
Code string `schema:"code"`
SelectedProvider domain.MFAType `schema:"selectedProvider"`
Provider domain.MFAType `schema:"provider"`
}
func OTPLink(origin, authRequestID, code string, provider domain.MFAType) string {
return fmt.Sprintf("%s%s?%s=%s&%s=%s&%s=%d", externalLink(origin), EndpointMFAOTPVerify, QueryAuthRequestID, authRequestID, queryCode, code, querySelectedProvider, provider)
}
// renderOTPVerification renders the OTP verification for SMS and Email based on the passed MFAType.
// It will send a new code to either phone or email first.
func (l *Login) handleOTPVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, providers []domain.MFAType, selectedProvider domain.MFAType, err error) {
if err != nil {
l.renderOTPVerification(w, r, authReq, providers, selectedProvider, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
var sendCode func(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) error
switch selectedProvider {
case domain.MFATypeOTPSMS:
sendCode = l.authRepo.SendMFAOTPSMS
case domain.MFATypeOTPEmail:
sendCode = l.authRepo.SendMFAOTPEmail
// another type should never be passed, but just making sure
case domain.MFATypeU2F,
domain.MFATypeTOTP,
domain.MFATypeU2FUserVerification:
l.renderError(w, r, authReq, err)
return
}
err = sendCode(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID)
l.renderOTPVerification(w, r, authReq, providers, selectedProvider, err)
}
func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, providers []domain.MFAType, selectedProvider domain.MFAType, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := &mfaOTPData{
userData: l.getUserData(r, authReq, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
SelectedProvider: selectedProvider,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), l.renderer.Templates[tmplOTPVerification], data, nil)
}
// handleOTPVerificationCheck handles form submissions of the OTP verification.
// On successful code verification, the check will be added to the auth request.
// A user is also able to request a code resend or choose another provider.
func (l *Login) handleOTPVerificationCheck(w http.ResponseWriter, r *http.Request) {
formData := new(mfaOTPFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
step, ok := authReq.PossibleSteps[0].(*domain.MFAVerificationStep)
if !ok {
l.renderError(w, r, authReq, err)
return
}
if formData.Resend {
l.handleOTPVerification(w, r, authReq, step.MFAProviders, formData.SelectedProvider, nil)
return
}
if formData.Code == "" {
l.renderMFAVerifySelected(w, r, authReq, step, formData.Provider, nil)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
var actionType authMethod
var verifyCode func(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) error
switch formData.SelectedProvider {
case domain.MFATypeOTPSMS:
actionType = authMethodOTPSMS
verifyCode = l.authRepo.VerifyMFAOTPSMS
case domain.MFATypeOTPEmail:
actionType = authMethodOTPEmail
verifyCode = l.authRepo.VerifyMFAOTPEmail
// another type should never be passed, but just making sure
case domain.MFATypeU2F,
domain.MFATypeTOTP,
domain.MFATypeU2FUserVerification:
l.renderOTPVerification(w, r, authReq, step.MFAProviders, formData.SelectedProvider, err)
return
}
err = verifyCode(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, formData.Code, authReq.ID, userAgentID, domain.BrowserInfoFromRequest(r))
metadata, actionErr := l.runPostInternalAuthenticationActions(authReq, r, actionType, err)
if err == nil && actionErr == nil && len(metadata) > 0 {
_, err = l.command.BulkSetUserMetadata(r.Context(), authReq.UserID, authReq.UserOrgID, metadata...)
} else if actionErr != nil && err == nil {
err = actionErr
}
if err != nil {
l.renderOTPVerification(w, r, authReq, step.MFAProviders, formData.SelectedProvider, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@ -54,9 +54,11 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
tmplPasswordlessRegistration: "passwordless_registration.html",
tmplPasswordlessRegistrationDone: "passwordless_registration_done.html",
tmplPasswordlessPrompt: "passwordless_prompt.html",
tmplMFAVerify: "mfa_verify_otp.html",
tmplMFAVerify: "mfa_verify_totp.html",
tmplMFAPrompt: "mfa_prompt.html",
tmplMFAInitVerify: "mfa_init_otp.html",
tmplMFASMSInit: "mfa_init_otp_sms.html",
tmplOTPVerification: "mfa_verify_otp.html",
tmplMFAU2FInit: "mfa_init_u2f.html",
tmplU2FVerification: "mfa_verification_u2f.html",
tmplMFAInitDone: "mfa_init_done.html",
@ -170,6 +172,12 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
"mfaInitVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAInitVerify)
},
"mfaInitSMSVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFASMSInitVerify)
},
"mfaOTPVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAOTPVerify)
},
"mfaInitU2FVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAInitU2FVerify)
},

View File

@ -31,6 +31,8 @@ const (
EndpointMFAVerify = "/mfa/verify"
EndpointMFAPrompt = "/mfa/prompt"
EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFASMSInitVerify = "/mfa/init/sms/verify"
EndpointMFAOTPVerify = "/mfa/otp/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerification = "/mail/verification"
@ -89,6 +91,9 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPromptSelection).Methods(http.MethodGet)
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAInitVerify, login.handleMFAInitVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMFASMSInitVerify, login.handleRegisterSMSCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAOTPVerify, login.handleOTPVerificationCheck).Methods(http.MethodGet)
router.HandleFunc(EndpointMFAOTPVerify, login.handleOTPVerificationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAInitU2FVerify, login.handleRegisterU2F).Methods(http.MethodPost)
router.HandleFunc(EndpointU2FVerification, login.handleU2FVerification).Methods(http.MethodPost)
router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet)

View File

@ -85,6 +85,8 @@ InitMFAPrompt:
потребителски акаунт.
Provider0: 'Приложение за удостоверяване (напр. Google/Microsoft Authenticator, Authy)'
Provider1: 'Зависи от устройството (напр. FaceID, Windows Hello, пръстов отпечатък)'
Provider3: OTP SMS
Provider4: OTP имейл
NextButtonText: следващия
SkipButtonText: пропуснете
InitMFAOTP:
@ -98,6 +100,15 @@ InitMFAOTP:
CodeLabel: Код
NextButtonText: следващия
CancelButtonText: анулиране
InitMFAOTPSMS:
Title: 2-факторна проверка
DescriptionPhone: Създайте своя 2-фактор. Въведете телефонния си номер, за да го потвърдите.
DescriptionCode: Създайте своя 2-фактор. Въведете получения код, за да потвърдите своя телефонен номер.
PhoneLabel: Тайна
CodeLabel: Код
EditButtonText: редактиране
ResendButtonText: код за препращане
NextButtonText: следващия
InitMFAU2F:
Title: Добавете ключ за сигурност
Description: >-
@ -118,6 +129,8 @@ InitMFADone:
MFAProvider:
Provider0: 'Приложение за удостоверяване (напр. Google/Microsoft Authenticator, Authy)'
Provider1: 'Зависи от устройството (напр. FaceID, Windows Hello, пръстов отпечатък)'
Provider3: OTP SMS
Provider4: OTP имейл
ChooseOther: или изберете друга опция
VerifyMFAOTP:
Title: Проверете 2-фактора

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: 2-Faktor-Authentifizierung gibt dir eine zusätzliche Sicherheit für dein Benutzerkonto. Damit stellst du sicher, dass nur du Zugriff auf deinen Account hast.
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
Provider1: Geräte abhängig (e.g FaceID, Windows Hello, Fingerprint)
Provider3: OTP SMS
Provider4: OTP Email
NextButtonText: weiter
SkipButtonText: überspringen
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: weiter
CancelButtonText: abbrechen
InitMFAOTPSMS:
Title: 2-Faktor hinzufügen
DescriptionPhone: Erstelle deinen 2-Faktor. Gib deine Telefonnummer ein, um sie zu verifizieren.
DescriptionCode: Erstelle deinen 2-Faktor. Gib den erhaltenen Code ein um deinen Telefonnummer zu verifizieren.
PhoneLabel: Telefonnummer
CodeLabel: Code
EditButtonText: bearbeiten
ResendButtonText: Code erneut senden
NextButtonText: weiter
InitMFAU2F:
Title: Sicherheitsschlüssel hinzufügen
Description: Ein Sicherheitsschlüssel ist eine Verifizierungsmethode, die in Ihrem Telefon integriert sein kann, Bluetooth verwenden oder direkt an den USB-Anschluss Ihres Computers angeschlossen werden.
@ -118,9 +130,18 @@ InitMFADone:
MFAProvider:
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
Provider1: Geräte abhängig (e.g FaceID, Windows Hello, Fingerprint)
Provider3: OTP SMS
Provider4: OTP Email
ChooseOther: oder wähle eine andere Option aus
VerifyMFAOTP:
Title: 2-Faktor verifizieren
Description: Verifiziere deinen Zweitfaktor
CodeLabel: Code
ResendButtonText: Code erneut senden
NextButtonText: next
VerifyOTP:
Title: 2-Faktor verifizieren
Description: Verifiziere deinen Zweitfaktor
CodeLabel: Code

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: 2-factor authentication gives you an additional security for your user account. This ensures that only you have access to your account.
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
Provider1: Device dependent (e.g FaceID, Windows Hello, Fingerprint)
Provider3: OTP SMS
Provider4: OTP Email
NextButtonText: next
SkipButtonText: skip
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: next
CancelButtonText: cancel
InitMFAOTPSMS:
Title: 2-Factor Verification
DescriptionPhone: Create your 2-factor. Enter your phone number to verify it.
DescriptionCode: Create your 2-factor. Enter the received code to verify your phone number.
PhoneLabel: Phone
CodeLabel: Code
EditButtonText: edit
ResendButtonText: resend code
NextButtonText: next
InitMFAU2F:
Title: Add security key
Description: A security key is a verification method that can be built into your phone, use Bluetooth, or plug directly into your computer's USB port.
@ -110,7 +122,7 @@ InitMFAU2F:
ErrorRetry: Retry, create a new challenge or choose a different method.
InitMFADone:
Title: Security key verified
Title: 2-factor verified
Description: Awesome! You just successfully set up your 2-factor and made your account way more secure. The Factor has to be entered on each login.
NextButtonText: next
CancelButtonText: cancel
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
Provider1: Device dependent (e.g FaceID, Windows Hello, Fingerprint)
Provider3: OTP SMS
Provider4: OTP Email
ChooseOther: or choose another option
VerifyMFAOTP:
@ -126,6 +140,13 @@ VerifyMFAOTP:
CodeLabel: Code
NextButtonText: next
VerifyOTP:
Title: Verify 2-Factor
Description: Verify your second factor
CodeLabel: Code
ResendButtonText: resend code
NextButtonText: next
VerifyMFAU2F:
Title: 2-Factor Verification
Description: Verify your 2-Factor with the registered device (e.g FaceID, Windows Hello, Fingerprint)

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: La autenticación de doble factor te proporciona seguridad adicional para tu cuenta de usuario. Ésta asegura que solo tú tienes acceso a tu cuenta.
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
Provider3: OTP SMS
Provider4: OTP email
NextButtonText: siguiente
SkipButtonText: saltar
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: siguiente
CancelButtonText: cancelar
InitMFASMS:
Title: Verificación de doble factor
DescriptionPhone: Crea tu doble factor de autenticación. Introduce tu número de teléfono para verificarlo.
DescriptionCode: Crea tu doble factor de autenticación. Ingrese el código recibido para verificar su número de teléfono.
PhoneLabel: Número de teléfono
CodeLabel: Código
EditButtonText: editar
ResendButtonText: reenviar código
NextButtonText: siguiente
InitMFAU2F:
Title: Añadir clave de seguridad
Description: Una clave de seguridad es un método de verificación que puede integrarse en tu teléfono móvil, con Bluetooth, o conectándolo directamente en el puerto USB de tu ordenador.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
Provider3: OTP SMS
Provider4: OTP email
ChooseOther: o elige otra opción
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: L'authentification à deux facteurs vous offre une sécurité supplémentaire pour votre compte d'utilisateur. Vous êtes ainsi assuré d'être le seul à avoir accès à votre compte.
Provider0: Application d'authentification (par exemple, Google/Microsoft Authenticator, Authy)
Provider1: Dépend de l'appareil (par ex. FaceID, Windows Hello, empreinte digitale)
Provider3: OTP SMS
Provider4: OTP e-mail
NextButtonText: Suivant
SkipButtonText: Passer
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: Suivant
CancelButtonText: Annuler
InitMFASMS:
Title: Vérification à deux facteurs
DescriptionPhone: Créez votre 2-facteurs. Entrez votre numéro de téléphone pour le vérifier.
DescriptionCode: Créez votre 2-facteurs. Entrez le code reçu pour vérifier votre numéro de téléphone.
PhoneLabel: Numéro de téléphone
CodeLabel: Code
EditButtonText: Modifier
ResendButtonText: Renvoyer le code
NextButtonText: Suivant
InitMFAU2F:
Title: Ajouter une clé de sécurité
Description: Une clé de sécurité est une méthode de vérification qui peut être intégrée à votre téléphone, utiliser Bluetooth ou se brancher directement sur le port USB de votre ordinateur.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: Application d'authentification (par exemple, Google/Microsoft Authenticator, Authy)
Provider1: Dépend de l'appareil (par ex. FaceID, Windows Hello, empreinte digitale)
Provider3: OTP SMS
Provider4: OTP e-mail
ChooseOther: ou choisissez une autre option
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: L'autenticazione a due fattori offre un'ulteriore sicurezza al vostro account utente. Questo garantisce che solo voi possiate accedere al vostro account.
Provider0: App Autenticatore (ad esempio Google/Microsoft Authenticator, Authy)
Provider1: Dipende dal dispositivo (ad es. FaceID, Windows Hello, impronta digitale)
Provider3: OTP SMS
Provider4: OTP e-mail
NextButtonText: Avanti
SkipButtonText: salta
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: Avanti
CancelButtonText: annulla
InitMFASMS:
Title: Verificazione a 2 fattori
DescriptionPhone: Crea il tuo 2 fattori. Inserisci il tuo numero di telefono per verificarlo.
DescriptionCode: Crea il tuo 2 fattori. Inserisci il codice ricevuto per verificare il tuo numero di telefono.
PhoneLabel: Numero di telefono
CodeLabel: Codice
EditButtonText: Modifica
ResendButtonText: Reinvia codice
NextButtonText: Avanti
InitMFAU2F:
Title: Aggiungi chiave di sicurezza
Description: Una chiave di sicurezza è un metodo di verifica che può essere integrato nel telefono, utilizzare il Bluetooth o collegarlo direttamente alla porta USB del computer.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: App Autenticatore (ad esempio Google/Microsoft Authenticator, Authy)
Provider1: Dipende dal dispositivo (ad es. FaceID, Windows Hello, impronta digitale)
Provider3: OTP SMS
Provider4: OTP e-mail
ChooseOther: o scegli un'altra opzione
VerifyMFAOTP:

View File

@ -82,6 +82,8 @@ InitMFAPrompt:
Description: 二要素認証でアカウントのセキュリティを強化します。
Provider0: 認証アプリGoogle/Microsoft Authenticator、Authyなど
Provider1: デバイス依存FaceID、Windows Hello、指紋など
Provider3: OTP SMS
Provider4: OTPメール
NextButtonText: 次へ
SkipButtonText: スキップ
@ -94,6 +96,16 @@ InitMFAOTP:
NextButtonText: 次へ
CancelButtonText: キャンセル
InitMFASMS:
Title: 二要素認証
DescriptionPhone: 二要素認証を作成します。確認するには電話番号を入力してください。
DescriptionCode: 二要素認証を作成します。受信したコードを入力して電話番号を確認します。
PhoneLabel: 電話番号
CodeLabel: コード
EditButtonText: 編集
ResendButtonText: コードを再送信
NextButtonText: 次へ
InitMFAU2F:
Title: セキュリティキーの追加
Description: セキュリティキーは、携帯電話への組み込みや、Bluetoothの使用、パソコンのUSBポートに直接差し込むことなどで認証する方法です。
@ -111,6 +123,8 @@ InitMFADone:
MFAProvider:
Provider0: AuthenticatorアプリGoogle/Microsoft Authenticator、Authyなど
Provider1: デバイス依存FaceID、Windows Hello、指紋など
Provider3: OTP SMS
Provider4: OTPメール
ChooseOther: または、他のオプションを選択
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: 2-факторската автентикација ви дава дополнителна безбедност за вашата корисничка сметка. Ова обезбедува само вие да имате пристап до вашата сметка.
Provider0: Апликација за автентикација (на пример Google/Microsoft Authenticator, Authy)
Provider1: Во зависност од вашиот уред (на пример FaceID, Windows Hello, отпечаток од прст)
Provider3: ОТП СМС
Provider4: ОТП е-пошта
NextButtonText: следно
SkipButtonText: прескокни
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: следно
CancelButtonText: откажи
InitMFASMS:
Title: Потврда на 2-факторска автентикација
DescriptionPhone: Направете двофакторна автентикација. Внесете го вашиот телефонски број за да го потврдите.
DescriptionCode: Направете двофакторна автентикација. Внесете го примениот код за да го потврдите вашиот телефонски број.
PhoneLabel: Телефонски број
CodeLabel: Код
EditButtonText: Уредување
ResendButtonText: повторно испрати код
NextButtonText: следно
InitMFAU2F:
Title: Додајте безбедносен клуч
Description: Безбедносниот клуч е метод на верификација кој може да се интегрира во вашиот телефон, да користи Bluetooth или директно да се поврзе во USB приклучокот на вашиот компјутер.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: Апликација за автентикација (на пример Google/Microsoft Authenticator, Authy)
Provider1: Во зависност од вашиот уред (на пример FaceID, Windows Hello, отпечаток од прст)
Provider3: ОТП СМС
Provider4: ОТП е-пошта
ChooseOther: или изберете друга опција
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: 2-etapowe uwierzytelnianie daje Ci dodatkową ochronę dla Twojego konta użytkownika. Dzięki temu masz pewność, że tylko Ty masz dostęp do swojego konta.
Provider0: Aplikacja uwierzytelniająca (np. Google/Microsoft Authenticator, Authy)
Provider1: Zależny od urządzenia (np. FaceID, Windows Hello, Odcisk palca)
Provider3: OTP SMS
Provider4: OTP e-mail
NextButtonText: dalej
SkipButtonText: pomiń
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: dalej
CancelButtonText: anuluj
InitMFASMS:
Title: Weryfikacja 2-etapowa
DescriptionPhone: Utwórz uwierzytelnianie dwuskładnikowe. Wprowadź swój numer telefonu, aby go zweryfikować.
DescriptionCode: Utwórz uwierzytelnianie dwuskładnikowe. Wprowadź otrzymany kod, aby zweryfikować swój numer telefonu.
PhoneLabel: Numer telefonu
CodeLabel: Kod
EditButtonText: edytować
ResendButtonText: wyślij kod ponownie
NextButtonText: dalej
InitMFAU2F:
Title: Dodaj klucz zabezpieczeń
Description: Klucz zabezpieczeń to metoda weryfikacji, która może być zintegrowana z twoim telefonem, używająca Bluetooth lub podłączana bezpośrednio do portu USB komputera.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: Aplikacja uwierzytelniająca (np. Google/Microsoft Authenticator, Authy)
Provider1: Zależny od urządzenia (np. FaceID, Windows Hello, Odcisk palca)
Provider3: OTP SMS
Provider4: OTP e-mail
ChooseOther: lub wybierz inną opcję
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: A autenticação de 2 fatores fornece uma segurança adicional para sua conta de usuário. Isso garante que apenas você tenha acesso à sua conta.
Provider0: Aplicativo de autenticação (por exemplo, Google/Microsoft Authenticator, Authy)
Provider1: Dependente do dispositivo (por exemplo, FaceID, Windows Hello, Impressão digital)
Provider3: OTP SMS
Provider4: OTP e-mail
NextButtonText: próximo
SkipButtonText: pular
@ -101,6 +103,16 @@ InitMFAOTP:
NextButtonText: próximo
CancelButtonText: cancelar
InitMFASMS:
Title: Verificação de 2 fatores
DescriptionPhone: Crie sua verificação de 2 fatores. Digite seu número de telefone para verificá-lo.
DescriptionCode: Crie sua verificação de 2 fatores. Digite o código recebido para verificar seu número de telefone.
PhoneLabel: Número de telefone
CodeLabel: Código
EditButtonText: editar
ResendButtonText: reenviar código
NextButtonText: próximo
InitMFAU2F:
Title: Adicionar chave de segurança
Description: Uma chave de segurança é um método de verificação que pode ser incorporado ao seu telefone, usar Bluetooth ou conectar diretamente à porta USB do seu computador.
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: Aplicativo de autenticação (por exemplo, Google/Microsoft Authenticator, Authy)
Provider1: Dependente do dispositivo (por exemplo, FaceID, Windows Hello, Impressão digital)
Provider3: OTP SMS
Provider4: OTP e-mail
ChooseOther: ou escolha outra opção
VerifyMFAOTP:

View File

@ -89,6 +89,8 @@ InitMFAPrompt:
Description: 两步验证为您的账户提供了额外的安全保障。这确保只有你能访问你的账户。
Provider0: 软件应用(如 Google/Migrosoft Authenticator、Authy
Provider1: 硬件设备(如 Face ID、Windows Hello、指纹
Provider3: 一次性密码短信
Provider4: 一次性密码电子邮件
NextButtonText: 继续
SkipButtonText: 跳过
@ -96,6 +98,16 @@ InitMFAOTP:
Title: 双因素验证
Description: 创建你的双因素。如果你还没有,请下载一个认证器应用程序。
OTPDescription: 使用您的身份验证器应用程序(例如 Google Authenticator扫描代码或复制密码并在下方插入生成的代码。
PhoneLabel: 电话号码
CodeLabel: 验证码
EditButtonText: 编辑
ResendButtonText: 重发代码
NextButtonText: 继续
InitMFASMS:
Title: 双因素验证
DescriptionPhone: 创建双因素身份验证。输入您的电话号码进行验证。
DescriptionCode: 创建双因素身份验证。输入收到的代码以验证您的电话号码。
SecretLabel: 秘钥
CodeLabel: 验证码
NextButtonText: 继续
@ -118,6 +130,8 @@ InitMFADone:
MFAProvider:
Provider0: 软件应用(如 Google/Migrosoft Authenticator、Authy
Provider1: 硬件设备(如 Face ID、Windows Hello、指纹
Provider3: 一次性密码短信
Provider4: 一次性密码电子邮件
ChooseOther: 或选择其他选项
VerifyMFAOTP:

View File

@ -0,0 +1,6 @@
let form = document.getElementsByTagName('form')[0];
let editButton = document.getElementById('edit');
editButton.addEventListener('click', function () {
form.submit();
});

View File

@ -1,15 +1,16 @@
@mixin lgn-mfa-base {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
margin: 2rem 0;
margin: 1rem 0;
.mfa {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
padding: 0 0.5rem;
padding: 1rem 0.5rem;
label {
display: flex;

View File

@ -0,0 +1,63 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "InitMFAOTPSMS.Title"}}</h1>
{{ template "user-profile" . }}
{{if .Edit}}
<p>{{t "InitMFAOTPSMS.DescriptionPhone"}}</p>
{{else}}
<p>{{t "InitMFAOTPSMS.DescriptionCode"}}</p>
{{end}}
</div>
<form action="{{ mfaInitSMSVerifyUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .MFAType }}" />
<input type="hidden" name="phone" value="{{ .Phone }}" />
<div class="fields">
{{if .Edit}}
<div class="field">
<label class="lgn-label" for="newPhone">{{t "InitMFAOTPSMS.PhoneLabel"}}</label>
<input class="lgn-input" type="tel" id="newPhone" name="newPhone" autocomplete="off" value="{{.Phone}}" autofocus required>
</div>
{{else}}
<div class="field lgn-actions">
<p>{{.Phone}}</p>
<span class="fill-space"></span>
<button type="button" id="edit" name="edit" value="true" class="lgn-stroked-button" formnovalidate>{{t "InitMFAOTPSMS.EditButtonText"}}</button>
</div>
{{end}}
{{if and .Phone (not .Edit) }}
<div class="field">
<label class="lgn-label" for="code">{{t "InitMFAOTPSMS.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="one-time-code" autofocus required>
</div>
{{end}}
</div>
{{ template "error-message" .}}
<div class="lgn-actions lgn-reverse-order">
<!-- position element in header -->
<a class="lgn-icon-button lgn-left-action" href="{{ mfaPromptChangeUrl .AuthReqID .MFAType }}">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "InitMFAOTPSMS.NextButtonText"}}</button>
{{if and .Phone (not .Edit) }}
<span class="fill-space"></span>
<button type="submit" name="resend" value="true" class="lgn-stroked-button" formnovalidate>{{t "InitMFAOTPSMS.ResendButtonText"}}</button>
{{end}}
</div>
</form>
<script src="{{ resourceUrl "scripts/edit.js" }}"></script>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -34,6 +34,16 @@
<img width="100px" height="100px" alt="OTP" src="{{ resourceUrl
"images/mfa/mfa-u2f.svg" }}" />
</div>
{{ end }} {{ if eq $provider 3 }}
<div class="mfa-img">
<img width="100px" height="100px" alt="OTP SMS" src="{{ resourceUrl
"images/mfa/mfa-u2f.svg" }}" /> // TODO: image
</div>
{{ end }}{{ if eq $provider 4 }}
<div class="mfa-img">
<img width="100px" height="100px" alt="OTP Email" src="{{ resourceUrl
"images/mfa/mfa-u2f.svg" }}" /> // TODO: image
</div>
{{ end }}
<span>{{ $providerName }} </span>
</label>

View File

@ -1,34 +1,37 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "VerifyMFAOTP.Title"}}</h1>
<h1>{{t "VerifyOTP.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "VerifyMFAOTP.Description"}}</p>
<p>{{t "VerifyOTP.Description"}}</p>
</div>
<form action="{{ mfaVerifyUrl }}" method="POST">
<form action="{{ mfaOTPVerifyUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .SelectedMFAProvider }}" />
<input type="hidden" name="selectedProvider" value="{{ .SelectedProvider }}" />
<div class="fields">
<label class="lgn-label" for="code">{{t "VerifyMFAOTP.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" autofocus required>
<label class="lgn-label" for="code">{{t "VerifyOTP.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="one-time-code" autofocus required>
</div>
{{ template "error-message" .}}
<div class="lgn-actions">
<div class="lgn-actions lgn-reverse-order">
<!-- position element in header -->
<a class="lgn-icon-button lgn-left-action" href="{{ loginUrl }}">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "VerifyOTP.NextButtonText"}}</button>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "VerifyMFAOTP.NextButtonText"}}</button>
<button type="submit" name="resend" value="true" class="lgn-stroked-button" formnovalidate>{{t "VerifyOTP.ResendButtonText"}}</button>
</div>
{{ if .MFAProviders }}
@ -45,4 +48,4 @@
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{template "main-bottom" .}}
{{template "main-bottom" .}}

View File

@ -0,0 +1,48 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "VerifyMFAOTP.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "VerifyMFAOTP.Description"}}</p>
</div>
<form action="{{ mfaVerifyUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="mfaType" value="{{ .SelectedMFAProvider }}" />
<div class="fields">
<label class="lgn-label" for="code">{{t "VerifyMFAOTP.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" autofocus required>
</div>
{{ template "error-message" .}}
<div class="lgn-actions">
<!-- position element in header -->
<a class="lgn-icon-button lgn-left-action" href="{{ loginUrl }}">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "VerifyMFAOTP.NextButtonText"}}</button>
</div>
{{ if .MFAProviders }}
<div class="lgn-mfa-other">
<p>{{t "MFAProvider.ChooseOther"}}</p>
{{ range $provider := .MFAProviders}}
{{ $providerName := (t (printf "MFAProvider.Provider%v" $provider)) }}
<button class="lgn-stroked-button" type="submit" name="provider" value="{{$provider}}"
formnovalidate>{{$providerName}}</button>
{{ end }}
</div>
{{ end }}
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -23,6 +23,10 @@ type AuthRequestRepository interface {
VerifyPassword(ctx context.Context, id, userID, resourceOwner, password, userAgentID string, info *domain.BrowserInfo) error
VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error
SendMFAOTPSMS(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) error
VerifyMFAOTPSMS(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) error
SendMFAOTPEmail(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) error
VerifyMFAOTPEmail(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) error
BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
VerifyMFAU2F(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string, preferredPlatformType domain.AuthenticatorAttachment) (login *domain.WebAuthNToken, err error)

View File

@ -376,6 +376,48 @@ func (repo *AuthRequestRepo) VerifyMFAOTP(ctx context.Context, authRequestID, us
return repo.Command.HumanCheckMFATOTP(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
}
func (repo *AuthRequestRepo) SendMFAOTPSMS(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
if err != nil {
return err
}
return repo.Command.HumanSendOTPSMS(ctx, userID, resourceOwner, request)
}
func (repo *AuthRequestRepo) VerifyMFAOTPSMS(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
if err != nil {
return err
}
return repo.Command.HumanCheckOTPSMS(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
}
func (repo *AuthRequestRepo) SendMFAOTPEmail(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
if err != nil {
return err
}
return repo.Command.HumanSendOTPEmail(ctx, userID, resourceOwner, request)
}
func (repo *AuthRequestRepo) VerifyMFAOTPEmail(ctx context.Context, userID, resourceOwner, code, authRequestID, userAgentID string, info *domain.BrowserInfo) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authRequestID, userAgentID, userID)
if err != nil {
return err
}
return repo.Command.HumanCheckOTPEmail(ctx, userID, code, resourceOwner, request.WithCurrentInfo(info))
}
func (repo *AuthRequestRepo) BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@ -138,6 +138,10 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
user_repo.HumanMFAOTPAddedType,
user_repo.HumanMFAOTPVerifiedType,
user_repo.HumanMFAOTPRemovedType,
user_repo.HumanOTPSMSAddedType,
user_repo.HumanOTPSMSRemovedType,
user_repo.HumanOTPEmailAddedType,
user_repo.HumanOTPEmailRemovedType,
user_repo.HumanU2FTokenAddedType,
user_repo.HumanU2FTokenVerifiedType,
user_repo.HumanU2FTokenRemovedType,

View File

@ -137,6 +137,9 @@ func writeModelToWebAuthN(wm *HumanWebAuthNWriteModel) *domain.WebAuthNToken {
}
func authRequestDomainToAuthRequestInfo(authRequest *domain.AuthRequest) *user.AuthRequestInfo {
if authRequest == nil {
return nil
}
info := &user.AuthRequestInfo{
ID: authRequest.ID,
UserAgentID: authRequest.AgentID,

View File

@ -2,6 +2,7 @@ package command
import (
"context"
"time"
"github.com/pquerna/otp"
"github.com/zitadel/logging"
@ -15,16 +16,16 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (c *Commands) ImportHumanTOTP(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
}
if err = c.checkUserExists(ctx, userID, resourceowner); err != nil {
if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil {
return err
}
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceowner)
otpWriteModel, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
@ -40,11 +41,11 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res
return err
}
func (c *Commands) AddHumanTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, 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")
}
prep, err := c.createHumanTOTP(ctx, userID, resourceowner)
prep, err := c.createHumanTOTP(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@ -114,12 +115,12 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st
}, nil
}
func (c *Commands) HumanCheckMFATOTPSetup(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.totpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
@ -145,11 +146,11 @@ func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, use
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) HumanCheckMFATOTP(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.totpWriteModelByID(ctx, userID, resourceowner)
existingOTP, err := c.totpWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
@ -191,7 +192,26 @@ func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner st
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
// AddHumanOTPSMS adds the OTP SMS factor to a user.
// It can only be added if it not already is and the phone has to be verified.
func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
return c.addHumanOTPSMS(ctx, userID, resourceOwner)
}
// AddHumanOTPSMSWithCheckSucceeded adds the OTP SMS factor to a user.
// It can only be added if it's not already and the phone has to be verified.
// An OTPSMSCheckSucceededEvent will be added to the passed AuthRequest, if not nil.
func (c *Commands) AddHumanOTPSMSWithCheckSucceeded(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) (*domain.ObjectDetails, error) {
if authRequest == nil {
return c.addHumanOTPSMS(ctx, userID, resourceOwner)
}
event := func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
return user.NewHumanOTPSMSCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))
}
return c.addHumanOTPSMS(ctx, userID, resourceOwner, event)
}
func (c *Commands) addHumanOTPSMS(ctx context.Context, userID, resourceOwner string, events ...eventCallback) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing")
}
@ -209,7 +229,12 @@ func (c *Commands) AddHumanOTPSMS(ctx context.Context, userID, resourceOwner str
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 {
cmds := make([]eventstore.Command, len(events)+1)
cmds[0] = user.NewHumanOTPSMSAddedEvent(ctx, userAgg)
for i, event := range events {
cmds[i+1] = event(ctx, userAgg)
}
if err = c.pushAppendAndReduce(ctx, otpWriteModel, cmds...); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
@ -225,7 +250,7 @@ func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.WriteModel.ResourceOwner, userID); err != nil {
return nil, err
}
}
@ -239,7 +264,77 @@ func (c *Commands) RemoveHumanOTPSMS(ctx context.Context, userID, resourceOwner
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) HumanSendOTPSMS(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) error {
smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
return c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
}
codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPSMSCodeAddedEvent(ctx, aggregate, code, expiry, info)
}
return c.sendHumanOTP(
ctx,
userID,
resourceOwner,
authRequest,
smsWriteModel,
domain.SecretGeneratorTypeOTPSMS,
codeAddedEvent,
)
}
func (c *Commands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string) (err error) {
smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
return c.otpSMSWriteModelByID(ctx, userID, resourceOwner)
}
codeSentEvent := func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command {
return user.NewHumanOTPSMSCodeSentEvent(ctx, aggregate)
}
return c.humanOTPSent(ctx, userID, resourceOwner, smsWriteModel, codeSentEvent)
}
func (c *Commands) HumanCheckOTPSMS(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
return c.otpSMSCodeWriteModelByID(ctx, userID, resourceOwner)
}
succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPSMSCheckSucceededEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPSMSCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
return c.humanCheckOTP(
ctx,
userID,
code,
resourceOwner,
authRequest,
writeModel,
domain.SecretGeneratorTypeOTPSMS,
succeededEvent,
failedEvent,
)
}
// AddHumanOTPEmail adds the OTP Email factor to a user.
// It can only be added if it not already is and the phone has to be verified.
func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
return c.addHumanOTPEmail(ctx, userID, resourceOwner)
}
// AddHumanOTPEmailWithCheckSucceeded adds the OTP Email factor to a user.
// It can only be added if it's not already and the email has to be verified.
// An OTPEmailCheckSucceededEvent will be added to the passed AuthRequest, if not nil.
func (c *Commands) AddHumanOTPEmailWithCheckSucceeded(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) (*domain.ObjectDetails, error) {
if authRequest == nil {
return c.addHumanOTPEmail(ctx, userID, resourceOwner)
}
event := func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
return user.NewHumanOTPEmailCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))
}
return c.addHumanOTPEmail(ctx, userID, resourceOwner, event)
}
func (c *Commands) addHumanOTPEmail(ctx context.Context, userID, resourceOwner string, events ...eventCallback) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing")
}
@ -254,7 +349,12 @@ func (c *Commands) AddHumanOTPEmail(ctx context.Context, userID, resourceOwner s
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 {
cmds := make([]eventstore.Command, len(events)+1)
cmds[0] = user.NewHumanOTPEmailAddedEvent(ctx, userAgg)
for i, event := range events {
cmds[i+1] = event(ctx, userAgg)
}
if err = c.pushAppendAndReduce(ctx, otpWriteModel, cmds...); err != nil {
return nil, err
}
return writeModelToObjectDetails(&otpWriteModel.WriteModel), nil
@ -270,7 +370,7 @@ func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwne
return nil, err
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.WriteModel.ResourceOwner, userID); err != nil {
return nil, err
}
}
@ -284,6 +384,147 @@ func (c *Commands) RemoveHumanOTPEmail(ctx context.Context, userID, resourceOwne
return writeModelToObjectDetails(&existingOTP.WriteModel), nil
}
func (c *Commands) HumanSendOTPEmail(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest) error {
smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
return c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
}
codeAddedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPEmailCodeAddedEvent(ctx, aggregate, code, expiry, info)
}
return c.sendHumanOTP(
ctx,
userID,
resourceOwner,
authRequest,
smsWriteModel,
domain.SecretGeneratorTypeOTPEmail,
codeAddedEvent,
)
}
func (c *Commands) HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) (err error) {
smsWriteModel := func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error) {
return c.otpEmailWriteModelByID(ctx, userID, resourceOwner)
}
codeSentEvent := func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command {
return user.NewHumanOTPEmailCodeSentEvent(ctx, aggregate)
}
return c.humanOTPSent(ctx, userID, resourceOwner, smsWriteModel, codeSentEvent)
}
func (c *Commands) HumanCheckOTPEmail(ctx context.Context, userID, code, resourceOwner string, authRequest *domain.AuthRequest) error {
writeModel := func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error) {
return c.otpEmailCodeWriteModelByID(ctx, userID, resourceOwner)
}
succeededEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPEmailCheckSucceededEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
failedEvent := func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command {
return user.NewHumanOTPEmailCheckFailedEvent(ctx, aggregate, authRequestDomainToAuthRequestInfo(authRequest))
}
return c.humanCheckOTP(
ctx,
userID,
code,
resourceOwner,
authRequest,
writeModel,
domain.SecretGeneratorTypeOTPEmail,
succeededEvent,
failedEvent,
)
}
// sendHumanOTP creates a code for a registered mechanism (sms / email), which is used for a check (during login)
func (c *Commands) sendHumanOTP(
ctx context.Context,
userID, resourceOwner string,
authRequest *domain.AuthRequest,
writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error),
secretGeneratorType domain.SecretGeneratorType,
codeAddedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, expiry time.Duration, info *user.AuthRequestInfo) eventstore.Command,
) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-S3SF1", "Errors.User.UserIDMissing")
}
existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if !existingOTP.OTPAdded() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFD52", "Errors.User.MFA.OTP.NotReady")
}
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, secretGeneratorType)
if err != nil {
return err
}
gen := crypto.NewEncryptionGenerator(*config, c.userEncryption)
value, _, err := crypto.NewCode(gen)
if err != nil {
return err
}
userAgg := &user.NewAggregate(userID, resourceOwner).Aggregate
_, err = c.eventstore.Push(ctx, codeAddedEvent(ctx, userAgg, value, gen.Expiry(), authRequestDomainToAuthRequestInfo(authRequest)))
return err
}
func (c *Commands) humanOTPSent(
ctx context.Context,
userID, resourceOwner string,
writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPWriteModel, error),
codeSentEvent func(ctx context.Context, aggregate *eventstore.Aggregate) eventstore.Command,
) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-AE2h2", "Errors.User.UserIDMissing")
}
existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if !existingOTP.OTPAdded() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SD3gh", "Errors.User.MFA.OTP.NotReady")
}
userAgg := &user.NewAggregate(userID, resourceOwner).Aggregate
_, err = c.eventstore.Push(ctx, codeSentEvent(ctx, userAgg))
return err
}
func (c *Commands) humanCheckOTP(
ctx context.Context,
userID, code, resourceOwner string,
authRequest *domain.AuthRequest,
writeModelByID func(ctx context.Context, userID string, resourceOwner string) (OTPCodeWriteModel, error),
secretGeneratorType domain.SecretGeneratorType,
checkSucceededEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
checkFailedEvent func(ctx context.Context, aggregate *eventstore.Aggregate, info *user.AuthRequestInfo) eventstore.Command,
) error {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-S453v", "Errors.User.UserIDMissing")
}
if code == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-SJl2g", "Errors.User.Code.Empty")
}
existingOTP, err := writeModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if !existingOTP.OTPAdded() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-d2r52", "Errors.User.MFA.OTP.NotReady")
}
if existingOTP.Code() == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound")
}
userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate
err = crypto.VerifyCodeWithAlgorithm(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, c.userEncryption)
if err == nil {
_, err = c.eventstore.Push(ctx, checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
return err
}
_, pushErr := c.eventstore.Push(ctx, checkFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
return err
}
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -308,6 +549,18 @@ func (c *Commands) otpSMSWriteModelByID(ctx context.Context, userID, resourceOwn
return writeModel, nil
}
func (c *Commands) otpSMSCodeWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPSMSCodeWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPSMSCodeWriteModel(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) }()
@ -319,3 +572,15 @@ func (c *Commands) otpEmailWriteModelByID(ctx context.Context, userID, resourceO
}
return writeModel, nil
}
func (c *Commands) otpEmailCodeWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPEmailCodeWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewHumanOTPEmailCodeWriteModel(userID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}

View File

@ -1,6 +1,8 @@
package command
import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
@ -60,6 +62,18 @@ func (wm *HumanTOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
return query
}
type OTPWriteModel interface {
OTPAdded() bool
ResourceOwner() string
}
type OTPCodeWriteModel interface {
OTPWriteModel
CodeCreationDate() time.Time
CodeExpiry() time.Duration
Code() *crypto.CryptoValue
}
type HumanOTPSMSWriteModel struct {
eventstore.WriteModel
@ -67,6 +81,14 @@ type HumanOTPSMSWriteModel struct {
otpAdded bool
}
func (wm *HumanOTPSMSWriteModel) OTPAdded() bool {
return wm.otpAdded
}
func (wm *HumanOTPSMSWriteModel) ResourceOwner() string {
return wm.WriteModel.ResourceOwner
}
func NewHumanOTPSMSWriteModel(userID, resourceOwner string) *HumanOTPSMSWriteModel {
return &HumanOTPSMSWriteModel{
WriteModel: eventstore.WriteModel{
@ -107,8 +129,66 @@ func (wm *HumanOTPSMSWriteModel) Query() *eventstore.SearchQueryBuilder {
).
Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
if wm.WriteModel.ResourceOwner != "" {
query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}
type HumanOTPSMSCodeWriteModel struct {
*HumanOTPSMSWriteModel
code *crypto.CryptoValue
codeCreationDate time.Time
codeExpiry time.Duration
}
func (wm *HumanOTPSMSCodeWriteModel) CodeCreationDate() time.Time {
return wm.codeCreationDate
}
func (wm *HumanOTPSMSCodeWriteModel) CodeExpiry() time.Duration {
return wm.codeExpiry
}
func (wm *HumanOTPSMSCodeWriteModel) Code() *crypto.CryptoValue {
return wm.code
}
func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCodeWriteModel {
return &HumanOTPSMSCodeWriteModel{
HumanOTPSMSWriteModel: NewHumanOTPSMSWriteModel(userID, resourceOwner),
}
}
func (wm *HumanOTPSMSCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
if e, ok := event.(*user.HumanOTPSMSCodeAddedEvent); ok {
wm.code = e.Code
wm.codeCreationDate = e.CreationDate()
wm.codeExpiry = e.Expiry
}
}
return wm.HumanOTPSMSWriteModel.Reduce()
}
func (wm *HumanOTPSMSCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
user.HumanOTPSMSCodeAddedType,
user.HumanPhoneVerifiedType,
user.HumanOTPSMSAddedType,
user.HumanOTPSMSRemovedType,
user.HumanPhoneRemovedType,
user.UserRemovedType,
).
Builder()
if wm.WriteModel.ResourceOwner != "" {
query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}
@ -120,6 +200,14 @@ type HumanOTPEmailWriteModel struct {
otpAdded bool
}
func (wm *HumanOTPEmailWriteModel) OTPAdded() bool {
return wm.otpAdded
}
func (wm *HumanOTPEmailWriteModel) ResourceOwner() string {
return wm.WriteModel.ResourceOwner
}
func NewHumanOTPEmailWriteModel(userID, resourceOwner string) *HumanOTPEmailWriteModel {
return &HumanOTPEmailWriteModel{
WriteModel: eventstore.WriteModel{
@ -151,15 +239,73 @@ func (wm *HumanOTPEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(user.HumanEmailVerifiedType,
EventTypes(
user.HumanEmailVerifiedType,
user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType,
user.UserRemovedType,
).
Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
if wm.WriteModel.ResourceOwner != "" {
query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}
type HumanOTPEmailCodeWriteModel struct {
*HumanOTPEmailWriteModel
code *crypto.CryptoValue
codeCreationDate time.Time
codeExpiry time.Duration
}
func (wm *HumanOTPEmailCodeWriteModel) CodeCreationDate() time.Time {
return wm.codeCreationDate
}
func (wm *HumanOTPEmailCodeWriteModel) CodeExpiry() time.Duration {
return wm.codeExpiry
}
func (wm *HumanOTPEmailCodeWriteModel) Code() *crypto.CryptoValue {
return wm.code
}
func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmailCodeWriteModel {
return &HumanOTPEmailCodeWriteModel{
HumanOTPEmailWriteModel: NewHumanOTPEmailWriteModel(userID, resourceOwner),
}
}
func (wm *HumanOTPEmailCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
if e, ok := event.(*user.HumanOTPEmailCodeAddedEvent); ok {
wm.code = e.Code
wm.codeCreationDate = e.CreationDate()
wm.codeExpiry = e.Expiry
}
}
return wm.HumanOTPEmailWriteModel.Reduce()
}
func (wm *HumanOTPEmailCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
user.HumanOTPEmailCodeAddedType,
user.HumanEmailVerifiedType,
user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType,
user.UserRemovedType,
).
Builder()
if wm.WriteModel.ResourceOwner != "" {
query.ResourceOwner(wm.WriteModel.ResourceOwner)
}
return query
}

File diff suppressed because it is too large Load Diff

View File

@ -97,7 +97,7 @@ func (c *Commands) VerifyHumanPhone(ctx context.Context, userID, code, resourceo
return nil, caos_errs.ThrowInvalidArgument(err, "COMMAND-sM0cs", "Errors.User.Code.Invalid")
}
func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string, phoneCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID, resourceowner string) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
@ -116,19 +116,17 @@ func (c *Commands) CreateHumanPhoneVerificationCode(ctx context.Context, userID,
if existingPhone.IsPhoneVerified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sf", "Errors.User.Phone.AlreadyVerified")
}
phoneCode, err := domain.NewPhoneCode(phoneCodeGenerator)
config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode)
if err != nil {
return nil, err
}
phoneCode, err := domain.NewPhoneCode(crypto.NewEncryptionGenerator(*config, c.userEncryption))
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingPhone, pushedEvents...)
if err != nil {
if err = c.pushAppendAndReduce(ctx, existingPhone, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.Code, phoneCode.Expiry)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingPhone.WriteModel), nil

View File

@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
@ -14,6 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/user"
)
@ -584,13 +586,13 @@ func TestCommandSide_VerifyHumanPhone(t *testing.T) {
func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore *eventstore.Eventstore
userEncryption crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
userID string
resourceOwner string
secretGenerator crypto.Generator
ctx context.Context
userID string
resourceOwner string
}
type res struct {
want *domain.ObjectDetails
@ -704,6 +706,19 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
),
),
),
expectFilter(
eventFromEventPusher(
instance.NewSecretGeneratorAddedEvent(context.Background(),
&instance.NewAggregate("instanceID").Aggregate,
domain.SecretGeneratorTypeVerifyPhoneCode,
8,
time.Hour,
true,
true,
true,
true,
)),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@ -713,7 +728,7 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
Crypted: []byte("12345678"),
},
time.Hour*1,
),
@ -721,12 +736,12 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
},
),
),
userEncryption: crypto.CreateMockEncryptionAlgWithCode(gomock.NewController(t), "12345678"),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
@ -738,9 +753,10 @@ func TestCommandSide_CreateVerificationCodeHumanPhone(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore,
userEncryption: tt.fields.userEncryption,
}
got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.secretGenerator)
got, err := r.CreateHumanPhoneVerificationCode(tt.args.ctx, tt.args.userID, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -9,14 +9,37 @@ import (
)
func CreateMockEncryptionAlg(ctrl *gomock.Controller) EncryptionAlgorithm {
return createMockEncryptionAlgorithm(
ctrl,
func(code []byte) ([]byte, error) {
return code, nil
},
)
}
// CreateMockEncryptionAlgWithCode compares the length of the value to be encrypted with the length of the provided code.
// It will return an error if they do not match.
// The provided code will be used to encrypt in favor of the value passed to the encryption.
// This function is intended to be used where the passed value is not in control, but where the returned encryption requires a static value.
func CreateMockEncryptionAlgWithCode(ctrl *gomock.Controller, code string) EncryptionAlgorithm {
return createMockEncryptionAlgorithm(
ctrl,
func(c []byte) ([]byte, error) {
if len(c) != len(code) {
return nil, errors.ThrowInvalidArgumentf(nil, "id", "invalid code length - expected %d, got %d", len(code), len(c))
}
return []byte(code), nil
},
)
}
func createMockEncryptionAlgorithm(ctrl *gomock.Controller, encryptFunction func(c []byte) ([]byte, error)) *MockEncryptionAlgorithm {
mCrypto := NewMockEncryptionAlgorithm(ctrl)
mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc")
mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id")
mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"})
mCrypto.EXPECT().Encrypt(gomock.Any()).AnyTimes().DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
encryptFunction,
)
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
func(code []byte, keyID string) (string, error) {

View File

@ -105,6 +105,8 @@ const (
MFATypeTOTP MFAType = iota
MFATypeU2F
MFATypeU2FUserVerification
MFATypeOTPSMS
MFATypeOTPEmail
)
type MFALevel int

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)
}

View File

@ -113,6 +113,14 @@ func (p *userAuthMethodProjection) reducers() []handler.AggregateReducer {
Event: user.HumanOTPSMSRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
{
Event: user.HumanPhoneRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
{
Event: user.UserV1PhoneRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
{
Event: user.HumanOTPEmailRemovedType,
Reduce: p.reduceRemoveAuthMethod,

View File

@ -168,7 +168,7 @@ func ChangeSecretGeneratorIncludeDigits(includeDigits bool) func(event *SecretGe
func ChangeSecretGeneratorIncludeSymbols(includeSymbols bool) func(event *SecretGeneratorChangedEvent) {
return func(e *SecretGeneratorChangedEvent) {
e.IncludeDigits = &includeSymbols
e.IncludeSymbols = &includeSymbols
}
}

View File

@ -91,10 +91,14 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, HumanMFAOTPCheckFailedType, HumanOTPCheckFailedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSAddedType, eventstore.GenericEventMapper[HumanOTPSMSAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSRemovedType, eventstore.GenericEventMapper[HumanOTPSMSRemovedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCodeAddedType, eventstore.GenericEventMapper[HumanOTPSMSCodeAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCodeSentType, eventstore.GenericEventMapper[HumanOTPSMSCodeSentEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckSucceededType, eventstore.GenericEventMapper[HumanOTPSMSCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPSMSCheckFailedType, eventstore.GenericEventMapper[HumanOTPSMSCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailAddedType, eventstore.GenericEventMapper[HumanOTPEmailAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailRemovedType, eventstore.GenericEventMapper[HumanOTPEmailRemovedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCodeAddedType, eventstore.GenericEventMapper[HumanOTPEmailCodeAddedEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCodeSentType, eventstore.GenericEventMapper[HumanOTPEmailCodeSentEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckSucceededType, eventstore.GenericEventMapper[HumanOTPEmailCheckSucceededEvent]).
RegisterFilterEventMapper(AggregateType, HumanOTPEmailCheckFailedType, eventstore.GenericEventMapper[HumanOTPEmailCheckFailedEvent]).
RegisterFilterEventMapper(AggregateType, HumanU2FTokenAddedType, HumanU2FAddedEventMapper).

View File

@ -3,6 +3,7 @@ package user
import (
"context"
"encoding/json"
"time"
"github.com/zitadel/zitadel/internal/eventstore"
@ -21,11 +22,15 @@ const (
otpSMSEventPrefix = otpEventPrefix + "sms."
HumanOTPSMSAddedType = otpSMSEventPrefix + "added"
HumanOTPSMSRemovedType = otpSMSEventPrefix + "removed"
HumanOTPSMSCodeAddedType = otpSMSEventPrefix + "code.added"
HumanOTPSMSCodeSentType = otpSMSEventPrefix + "code.sent"
HumanOTPSMSCheckSucceededType = otpSMSEventPrefix + "check.succeeded"
HumanOTPSMSCheckFailedType = otpSMSEventPrefix + "check.failed"
otpEmailEventPrefix = otpEventPrefix + "email."
HumanOTPEmailAddedType = otpEmailEventPrefix + "added"
HumanOTPEmailRemovedType = otpEmailEventPrefix + "removed"
HumanOTPEmailCodeAddedType = otpEmailEventPrefix + "code.added"
HumanOTPEmailCodeSentType = otpEmailEventPrefix + "code.sent"
HumanOTPEmailCheckSucceededType = otpEmailEventPrefix + "check.succeeded"
HumanOTPEmailCheckFailedType = otpEmailEventPrefix + "check.failed"
)
@ -271,6 +276,78 @@ func NewHumanOTPSMSRemovedEvent(
}
}
type HumanOTPSMSCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
*AuthRequestInfo
}
func (e *HumanOTPSMSCodeAddedEvent) Data() interface{} {
return e
}
func (e *HumanOTPSMSCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
info *AuthRequestInfo,
) *HumanOTPSMSCodeAddedEvent {
return &HumanOTPSMSCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSCodeAddedType,
),
Code: code,
Expiry: expiry,
AuthRequestInfo: info,
}
}
type HumanOTPSMSCodeSentEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
*AuthRequestInfo
}
func (e *HumanOTPSMSCodeSentEvent) Data() interface{} {
return e
}
func (e *HumanOTPSMSCodeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPSMSCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPSMSCodeSentEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPSMSCodeSentEvent {
return &HumanOTPSMSCodeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPSMSCodeSentType,
),
}
}
type HumanOTPSMSCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo
@ -393,6 +470,78 @@ func NewHumanOTPEmailRemovedEvent(
}
}
type HumanOTPEmailCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
*AuthRequestInfo
}
func (e *HumanOTPEmailCodeAddedEvent) Data() interface{} {
return e
}
func (e *HumanOTPEmailCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
info *AuthRequestInfo,
) *HumanOTPEmailCodeAddedEvent {
return &HumanOTPEmailCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailCodeAddedType,
),
Code: code,
Expiry: expiry,
AuthRequestInfo: info,
}
}
type HumanOTPEmailCodeSentEvent struct {
eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
*AuthRequestInfo
}
func (e *HumanOTPEmailCodeSentEvent) Data() interface{} {
return e
}
func (e *HumanOTPEmailCodeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *HumanOTPEmailCodeSentEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
func NewHumanOTPEmailCodeSentEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *HumanOTPEmailCodeSentEvent {
return &HumanOTPEmailCodeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanOTPEmailCodeSentType,
),
}
}
type HumanOTPEmailCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo

View File

@ -630,6 +630,24 @@ EventTypes:
check:
succeeded: Многофакторната OTP проверка е успешна
failed: Многофакторната OTP проверка е неуспешна
sms:
added: Добавен Многофакторната OTP SMS
removed: Премахнато многофакторно OTP SMS
code:
added: Добавен многофакторен OTP SMS код
sent: Многофакторната OTP SMS код е изпратен
check:
succeeded: Успешна многофакторна OTP SMS проверка
failed: Многофакторната OTP проверка на SMS не бе успешна
email:
added: Добавен Многофакторната OTP имейл
removed: Премахнат многофакторен OTP имейл
code:
added: Добавен многофакторен OTP имейл код
sent: Многофакторният OTP имейл код е изпратен
check:
succeeded: Многофакторната еднократна имейл потвърждение е успешна
failed: Многофакторната OTP проверка на имейл не бе успешна
u2f:
token:
added: Добавен е многофакторен U2F токен

View File

@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Multifaktor OTP Verifikation erfolgreich
failed: Multifaktor OTP Verifikation fehlgeschlagen
sms:
added: Multifaktor OTP SMS hinzugefügt
removed: Multifaktor OTP SMS entfernt
code:
added: Multifaktor OTP SMS Code hinzugefügt
sent: Multifaktor OTP SMS Code versendet
check:
succeeded: Multifaktor OTP SMS Verifikation erfolgreich
failed: Multifaktor OTP SMS Verifikation fehlgeschlagen
email:
added: Multifaktor OTP Email hinzugefügt
removed: Multifaktor OTP Email entfernt
code:
added: Multifaktor OTP Email Code hinzugefügt
sent: Multifaktor OTP Email Code versendet
check:
succeeded: Multifaktor OTP Email Verifikation erfolgreich
failed: Multifaktor OTP Email Verifikation fehlgeschlagen
u2f:
token:
added: Multifaktor U2F Token hinzugefügt

View File

@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Multifactor OTP check succeeded
failed: Multifactor OTP check failed
sms:
added: Multifactor OTP SMS added
removed: Multifactor OTP SMS removed
code:
added: Multifactor OTP SMS code added
sent: Multifactor OTP SMS code sent
check:
succeeded: Multifactor OTP SMS check succeeded
failed: Multifactor OTP SMS check failed
email:
added: Multifactor OTP Email added
removed: Multifactor OTP Email removed
code:
added: Multifactor OTP Email code added
sent: Multifactor OTP Email code sent
check:
succeeded: Multifactor OTP Email check succeeded
failed: Multifactor OTP Email check failed
u2f:
token:
added: Multifactor U2F Token added

View File

@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Comprobación exitosa de Multifactor OTP
failed: Comprobación fallida de Multifactor OTP
sms:
added: Multifactor OTP SMS añadido
removed: Multifactor OTP SMS elimonado
code:
added: Código Multifactor OTP SMS añadido
sent: Código Multifactor OTP SMS enviado
check:
succeeded: Comprobación Multifactor OTP SMS exitosa
failed: Comprobación Multifactor OTP SMS fallida
email:
added: Multifactor OTP email añadido
removed: Multifactor OTP email elimonado
code:
added: Código Multifactor OTP email añadido
sent: Código Multifactor OTP email enviado
check:
succeeded: Comprobación Multifactor OTP email exitosa
failed: Comprobación Multifactor OTP email fallida
u2f:
token:
added: Multifactor U2F Token añadido

View File

@ -616,6 +616,24 @@ EventTypes:
check:
succeeded: Vérification de l'OTP multifactorielle réussie
failed: La vérification de l'OTP multifactorielle a échoué
sms:
added: Ajout de SMS OTP multifactoriels
removed: Suppression des SMS OTP multifactoriels
code:
added: Ajout du code SMS OTP multifactoriel
sent: Code SMS OTP multifacteur envoyé
check:
succeeded: Vérification par SMS OTP multifacteur réussie
failed: Échec de la vérification par SMS OTP multifacteur
email:
added: Ajout d'un e-mail OTP multifactoriel
removed: Suppression de l'e-mail OTP multifacteur
code:
added: Ajout d'un code de messagerie OTP multifactoriel
sent: Code de messagerie OTP multifacteur envoyé
check:
succeeded: Vérification de l'e-mail OTP multifacteur réussie
failed: Échec de la vérification de l'e-mail OTP multifacteur
u2f:
token:
added: Ajout d'un jeton U2F multifacteur

View File

@ -616,6 +616,24 @@ EventTypes:
check:
succeeded: Controllo OTP riuscito
failed: Controllo OTP fallito
sms:
added: Aggiunto SMS OTP
removed: OTP SMS rimosso
code:
added: Aggiunto codice OTP SMS
sent: Codice OTP SMS inviato
check:
succeeded: Controllo OTP SMS riuscito
failed: Controllo OTP SMS fallito
email:
added: Aggiunto OTP e-mail
removed: OTP e-mail rimosso
code:
added: Aggiunto codice OTP e-mail
sent: Codice OTP e-mail inviato
check:
succeeded: OTP Controllo e-mail riuscito
failed: OTP Controllo e-mail fallito
u2f:
token:
added: Aggiunto il U2F Token

View File

@ -603,6 +603,24 @@ EventTypes:
check:
succeeded: MFA OTPチェックの成功
failed: MFA OTPチェックの失敗
sms:
added: 多要素 OTP SMS を追加しました
removed: 多要素 OTP SMS を削除しました
code:
added: 多要素 OTP SMS コードを追加しました
sent: 多要素 OTP SMS コードが送信されました
check:
succeeded: 多要素 OTP SMS 検証が成功しました
failed: 多要素 OTP SMS 検証が失敗しました
email:
added: 多要素 OTP 電子メールを追加しました
removed: 多要素 OTP 電子メールを削除しました
code:
added: 多要素 OTP 電子メール コードを追加しました
sent: 多要素 OTP 電子メール コードが送信されました
check:
succeeded: 多要素 OTP 電子メール検証が成功しました
failed: 多要素 OTP 電子メール検証が失敗しました
u2f:
token:
added: MFA U2Fトークンの追加

View File

@ -614,6 +614,24 @@ EventTypes:
check:
succeeded: Проверката на мултифактор OTP е успешна
failed: Проверката на мултифактор OTP е неуспешна
sms:
added: Додадена е мултифакторна OTP SMS
removed: Отстранета мултифакторна OTP SMS
code:
added: Додаден мултифакторски OTP SMS код
sent: Мултифакторен OTP СМС-код е испратен
check:
succeeded: Мултифакторна OTP СМС-верификација е успешна
failed: Мултифакторна OTP СМС-верификација не успеа
email:
added: Додадена е повеќефакторна OTP е-пошта
removed: Отстранета мултифакторна OTP е-пошта
code:
added: Додаден мултифакторски OTP код за е-пошта
sent: Испратен е-пошта OTP-код со повеќе фактори
check:
succeeded: Успешна е-пошта OTP-верификација на мултифактор
failed: Неуспешна потврда на е-пошта OTP со повеќе фактори
u2f:
token:
added: Додаден мултифактор U2F токен

View File

@ -618,6 +618,24 @@ EventTypes:
check:
succeeded: Sprawdzenie wielofaktorowego OTP zakończone powodzeniem
failed: Sprawdzenie wielofaktorowego OTP nie powiodło się
sms:
added: Dodano wieloskładnikowy SMS OTP
removed: Usunięto wieloskładnikowy SMS OTP
code:
added: Dodano wieloczynnikowy kod SMS OTP
sent: Wysłano kod SMS Multifactor OTP
check:
succeeded: Pomyślna weryfikacja wieloczynnikowego SMS-a OTP
failed: Wieloskładnikowa weryfikacja wiadomości SMS OTP nie powiodła się
email:
added: Dodano wieloskładnikowy e-mail OTP
removed: Usunięto wieloskładnikowy e-mail OTP
code:
added: Dodano wieloskładnikowy kod e-mail OTP
sent: Wysłano kod e-mail Multifactor OTP
check:
succeeded: Pomyślna wieloczynnikowa weryfikacja adresu e-mail OTP
failed: Wieloczynnikowa weryfikacja adresu e-mail OTP nie powiodła się
u2f:
token:
added: Dodano token wielofaktorowego U2F

View File

@ -609,6 +609,24 @@ EventTypes:
check:
succeeded: Verificação de OTP de autenticação multifator bem-sucedida
failed: Verificação de OTP de autenticação multifator falhou
sms:
added: Adicionado SMS OTP multifator
removed: SMS OTP multifator removido
code:
added: Adicionado código SMS OTP multifator
sent: Código SMS OTP multifator enviado
check:
succeeded: Verificação multifator OTP SMS bem-sucedida
failed: Falha na verificação multifator OTP SMS
email:
added: Adicionado e-mail OTP multifator
removed: E-mail OTP multifator removido
code:
added: Adicionado código de e-mail OTP multifator
sent: Código de e-mail OTP multifator enviado
check:
succeeded: Verificação de e-mail OTP multifator bem-sucedida
failed: Falha na verificação de e-mail OTP multifator
u2f:
token:
added: Token U2F de autenticação multifator adicionado

View File

@ -612,6 +612,24 @@ EventTypes:
check:
succeeded: 验证 MFA OTP 成功
failed: 验证 MFA OTP 失败
sms:
added: 添加了多因素 OTP 短信
removed: 删除了多因素 OTP 短信
code:
added: 添加了多因素 OTP 短信代码
sent: 已发送多因素 OTP 短信代码
check:
succeeded: 多因素 OTP 短信验证成功
failed: 多因素 OTP 短信验证失败
email:
added: 添加了多因素 OTP 电子邮件
removed: 删除了多因素 OTP 电子邮件
code:
added: 添加了多因素 OTP 电子邮件代码
sent: 已发送多因素 OTP 电子邮件代码
check:
succeeded: 多因素 OTP 电子邮件验证成功
failed: 多因素 OTP 电子邮件验证失败
u2f:
token:
added: 添加 MFA U2F 令牌

View File

@ -49,6 +49,8 @@ type HumanView struct {
Region string
StreetAddress string
OTPState MFAState
OTPSMSAdded bool
OTPEmailAdded bool
U2FTokens []*WebAuthNView
PasswordlessTokens []*WebAuthNView
MFAMaxSetUp domain.MFALevel
@ -162,10 +164,17 @@ func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.L
}
case domain.SecondFactorTypeU2F:
types = append(types, domain.MFATypeU2F)
case domain.SecondFactorTypeOTPSMS:
if !u.OTPSMSAdded {
types = append(types, domain.MFATypeOTPSMS)
}
case domain.SecondFactorTypeOTPEmail:
if !u.OTPEmailAdded {
types = append(types, domain.MFATypeOTPEmail)
}
}
}
}
//PLANNED: add sms
}
return types
}
@ -189,10 +198,17 @@ func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPo
if u.IsU2FReady() {
types = append(types, domain.MFATypeU2F)
}
case domain.SecondFactorTypeOTPSMS:
if u.OTPSMSAdded {
types = append(types, domain.MFATypeOTPSMS)
}
case domain.SecondFactorTypeOTPEmail:
if u.OTPEmailAdded {
types = append(types, domain.MFATypeOTPEmail)
}
}
}
}
//PLANNED: add sms
}
return types, required
}

View File

@ -89,6 +89,8 @@ type HumanView struct {
Region string `json:"region" gorm:"column:region"`
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
OTPState int32 `json:"-" gorm:"column:otp_state"`
OTPSMSAdded bool `json:"-" gorm:"column:otp_sms_added"`
OTPEmailAdded bool `json:"-" gorm:"column:otp_email_added"`
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
@ -178,6 +180,8 @@ func UserToModel(user *UserView) *model.UserView {
Region: user.Region,
StreetAddress: user.StreetAddress,
OTPState: model.MFAState(user.OTPState),
OTPSMSAdded: user.OTPSMSAdded,
OTPEmailAdded: user.OTPEmailAdded,
MFAMaxSetUp: domain.MFALevel(user.MFAMaxSetUp),
MFAInitSkipped: user.MFAInitSkipped,
InitRequired: user.InitRequired,
@ -301,6 +305,8 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
user.HumanPhoneRemovedType:
u.Phone = ""
u.IsPhoneVerified = false
u.OTPSMSAdded = false
u.MFAInitSkipped = time.Time{}
case user.UserDeactivatedType:
u.State = int32(model.UserStateInactive)
case user.UserReactivatedType,
@ -326,6 +332,16 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
case user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPRemovedType:
u.OTPState = int32(model.MFAStateUnspecified)
case user.HumanOTPSMSAddedType:
u.OTPSMSAdded = true
case user.HumanOTPSMSRemovedType:
u.OTPSMSAdded = false
u.MFAInitSkipped = time.Time{}
case user.HumanOTPEmailAddedType:
u.OTPEmailAdded = true
case user.HumanOTPEmailRemovedType:
u.OTPEmailAdded = false
u.MFAInitSkipped = time.Time{}
case user.HumanU2FTokenAddedType:
err = u.addU2FToken(event)
case user.HumanU2FTokenVerifiedType:
@ -520,7 +536,8 @@ func (u *UserView) ComputeMFAMaxSetUp() {
return
}
}
if u.OTPState == int32(model.MFAStateReady) {
if u.OTPState == int32(model.MFAStateReady) ||
u.OTPSMSAdded || u.OTPEmailAdded {
u.MFAMaxSetUp = int32(domain.MFALevelSecondFactor)
return
}
@ -575,6 +592,10 @@ func (u *UserView) EventTypes() []models.EventType {
models.EventType(user.HumanMFAOTPVerifiedType),
models.EventType(user.UserV1MFAOTPRemovedType),
models.EventType(user.HumanMFAOTPRemovedType),
models.EventType(user.HumanOTPSMSAddedType),
models.EventType(user.HumanOTPSMSRemovedType),
models.EventType(user.HumanOTPEmailAddedType),
models.EventType(user.HumanOTPEmailRemovedType),
models.EventType(user.HumanU2FTokenAddedType),
models.EventType(user.HumanU2FTokenVerifiedType),
models.EventType(user.HumanU2FTokenRemovedType),

View File

@ -139,12 +139,32 @@ func (v *UserSessionView) AppendEvent(event *models.Event) error {
case user.UserV1MFAOTPCheckSucceededType,
user.HumanMFAOTPCheckSucceededType:
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeTOTP)
case user.HumanOTPSMSCheckSucceededType:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTPSMS)
}
case user.HumanOTPEmailCheckSucceededType:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreationDate, domain.MFATypeOTPEmail)
}
case user.UserV1MFAOTPCheckFailedType,
user.UserV1MFAOTPRemovedType,
user.HumanMFAOTPCheckFailedType,
user.HumanMFAOTPRemovedType,
user.HumanU2FTokenCheckFailedType,
user.HumanU2FTokenRemovedType:
user.HumanU2FTokenRemovedType,
user.HumanOTPSMSCheckFailedType,
user.HumanOTPEmailCheckFailedType:
v.SecondFactorVerification = time.Time{}
case user.HumanU2FTokenVerifiedType:
data := new(es_model.WebAuthNVerify)
@ -218,6 +238,10 @@ func (v *UserSessionView) EventTypes() []models.EventType {
models.EventType(user.UserV1MFAOTPRemovedType),
models.EventType(user.HumanMFAOTPCheckFailedType),
models.EventType(user.HumanMFAOTPRemovedType),
models.EventType(user.HumanOTPSMSCheckSucceededType),
models.EventType(user.HumanOTPSMSCheckFailedType),
models.EventType(user.HumanOTPEmailCheckSucceededType),
models.EventType(user.HumanOTPEmailCheckFailedType),
models.EventType(user.HumanU2FTokenCheckFailedType),
models.EventType(user.HumanU2FTokenRemovedType),
models.EventType(user.HumanU2FTokenVerifiedType),