fix: allow login with user created through v2 api without password (#8291)

# Which Problems Are Solved

User created through the User V2 API without any authentication method
and possibly unverified email address was not able to login through the
current hosted login UI.

An unverified email address would result in a mail verification and not
an initialization mail like it would with the management API. Also the
login UI would then require the user to enter the init code, which the
user never received.

# How the Problems Are Solved

- When verifying the email through the login UI, it will check for
existing auth methods (password, IdP, passkeys). In case there are none,
the user will be prompted to set a password.
- When a user was created through the V2 API with a verified email and
no auth method, the user will be prompted to set a password in the login
UI.
- Since setting a password requires a corresponding code, the code will
be generated and sent when login in.

# Additional Changes

- Changed `RequestSetPassword` to get the codeGenerator from the
eventstore instead of getting it from query.

# Additional Context

- closes https://github.com/zitadel/zitadel/issues/6600
- closes https://github.com/zitadel/zitadel/issues/8235

---------

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Livio Spring
2024-07-17 06:43:07 +02:00
committed by GitHub
parent e126ccc9aa
commit 07b2bac463
17 changed files with 465 additions and 107 deletions

View File

@@ -50,7 +50,7 @@ func (s *Server) VerifyMyEmail(ctx context.Context, req *auth_pb.VerifyMyEmailRe
return nil, err
}
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, emailCodeGenerator)
objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, "", "", emailCodeGenerator)
if err != nil {
return nil, err
}

View File

@@ -586,11 +586,7 @@ func (s *Server) SetHumanPassword(ctx context.Context, req *mgmt_pb.SetHumanPass
}
func (s *Server) SendHumanResetPasswordNotification(ctx context.Context, req *mgmt_pb.SendHumanResetPasswordNotificationRequest) (*mgmt_pb.SendHumanResetPasswordNotificationResponse, error) {
passwordCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordResetCode, s.userCodeAlg)
if err != nil {
return nil, err
}
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), passwordCodeGenerator, "")
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), "")
if err != nil {
return nil, err
}

View File

@@ -97,12 +97,7 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
userID = authReq.UserID
authReqID = authReq.ID
}
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil {
l.renderInitPassword(w, r, authReq, userID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator, authReqID)
_, err := l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, authReqID)
l.renderInitPassword(w, r, authReq, userID, "", err)
}

View File

@@ -1,10 +1,16 @@
package login
import (
"context"
"net/http"
"net/url"
"github.com/zitadel/logging"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
@@ -16,15 +22,25 @@ const (
)
type mailVerificationFormData struct {
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
PasswordInit bool `schema:"passwordInit"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
}
type mailVerificationData struct {
baseData
profileData
UserID string
UserID string
Code string
PasswordInit bool
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
func MailVerificationLink(origin, userID, code, orgID, authRequestID string) string {
@@ -40,11 +56,32 @@ func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryUserID)
code := r.FormValue(queryCode)
if code != "" {
l.checkMailCode(w, r, authReq, userID, code)
if userID == "" && authReq == nil {
l.renderError(w, r, authReq, nil)
return
}
l.renderMailVerification(w, r, authReq, userID, nil)
if userID == "" {
userID = authReq.UserID
}
passwordInit := l.checkUserNoFirstFactor(r.Context(), userID)
if code != "" && !passwordInit {
l.checkMailCode(w, r, authReq, userID, code, "")
return
}
l.renderMailVerification(w, r, authReq, userID, code, passwordInit, nil)
}
func (l *Login) checkUserNoFirstFactor(ctx context.Context, userID string) bool {
userIDQuery, err := query.NewUserAuthMethodUserIDSearchQuery(userID)
logging.WithFields("userID", userID).OnError(err).Warn("error creating NewUserAuthMethodUserIDSearchQuery")
authMethodsQuery, err := query.NewUserAuthMethodTypesSearchQuery(domain.UserAuthMethodTypeIDP, domain.UserAuthMethodTypePassword, domain.UserAuthMethodTypePasswordless)
logging.WithFields("userID", userID).OnError(err).Warn("error creating NewUserAuthMethodTypesSearchQuery")
authMethods, err := l.query.SearchUserAuthMethods(ctx, &query.UserAuthMethodSearchQueries{Queries: []query.SearchQuery{userIDQuery, authMethodsQuery}}, false)
if err != nil {
logging.WithFields("userID", userID).OnError(err).Warn("unable to load user's auth methods for mail verification")
return false
}
return len(authMethods.AuthMethods) == 0
}
func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Request) {
@@ -55,7 +92,12 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
return
}
if !data.Resend {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
if data.PasswordInit && data.Password != data.PasswordConfirm {
err := zerrors.ThrowInvalidArgument(nil, "VIEW-fsdfd", "Errors.User.Password.ConfirmationWrong")
l.renderMailVerification(w, r, authReq, data.UserID, data.Code, data.PasswordInit, err)
return
}
l.checkMailCode(w, r, authReq, data.UserID, data.Code, data.Password)
return
}
var userOrg, authReqID string
@@ -65,14 +107,14 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
l.renderMailVerification(w, r, authReq, data.UserID, "", data.PasswordInit, err)
return
}
_, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator, authReqID)
l.renderMailVerification(w, r, authReq, data.UserID, err)
l.renderMailVerification(w, r, authReq, data.UserID, "", data.PasswordInit, err)
}
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string) {
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code, password string) {
userOrg := ""
if authReq != nil {
userID = authReq.UserID
@@ -80,31 +122,52 @@ func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *d
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
l.renderMailVerification(w, r, authReq, userID, "", password != "", err)
return
}
_, err = l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg, emailCodeGenerator)
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err = l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg, password, userAgentID, emailCodeGenerator)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
l.renderMailVerification(w, r, authReq, userID, "", password != "", err)
return
}
l.renderMailVerified(w, r, authReq, userOrg)
}
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID string, err error) {
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string, passwordInit bool, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if userID == "" {
if userID == "" && authReq != nil {
userID = authReq.UserID
}
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
Code: code,
PasswordInit: passwordInit,
}
if passwordInit {
policy := l.getPasswordComplexityPolicyByUserID(r, userID)
if policy != nil {
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
}
if authReq == nil {
user, err := l.query.GetUserByID(r.Context(), false, userID)

View File

@@ -25,15 +25,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderPasswordResetDone(w, r, authReq, err)
return
}
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) {
err = nil
}
l.renderPasswordResetDone(w, r, authReq, err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, passwordCodeGenerator, authReq.ID)
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, authReq.ID)
l.renderPasswordResetDone(w, r, authReq, err)
}

View File

@@ -313,7 +313,7 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
case *domain.ChangePasswordStep:
l.renderChangePassword(w, r, authReq, err)
case *domain.VerifyEMailStep:
l.renderMailVerification(w, r, authReq, "", err)
l.renderMailVerification(w, r, authReq, authReq.UserID, "", step.InitPassword, err)
case *domain.MFAPromptStep:
l.renderMFAPrompt(w, r, authReq, step, err)
case *domain.InitUserStep:

View File

@@ -17,13 +17,29 @@
<div class="fields">
<label class="lgn-label" for="code">{{t "EmailVerification.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" autofocus required>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" value="{{ .Code }}" {{if not .Code}}autofocus{{end}} required>
</div>
{{ if .PasswordInit }}
<div class="field">
<label class="lgn-label" for="password">{{t "InitUser.NewPasswordLabel"}}</label>
<input data-minlength="{{ .MinLength }}" data-has-uppercase="{{ .HasUppercase }}"
data-has-lowercase="{{ .HasLowercase }}" data-has-number="{{ .HasNumber }}"
data-has-symbol="{{ .HasSymbol }}" class="lgn-input" type="password" id="password" name="password"
autocomplete="new-password" autofocus required>
</div>
<div class="field">
<label class="lgn-label" for="passwordconfirm">{{t "InitUser.NewPasswordConfirm"}}</label>
<input class="lgn-input" type="password" id="passwordconfirm" name="passwordconfirm"
autocomplete="new-password" autofocus required>
{{ template "password-complexity-policy-description" . }}
</div>
{{ end }}
{{ template "error-message" .}}
<div class="lgn-actions lgn-reverse-order">
<button type="submit" id="submit-button" name="resend" value="false"
<button type="submit" id="init-button" name="resend" value="false"
class="lgn-primary lgn-raised-button">{{t "EmailVerification.NextButtonText"}}
</button>
@@ -40,6 +56,7 @@
</div>
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
<script src="{{ resourceUrl "scripts/password_policy_check.js" }}"></script>
<script src="{{ resourceUrl "scripts/init_password_check.js" }}"></script>
{{template "main-bottom" .}}