mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat(login): add OTP (email and sms) (#6353)
* feat: login with otp * fix(i18n): japanese translation * add missing files * fix provider change * add event types translations to en * add tests * resourceOwner * remove unused handler * fix: secret generators and add comments * add setup step * rename * linting * fix setup * improve otp handling * fix autocomplete * translations for login and notifications * translations for event types * changes from review * check selected mfa type
This commit is contained in:
parent
faa9ed4de9
commit
7c494fd219
26
cmd/setup/12.go
Normal file
26
cmd/setup/12.go
Normal 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"
|
||||
}
|
2
cmd/setup/12/12_add_otp_columns.sql
Normal file
2
cmd/setup/12/12_add_otp_columns.sql
Normal 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;
|
@ -67,6 +67,7 @@ type Steps struct {
|
||||
s9EventstoreIndexes2 *EventstoreIndexesNew
|
||||
CorrectCreationDate *CorrectCreationDate
|
||||
AddEventCreatedAt *AddEventCreatedAt
|
||||
s12AddOTPColumns *AddOTPColumns
|
||||
}
|
||||
|
||||
type encryptionKeyConfig struct {
|
||||
|
@ -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)
|
||||
|
@ -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]],
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
125
internal/api/ui/login/mfa_init_sms.go
Normal file
125
internal/api/ui/login/mfa_init_sms.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
126
internal/api/ui/login/mfa_verify_otp_handler.go
Normal file
126
internal/api/ui/login/mfa_verify_otp_handler.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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-фактора
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
6
internal/api/ui/login/static/resources/scripts/edit.js
Normal file
6
internal/api/ui/login/static/resources/scripts/edit.js
Normal file
@ -0,0 +1,6 @@
|
||||
let form = document.getElementsByTagName('form')[0];
|
||||
let editButton = document.getElementById('edit');
|
||||
editButton.addEventListener('click', function () {
|
||||
form.submit();
|
||||
});
|
||||
|
@ -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;
|
||||
|
63
internal/api/ui/login/static/templates/mfa_init_otp_sms.html
Normal file
63
internal/api/ui/login/static/templates/mfa_init_otp_sms.html
Normal 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" .}}
|
@ -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>
|
||||
|
@ -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" .}}
|
||||
|
48
internal/api/ui/login/static/templates/mfa_verify_totp.html
Normal file
48
internal/api/ui/login/static/templates/mfa_verify_totp.html
Normal 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" .}}
|
@ -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)
|
||||
|
@ -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) }()
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -105,6 +105,8 @@ const (
|
||||
MFATypeTOTP MFAType = iota
|
||||
MFATypeU2F
|
||||
MFATypeU2FUserVerification
|
||||
MFATypeOTPSMS
|
||||
MFATypeOTPEmail
|
||||
)
|
||||
|
||||
type MFALevel int
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@ -106,6 +107,14 @@ func (u *userNotifier) reducers() []handler.AggregateReducer {
|
||||
Event: user.HumanPasswordChangedType,
|
||||
Reduce: u.reducePasswordChanged,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPSMSCodeAddedType,
|
||||
Reduce: u.reduceOTPSMSCodeAdded,
|
||||
},
|
||||
{
|
||||
Event: user.HumanOTPEmailCodeAddedType,
|
||||
Reduce: u.reduceOTPEmailCodeAdded,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -332,6 +341,136 @@ func (u *userNotifier) reducePasswordCodeAdded(event eventstore.Event) (*handler
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceOTPSMSCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPSMSCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-ASF3g", "reduce.wrong.event.type %s", user.HumanOTPSMSCodeAddedType)
|
||||
}
|
||||
ctx := HandlerContext(event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanOTPSMSCodeAddedType, user.HumanOTPSMSCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifySMSOTPMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, origin, err := u.queries.Origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notify := types.SendSMSTwilio(
|
||||
ctx,
|
||||
translator,
|
||||
notifyUser,
|
||||
u.queries.GetTwilioConfig,
|
||||
u.queries.GetFileSystemProvider,
|
||||
u.queries.GetLogProvider,
|
||||
colors,
|
||||
u.assetsPrefix(ctx),
|
||||
e,
|
||||
u.metricSuccessfulDeliveriesSMS,
|
||||
u.metricFailedDeliveriesSMS,
|
||||
)
|
||||
err = notify.SendOTPSMSCode(authz.GetInstance(ctx).RequestedDomain(), origin, code, e.Expiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = u.commands.HumanOTPSMSCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceOTPEmailCodeAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.HumanOTPEmailCodeAddedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-JL3hw", "reduce.wrong.event.type %s", user.HumanOTPEmailCodeAddedType)
|
||||
}
|
||||
ctx := HandlerContext(event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||
user.HumanOTPEmailCodeAddedType, user.HumanOTPEmailCodeSentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alreadyHandled {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.VerifyEmailOTPMessageType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, origin, err := u.queries.Origin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var authRequestID string
|
||||
if e.AuthRequestInfo != nil {
|
||||
authRequestID = e.AuthRequestInfo.ID
|
||||
}
|
||||
notify := types.SendEmail(
|
||||
ctx,
|
||||
string(template.Template),
|
||||
translator,
|
||||
notifyUser,
|
||||
u.queries.GetSMTPConfig,
|
||||
u.queries.GetFileSystemProvider,
|
||||
u.queries.GetLogProvider,
|
||||
colors,
|
||||
u.assetsPrefix(ctx),
|
||||
e,
|
||||
u.metricSuccessfulDeliveriesEmail,
|
||||
u.metricFailedDeliveriesEmail,
|
||||
)
|
||||
err = notify.SendOTPEmailCode(notifyUser, authz.GetInstance(ctx).RequestedDomain(), origin, code, authRequestID, e.Expiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = u.commands.HumanOTPEmailCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
func (u *userNotifier) reduceDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*user.DomainClaimedEvent)
|
||||
if !ok {
|
||||
@ -583,7 +722,7 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
|
||||
e,
|
||||
u.metricSuccessfulDeliveriesSMS,
|
||||
u.metricFailedDeliveriesSMS,
|
||||
).SendPhoneVerificationCode(notifyUser, origin, code)
|
||||
).SendPhoneVerificationCode(notifyUser, origin, code, authz.GetInstance(ctx).RequestedDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -40,7 +40,10 @@ VerifyEmailOTP:
|
||||
Text: Моля, използвай бутона 'Удостовери' или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
|
||||
ButtonText: Удостовери
|
||||
VerifySMSOTP:
|
||||
Text: Моля, посети {{ .VerifyURL }} или копирай временната парола {{.OTP}} и я постави на екрана за удостоверяване, за да се удостовериш в ZITADEL в рамките на следващите пет минути.
|
||||
Text: >-
|
||||
{{.OTP}} е вашата еднократна парола за {{ .Domain }}. Използвайте го в рамките на следващия {{.Expiry}}.
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Домейнът е заявен
|
||||
PreHeader: Промяна на имейл/потребителско име
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Bitte nutze den 'Authentifizieren'-Button oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
|
||||
ButtonText: Authentifizieren
|
||||
VerifySMSOTP:
|
||||
Text: Bitte besuche {{ .VerifyURL }} oder kopiere das Einmalpasswort {{.OTP}} und füge es in den Authentifizierungsbildschirm ein, um dich innerhalb der nächsten fünf Minuten bei ZITADEL zu authentifizieren.
|
||||
Text: >-
|
||||
{{.OTP}} ist Ihr Einmalpasswort für {{ .Domain }}. Verwenden Sie es innerhalb der nächsten {{.Expiry}}.
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Domain wurde beansprucht
|
||||
PreHeader: Email / Username ändern
|
||||
|
@ -31,10 +31,13 @@ VerifyEmailOTP:
|
||||
PreHeader: Verify One-Time Password
|
||||
Subject: Verify One-Time Password
|
||||
Greeting: Hello {{.DisplayName}},
|
||||
Text: Please use the "Authenticate" button or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
|
||||
Text: Please use the one-time password {{.OTP}} to authenticate at ZITADEL within the next five minutes or click the "Authenticate" button.
|
||||
ButtonText: Authenticate
|
||||
VerifySMSOTP:
|
||||
Text: Please visit {{ .VerifyURL }} or copy the one-time password {{.OTP}} and paste it to to the authentication screen in order to authenticate at ZITADEL within the next five minutes.
|
||||
Text: >-
|
||||
{{.OTP}} is your one-time-password for {{ .Domain }}. Use it within the next {{.Expiry}}.
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Domain has been claimed
|
||||
PreHeader: Change email / username
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Por favor, utiliza el botón 'Autenticar' o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
|
||||
ButtonText: Autenticar
|
||||
VerifySMSOTP:
|
||||
Text: Por favor, visita {{ .VerifyURL }} o copia la contraseña de un solo uso {{.OTP}} y pégala en la pantalla de autenticación para autenticarte en ZITADEL en los próximos cinco minutos.
|
||||
Text: >-
|
||||
{{.OTP}} es su contraseña de un solo uso para {{ .Domain }}. Úselo dentro de los próximos {{.Expiry}}.
|
||||
|
||||
@{{.Dominio}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Se ha reclamado un dominio
|
||||
PreHeader: Cambiar dirección de correo electrónico / nombre de usuario
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Utilise le bouton 'Authentifier' ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
|
||||
ButtonText: Authentifier
|
||||
VerifySMSOTP:
|
||||
Text: Visite {{ .VerifyURL }} ou copie le mot de passe à usage unique {{.OTP}} et colle-le à l'écran d'authentification pour t'authentifier sur ZITADEL dans les cinq prochaines minutes.
|
||||
Text: >-
|
||||
{{.OTP}} est votre mot de passe à usage unique pour {{ .Domain }}. Utilisez-le dans le prochain {{.Expiry}}.
|
||||
|
||||
@{{.Domaine}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Le domaine a été réclamé
|
||||
PreHeader: Modifier l'email / le nom d'utilisateur
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Per favore, utilizza il pulsante 'Autentica' o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
|
||||
ButtonText: Autentica
|
||||
VerifySMSOTP:
|
||||
Text: Per favore, visita {{ .VerifyURL }} o copia la password monouso {{.OTP}} e incollala nella schermata di autenticazione per autenticarti a ZITADEL entro i prossimi cinque minuti.
|
||||
Text: >-
|
||||
{{.OTP}} è la tua password monouso per {{ .Domain }}. Usalo entro il prossimo {{.Expiry}}.
|
||||
|
||||
@{{.Dominio}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Il dominio è stato rivendicato
|
||||
PreHeader: Cambiare email / nome utente
|
||||
|
@ -31,10 +31,13 @@ VerifyEmailOTP:
|
||||
PreHeader: ワンタイムパスワードを確認する
|
||||
Subject: ワンタイムパスワードを確認する
|
||||
Greeting: こんにちは、{{.DisplayName}}さん
|
||||
Text: '認証'ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
|
||||
Text: 認証ボタンを使用するか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
|
||||
ButtonText: 認証
|
||||
VerifySMSOTP:
|
||||
Text: {{ .VerifyURL }} を訪れるか、ワンタイムパスワード {{.OTP}} をコピーして認証画面に貼り付け、次の5分以内にZITADELで認証してください。
|
||||
Text: >-
|
||||
{{.OTP}} は、{{ .Domain }} のワンタイムパスワードです。次の {{.Expiry}} 以内に使用してください。
|
||||
|
||||
@{{.ドメイン}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - ドメインの登録
|
||||
PreHeader: メールアドレス・ユーザー名の変更
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Ве молам, користи го копчето 'Автентицирај' или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
|
||||
ButtonText: Автентицирај
|
||||
VerifySMSOTP:
|
||||
Text: Ве молам, посети го {{ .VerifyURL }} или копирај ја еднократната лозинка {{.OTP}} и стави ја на екранот за автентикација за да се автентицираш на ZITADEL во следните пет минути.
|
||||
Text: >-
|
||||
{{.OTP}} е вашата еднократна лозинка за {{ .Домен }}. Користете го во следниот {{.Истек}}.
|
||||
|
||||
@{{.Домен}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Доменот е преземен
|
||||
PreHeader: Промена на е-пошта / корисничко име
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Proszę, użyj przycisku 'Uwierzytelnij' lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
|
||||
ButtonText: Uwierzytelnij
|
||||
VerifySMSOTP:
|
||||
Text: Proszę, odwiedź {{ .VerifyURL }} lub skopiuj hasło jednorazowe {{.OTP}} i wklej go na ekran uwierzytelniania, aby uwierzytelnić się w ZITADEL w ciągu najbliższych pięciu minut.
|
||||
Text: >-
|
||||
{{.OTP}} to Twoje jednorazowe hasło do domeny {{ .Domain }}. Użyj go w ciągu najbliższych {{.Expiry}}.
|
||||
|
||||
@{{.Domena}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Domena została zarejestrowana
|
||||
PreHeader: Zmiana adresu e-mail / nazwy użytkownika
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: Por favor, usa o botão 'Autenticar' ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
|
||||
ButtonText: Autenticar
|
||||
VerifySMSOTP:
|
||||
Text: Por favor, visita {{ .VerifyURL }} ou copia a senha de uso único {{.OTP}} e cola-a na tela de autenticação para te autenticares no ZITADEL nos próximos cinco minutos.
|
||||
Text: >-
|
||||
{{.OTP}} é sua senha única para {{ .Domain }}. Use-o nos próximos {{.Expiry}}.
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - Domínio foi reivindicado
|
||||
PreHeader: Alterar e-mail / nome de usuário
|
||||
|
@ -34,7 +34,10 @@ VerifyEmailOTP:
|
||||
Text: 请使用 '验证' 按钮,或复制一次性密码 {{.OTP}} 并将其粘贴到验证屏幕中,以在接下来的五分钟内在 ZITADEL 中进行验证。
|
||||
ButtonText: 验证
|
||||
VerifySMSOTP:
|
||||
Text: 请访问 {{ .VerifyURL }} 或复制一次性密码 {{.OTP}} 并将其粘贴到身份验证屏幕,以在接下来的五分钟内在ZITADEL进行身份验证。
|
||||
Text: >-
|
||||
{{.OTP}} 是您的 {{ .Domain }} 的一次性密码。在下一个 {{.Expiry}} 内使用它。
|
||||
|
||||
@{{.Domain}} #{{.OTP}}
|
||||
DomainClaimed:
|
||||
Title: ZITADEL - 域名所有权验证
|
||||
PreHeader: 更改电子邮件/用户名
|
||||
|
29
internal/notification/types/otp.go
Normal file
29
internal/notification/types/otp.go
Normal file
@ -0,0 +1,29 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendOTPSMSCode(requestedDomain, origin, code string, expiry time.Duration) error {
|
||||
args := otpArgs(code, origin, requestedDomain, expiry)
|
||||
return notify("", args, domain.VerifySMSOTPMessageType, false)
|
||||
}
|
||||
|
||||
func (notify Notify) SendOTPEmailCode(user *query.NotifyUser, requestedDomain, origin, code, authRequestID string, expiry time.Duration) error {
|
||||
url := login.OTPLink(origin, authRequestID, code, domain.MFATypeOTPEmail)
|
||||
args := otpArgs(code, origin, requestedDomain, expiry)
|
||||
return notify(url, args, domain.VerifyEmailOTPMessageType, false)
|
||||
}
|
||||
|
||||
func otpArgs(code, origin, requestedDomain string, expiry time.Duration) map[string]interface{} {
|
||||
args := make(map[string]interface{})
|
||||
args["OTP"] = code
|
||||
args["Origin"] = origin
|
||||
args["Domain"] = requestedDomain
|
||||
args["Expiry"] = expiry
|
||||
return args
|
||||
}
|
@ -5,8 +5,9 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code string) error {
|
||||
func (notify Notify) SendPhoneVerificationCode(user *query.NotifyUser, origin, code, requestedDomain string) error {
|
||||
args := make(map[string]interface{})
|
||||
args["Code"] = code
|
||||
args["Domain"] = requestedDomain
|
||||
return notify("", args, domain.VerifyPhoneMessageType, true)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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 токен
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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トークンの追加
|
||||
|
@ -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 токен
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 令牌
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user