Skip to content

Commit

Permalink
feat: add possibility to ignore username errors on first login screen (
Browse files Browse the repository at this point in the history
…#3412)

* feat: add possibility to ignore username errors on first login screen

* ignore user not found error on password reset if IgnoreUnknownUsernames

* use constant

* correct translations

Co-authored-by: Max Peintner <max@caos.ch>

Co-authored-by: Max Peintner <max@caos.ch>
  • Loading branch information
livio-a and peintnermax committed Apr 6, 2022
1 parent 8e50a94 commit 2721441
Show file tree
Hide file tree
Showing 43 changed files with 599 additions and 231 deletions.
Expand Up @@ -147,7 +147,6 @@
</ng-template>
</div>
<div class="row">

<mat-slide-toggle class="toggle" color="primary"
[disabled]="disabled || serviceType === PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) === false"
ngDefaultControl [(ngModel)]="loginData.hidePasswordReset">
Expand All @@ -166,6 +165,17 @@
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary"
[disabled]="disabled"
ngDefaultControl [(ngModel)]="loginData.ignoreUnknownUsernames">
{{'POLICY.DATA.IGNOREUNKNOWNUSERNAMES' | translate}}
</mat-slide-toggle>

<cnsl-info-section class="info">
{{'POLICY.DATA.IGNOREUNKNOWNUSERNAMES_DESC' | translate}}
</cnsl-info-section>
</div>

<div class="row">
<cnsl-form-field class="form-field" label="Access Code" required="true">
Expand Down Expand Up @@ -205,4 +215,4 @@

<cnsl-policy-grid class="grid" [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="security">
</cnsl-policy-grid>
</cnsl-detail-layout>
</cnsl-detail-layout>
Expand Up @@ -107,6 +107,7 @@ export class LoginPolicyComponent implements OnDestroy {
mgmtreq.setForceMfa(this.loginData.forceMfa);
mgmtreq.setPasswordlessType(this.loginData.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset);
mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
if ((this.loginData as LoginPolicy.AsObject).isDefault) {
return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq);
} else {
Expand All @@ -120,6 +121,7 @@ export class LoginPolicyComponent implements OnDestroy {
adminreq.setForceMfa(this.loginData.forceMfa);
adminreq.setPasswordlessType(this.loginData.passwordlessType);
adminreq.setHidePasswordReset(this.loginData.hidePasswordReset);
adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);

return (this.service as AdminService).updateLoginPolicy(adminreq);
}
Expand Down
2 changes: 2 additions & 0 deletions console/src/assets/i18n/de.json
Expand Up @@ -992,6 +992,8 @@
"HIDEPASSWORDRESET": "Passwort vergessen ausblenden",
"HIDEPASSWORDRESET_DESC": "Ist die Option gewählt, ist es nicht möglich im Login das Passwort zurück zusetzen via Passwort vergessen Link.",
"HIDELOGINNAMESUFFIX": "Loginname Suffix ausblenden",
"IGNOREUNKNOWNUSERNAMES": "Unbekannte Usernamen ignorieren",
"IGNOREUNKNOWNUSERNAMES_DESC": "Ist die Option gewählt, wird der Passwort Schritt im Login auch angezeigt wenn der User nicht gefunden wurde. Dem Benutzer wird auf bei der Passwortprüfung nicht angezeigt ob der Username oder das Passwort falsch war.",
"ERRORMSGPOPUP": "Fehler als Dialog Fenster",
"DISABLEWATERMARK": "Wasserzeichen ausblenden"
},
Expand Down
2 changes: 2 additions & 0 deletions console/src/assets/i18n/en.json
Expand Up @@ -991,6 +991,8 @@
"FORCEMFA_DESC": "If the option is selected, users have to configure a second factor for login.",
"HIDEPASSWORDRESET": "Hide Password reset",
"HIDEPASSWORDRESET_DESC": "If the option is selected, the user can't reset his password in the login process.",
"IGNOREUNKNOWNUSERNAMES": "Ignore unknown usernames",
"IGNOREUNKNOWNUSERNAMES_DESC": "If the option is selected, the password screen will be displayed in the login process even if the user was not found. The error on the password check will not disclose if the username or password was wrong.",
"HIDELOGINNAMESUFFIX": "Hide Loginname suffix",
"ERRORMSGPOPUP": "Show Error in Dialog",
"DISABLEWATERMARK": "Hide Watermark"
Expand Down
2 changes: 2 additions & 0 deletions console/src/assets/i18n/it.json
Expand Up @@ -991,6 +991,8 @@
"FORCEMFA_DESC": "Se l'opzione \u00e8 selezionata, gli utenti devono configurare un secondo fattore per il login.",
"HIDEPASSWORDRESET": "Nascondi ripristino della password",
"HIDEPASSWORDRESET_DESC": "Se l'opzione \u00e8 selezionata, l'utente non pu\u00f2 resettare la sua password nel interfaccia login.",
"IGNOREUNKNOWNUSERNAMES": "Ignora un nome utente sconosciuto",
"IGNOREUNKNOWNUSERNAMES_DESC": "Se l'opzione \u00e8 selezionata, l'inserimento della password viene mostrato anche se nessun utente è stato trovato. Nota che dopo il controllo della password, non viene mostrato se il nome utente o la password erano errati.",
"HIDELOGINNAMESUFFIX": "Nascondi il suffisso del nome utente",
"ERRORMSGPOPUP": "Mostra l'errore nella finestra di dialogo",
"DISABLEWATERMARK": "Nascondi la filigrana"
Expand Down
1 change: 1 addition & 0 deletions docs/docs/apis/proto/admin.md
Expand Up @@ -3266,6 +3266,7 @@ This is an empty request
| force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |



Expand Down
2 changes: 2 additions & 0 deletions docs/docs/apis/proto/management.md
Expand Up @@ -3038,6 +3038,7 @@ This is an empty request
| force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |



Expand Down Expand Up @@ -7754,6 +7755,7 @@ This is an empty request
| force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |



Expand Down
1 change: 1 addition & 0 deletions docs/docs/apis/proto/policy.md
Expand Up @@ -63,6 +63,7 @@ title: zitadel/policy.proto
| passwordless_type | PasswordlessType | - | |
| is_default | bool | - | |
| hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |



Expand Down
13 changes: 7 additions & 6 deletions internal/api/grpc/admin/login_policy_converter.go
Expand Up @@ -10,12 +10,13 @@ import (

func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.LoginPolicy {
return &domain.LoginPolicy{
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
}
}

Expand Down
26 changes: 14 additions & 12 deletions internal/api/grpc/management/policy_login_converter.go
Expand Up @@ -10,23 +10,25 @@ import (

func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.LoginPolicy {
return &domain.LoginPolicy{
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
}
}

func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domain.LoginPolicy {
return &domain.LoginPolicy{
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
AllowUsernamePassword: p.AllowUsernamePassword,
AllowRegister: p.AllowRegister,
AllowExternalIDP: p.AllowExternalIdp,
ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
}
}

Expand Down
18 changes: 10 additions & 8 deletions internal/api/grpc/policy/login_policy.go
@@ -1,22 +1,24 @@
package policy

import (
timestamp_pb "google.golang.org/protobuf/types/known/timestamppb"

"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/object"
policy_pb "github.com/caos/zitadel/pkg/grpc/policy"
timestamp_pb "google.golang.org/protobuf/types/known/timestamppb"
)

func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
return &policy_pb.LoginPolicy{
IsDefault: policy.IsDefault,
AllowUsernamePassword: policy.AllowUsernamePassword,
AllowRegister: policy.AllowRegister,
AllowExternalIdp: policy.AllowExternalIDPs,
ForceMfa: policy.ForceMFA,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
HidePasswordReset: policy.HidePasswordReset,
IsDefault: policy.IsDefault,
AllowUsernamePassword: policy.AllowUsernamePassword,
AllowRegister: policy.AllowRegister,
AllowExternalIdp: policy.AllowExternalIDPs,
ForceMfa: policy.ForceMFA,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
Details: &object.ObjectDetails{
Sequence: policy.Sequence,
CreationDate: timestamp_pb.New(policy.CreationDate),
Expand Down
55 changes: 39 additions & 16 deletions internal/auth/repository/eventsourcing/eventstore/auth_request.go
Expand Up @@ -26,6 +26,8 @@ import (
user_view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)

const unknownUserID = "UNKNOWN"

type AuthRequestRepo struct {
Command *command.Commands
Query *query.Queries
Expand Down Expand Up @@ -290,7 +292,7 @@ func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAge
if err != nil {
return err
}
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, userID)
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, userID, false)
if err != nil {
return err
}
Expand All @@ -310,6 +312,9 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, res
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, id, userAgentID, userID)
if err != nil {
if isIgnoreUserNotFoundError(err, request) {
return errors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid")
}
return err
}
policy, err := repo.getLockoutPolicy(ctx, resourceOwner)
Expand All @@ -319,6 +324,10 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, res
return repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info), lockoutPolicyToDomain(policy))
}

func isIgnoreUserNotFoundError(err error, request *domain.AuthRequest) bool {
return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) && errors.Contains(err, "Errors.User.NotFound")
}

func lockoutPolicyToDomain(policy *query.LockoutPolicy) *domain.LockoutPolicy {
return &domain.LockoutPolicy{
ObjectRoot: es_models.ObjectRoot{
Expand Down Expand Up @@ -489,9 +498,9 @@ func (repo *AuthRequestRepo) getAuthRequestEnsureUser(ctx context.Context, authR
if request.UserID != userID {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-GBH32", "Errors.User.NotMatchingUserID")
}
_, err = activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID)
_, err = activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, false)
if err != nil {
return nil, err
return request, err
}
return request, nil
}
Expand Down Expand Up @@ -618,6 +627,13 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
}
}
}
if request.LoginPolicy.IgnoreUnknownUsernames {
if err != nil && !errors.IsNotFound(err) {
return err
}
request.SetUserInfo(unknownUserID, loginName, loginName, "", "", request.RequestedOrgID)
return nil
}
if err != nil {
return err
}
Expand Down Expand Up @@ -656,15 +672,16 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
CreationDate: policy.CreationDate,
ChangeDate: policy.ChangeDate,
},
Default: policy.IsDefault,
AllowUsernamePassword: policy.AllowUsernamePassword,
AllowRegister: policy.AllowRegister,
AllowExternalIDP: policy.AllowExternalIDPs,
ForceMFA: policy.ForceMFA,
SecondFactors: policy.SecondFactors,
MultiFactors: policy.MultiFactors,
PasswordlessType: policy.PasswordlessType,
HidePasswordReset: policy.HidePasswordReset,
Default: policy.IsDefault,
AllowUsernamePassword: policy.AllowUsernamePassword,
AllowRegister: policy.AllowRegister,
AllowExternalIDP: policy.AllowExternalIDPs,
ForceMFA: policy.ForceMFA,
SecondFactors: policy.SecondFactors,
MultiFactors: policy.MultiFactors,
PasswordlessType: policy.PasswordlessType,
HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
}
}

Expand All @@ -688,7 +705,7 @@ func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request
if err != nil {
return err
}
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID)
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID, false)
if err != nil {
return err
}
Expand Down Expand Up @@ -734,7 +751,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
}
return steps, nil
}
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID)
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, request.LoginPolicy.IgnoreUnknownUsernames)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1114,10 +1131,16 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
return user_view_model.UserSessionToModel(&sessionCopy, provider.PrefixAvatarURL()), nil
}

func activeUserByID(ctx context.Context, userViewProvider userViewProvider, userEventProvider userEventProvider, queries orgViewProvider, lockoutPolicyProvider lockoutPolicyViewProvider, userID string) (*user_model.UserView, error) {
func activeUserByID(ctx context.Context, userViewProvider userViewProvider, userEventProvider userEventProvider, queries orgViewProvider, lockoutPolicyProvider lockoutPolicyViewProvider, userID string, ignoreUnknownUsernames bool) (user *user_model.UserView, err error) {
// PLANNED: Check LockoutPolicy
user, err := userByID(ctx, userViewProvider, userEventProvider, userID)
user, err = userByID(ctx, userViewProvider, userEventProvider, userID)
if err != nil {
if ignoreUnknownUsernames && errors.IsNotFound(err) {
return &user_model.UserView{
ID: userID,
HumanView: &user_model.HumanView{},
}, nil
}
return nil, err
}

Expand Down

0 comments on commit 2721441

Please sign in to comment.