diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index e2d94e9d47..62ecf18f7d 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -2,6 +2,7 @@ package auth import ( "context" + "github.com/golang/protobuf/ptypes/empty" "github.com/caos/zitadel/pkg/grpc/auth" @@ -175,6 +176,14 @@ func (s *Server) RemoveMyMfaU2F(ctx context.Context, id *auth.WebAuthNTokenID) ( return &empty.Empty{}, err } +func (s *Server) GetMyPasswordless(ctx context.Context, _ *empty.Empty) (_ *auth.WebAuthNTokens, err error) { + tokens, err := s.repo.GetMyPasswordless(ctx) + if err != nil { + return nil, err + } + return webAuthNTokensFromModel(tokens), err +} + func (s *Server) AddMyPasswordless(ctx context.Context, _ *empty.Empty) (_ *auth.WebAuthNResponse, err error) { u2f, err := s.repo.AddMyPasswordless(ctx) return verifyWebAuthNFromModel(u2f), err diff --git a/internal/api/grpc/auth/user_converter.go b/internal/api/grpc/auth/user_converter.go index 21e56f79fa..3251320201 100644 --- a/internal/api/grpc/auth/user_converter.go +++ b/internal/api/grpc/auth/user_converter.go @@ -436,3 +436,19 @@ func verifyWebAuthNFromModel(u2f *usr_model.WebAuthNToken) *auth.WebAuthNRespons State: mfaStateFromModel(u2f.State), } } + +func webAuthNTokensFromModel(tokens []*usr_model.WebAuthNToken) *auth.WebAuthNTokens { + result := make([]*auth.WebAuthNToken, len(tokens)) + for i, token := range tokens { + result[i] = webAuthNTokenFromModel(token) + } + return &auth.WebAuthNTokens{Tokens: result} +} + +func webAuthNTokenFromModel(token *usr_model.WebAuthNToken) *auth.WebAuthNToken { + return &auth.WebAuthNToken{ + Id: token.WebAuthNTokenID, + Name: token.WebAuthNTokenName, + State: mfaStateFromModel(token.State), + } +} diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index a4d9ad2cd0..b7151f070d 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -2,9 +2,11 @@ package management import ( "context" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/pkg/grpc/management" - "github.com/golang/protobuf/ptypes/empty" ) func (s *Server) GetUserByID(ctx context.Context, id *management.UserID) (*management.UserView, error) { @@ -231,6 +233,19 @@ func (s *Server) RemoveMfaU2F(ctx context.Context, webAuthNTokenID *management.W return &empty.Empty{}, err } +func (s *Server) GetPasswordless(ctx context.Context, userID *management.UserID) (_ *management.WebAuthNTokens, err error) { + tokens, err := s.user.GetPasswordless(ctx, userID.Id) + if err != nil { + return nil, err + } + return webAuthNTokensFromModel(tokens), err +} + +func (s *Server) RemovePasswordless(ctx context.Context, id *management.WebAuthNTokenID) (*empty.Empty, error) { + err := s.user.RemovePasswordless(ctx, id.UserId, id.Id) + return &empty.Empty{}, err +} + func (s *Server) SearchUserMemberships(ctx context.Context, in *management.UserMembershipSearchRequest) (*management.UserMembershipSearchResponse, error) { request := userMembershipSearchRequestsToModel(in) request.AppendUserIDQuery(in.UserId) diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 6b1dfe07eb..ced79f4948 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -629,3 +629,19 @@ func userChangesToMgtAPI(changes *usr_model.UserChanges) (_ []*management.Change return result } + +func webAuthNTokensFromModel(tokens []*usr_model.WebAuthNToken) *management.WebAuthNTokens { + result := make([]*management.WebAuthNToken, len(tokens)) + for i, token := range tokens { + result[i] = webAuthNTokenFromModel(token) + } + return &management.WebAuthNTokens{Tokens: result} +} + +func webAuthNTokenFromModel(token *usr_model.WebAuthNToken) *management.WebAuthNToken { + return &management.WebAuthNToken{ + Id: token.WebAuthNTokenID, + Name: token.WebAuthNTokenName, + State: mfaStateFromModel(token.State), + } +} diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index 7fffb728d8..1421b97929 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -326,10 +326,18 @@ func (repo *UserRepo) RemoveMyMFAU2F(ctx context.Context, webAuthNTokenID string return repo.UserEvents.RemoveU2FToken(ctx, authz.GetCtxData(ctx).UserID, webAuthNTokenID) } +func (repo *UserRepo) GetPasswordless(ctx context.Context, userID string) ([]*model.WebAuthNToken, error) { + return repo.UserEvents.GetPasswordless(ctx, userID) +} + func (repo *UserRepo) AddPasswordless(ctx context.Context, userID string) (*model.WebAuthNToken, error) { return repo.UserEvents.AddPasswordless(ctx, userID, true) } +func (repo *UserRepo) GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNToken, error) { + return repo.UserEvents.GetPasswordless(ctx, authz.GetCtxData(ctx).UserID) +} + func (repo *UserRepo) AddMyPasswordless(ctx context.Context) (*model.WebAuthNToken, error) { return repo.UserEvents.AddPasswordless(ctx, authz.GetCtxData(ctx).UserID, false) } diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index 1399a2b6a6..8ae79d92d2 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -32,6 +32,7 @@ type UserRepository interface { VerifyMFAU2FSetup(ctx context.Context, userID, tokenName, userAgentID string, credentialData []byte) error RemoveMFAU2F(ctx context.Context, userID, webAuthNTokenID string) error + GetPasswordless(ctx context.Context, id string) ([]*model.WebAuthNToken, error) AddPasswordless(ctx context.Context, id string) (*model.WebAuthNToken, error) VerifyPasswordlessSetup(ctx context.Context, userID, tokenName, userAgentID string, credentialData []byte) error RemovePasswordless(ctx context.Context, userID, webAuthNTokenID string) error @@ -80,6 +81,7 @@ type myUserRepo interface { VerifyMyMFAU2FSetup(ctx context.Context, tokenName string, data []byte) error RemoveMyMFAU2F(ctx context.Context, webAuthNTokenID string) error + GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNToken, error) AddMyPasswordless(ctx context.Context) (*model.WebAuthNToken, error) VerifyMyPasswordlessSetup(ctx context.Context, tokenName string, data []byte) error RemoveMyPasswordless(ctx context.Context, webAuthNTokenID string) error diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index 08e5ced20f..4a8148c52c 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -235,6 +235,14 @@ func (repo *UserRepo) RemoveU2F(ctx context.Context, userID, webAuthNTokenID str return repo.UserEvents.RemoveU2FToken(ctx, userID, webAuthNTokenID) } +func (repo *UserRepo) GetPasswordless(ctx context.Context, userID string) ([]*usr_model.WebAuthNToken, error) { + return repo.UserEvents.GetPasswordless(ctx, userID) +} + +func (repo *UserRepo) RemovePasswordless(ctx context.Context, userID, webAuthNTokenID string) error { + return repo.UserEvents.RemovePasswordlessToken(ctx, userID, webAuthNTokenID) +} + func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { policy, err := repo.View.PasswordComplexityPolicyByAggregateID(authz.GetCtxData(ctx).OrgID) if err != nil && caos_errs.IsNotFound(err) { diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go index b1a23581d9..da8d81b359 100644 --- a/internal/management/repository/user.go +++ b/internal/management/repository/user.go @@ -34,6 +34,9 @@ type UserRepository interface { RemoveOTP(ctx context.Context, userID string) error RemoveU2F(ctx context.Context, userID, webAuthNTokenID string) error + GetPasswordless(ctx context.Context, userID string) ([]*model.WebAuthNToken, error) + RemovePasswordless(ctx context.Context, userID, webAuthNTokenID string) error + SearchExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) RemoveExternalIDP(ctx context.Context, externalIDP *model.ExternalIDP) error diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 03810e5730..2c065366e0 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -446,9 +446,16 @@ EventTypes: login: added: Login Richtlinie hinzugefügt changed: Login Richtlinie geändert + removed: Login Richtline gelöscht idpprovider: added: Idp Provider zu Login Richtlinie hinzugefügt removed: Idp Provider aus Login Richtlinie gelöscht + secondfactor: + added: Zweitfaktor zu Login Richtlinie hinzugefügt + removed: Zweitfaktor aus Login Richtlinie gelöscht + multifactor: + added: Multifaktor zu Login Richtlinie hinzugefügt + removed: Multifaktor aus Login Richtlinie gelöscht password: complexity: added: Passwort Komplexitäts Richtlinie hinzugefügt diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 637845077b..4654927508 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -446,9 +446,16 @@ EventTypes: login: added: Login Policy added changed: Login Policy changed + removed: Login Policy removed idpprovider: added: Idp Provider added to Login Policy removed: Idp Provider removed from Login Policy + secondfactor: + added: Second factor added to Login Policy + removed: Second factor removed from Login Policy + multifactor: + added: Multi factor added to Login Policy + removed: Multi factor removed from Login Policy password: complexity: added: Password complexity policy added diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go index 019c92bbe3..24d3efeaf6 100644 --- a/internal/user/model/user_view.go +++ b/internal/user/model/user_view.go @@ -136,16 +136,6 @@ func (u *UserView) MFATypesSetupPossible(level req_model.MFALevel, policy *iam_m } } //PLANNED: add sms - fallthrough - case req_model.MFALevelMultiFactor: - if policy.HasMultiFactors() { - for _, mfaType := range policy.MultiFactors { - switch mfaType { - case iam_model.MultiFactorTypeU2FWithPIN: - types = append(types, req_model.MFATypeU2FUserVerification) - } - } - } } return types } diff --git a/internal/user/repository/eventsourcing/eventstore.go b/internal/user/repository/eventsourcing/eventstore.go index 447351435f..bae424ea58 100644 --- a/internal/user/repository/eventsourcing/eventstore.go +++ b/internal/user/repository/eventsourcing/eventstore.go @@ -1410,6 +1410,14 @@ func (es *UserEventstore) VerifyMFAU2F(ctx context.Context, userID string, crede return finishErr } +func (es *UserEventstore) GetPasswordless(ctx context.Context, userID string) ([]*usr_model.WebAuthNToken, error) { + user, err := es.HumanByID(ctx, userID) + if err != nil { + return nil, err + } + return user.PasswordlessTokens, nil +} + func (es *UserEventstore) AddPasswordless(ctx context.Context, userID string, isLoginUI bool) (*usr_model.WebAuthNToken, error) { user, err := es.HumanByID(ctx, userID) if err != nil { diff --git a/pkg/grpc/auth/proto/auth.proto b/pkg/grpc/auth/proto/auth.proto index 70490f9f22..645a03af68 100644 --- a/pkg/grpc/auth/proto/auth.proto +++ b/pkg/grpc/auth/proto/auth.proto @@ -337,6 +337,15 @@ service AuthService { }; } + rpc GetMyPasswordless(google.protobuf.Empty) returns (WebAuthNTokens) { + option (google.api.http) = { + get: "/users/me/passwordless" + }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + rpc AddMyPasswordless(google.protobuf.Empty) returns (WebAuthNResponse) { option (google.api.http) = { post: "/users/me/passwordless" @@ -666,6 +675,16 @@ message MfaOtpResponse { MFAState state = 4; } +message WebAuthNTokens { + repeated WebAuthNToken tokens = 1; +} + +message WebAuthNToken { + string id = 1; + string name = 2; + MFAState state = 3; +} + message WebAuthNResponse { string id = 1; bytes public_key = 2; diff --git a/pkg/grpc/management/proto/management.proto b/pkg/grpc/management/proto/management.proto index e385818c93..a1a723e9fb 100644 --- a/pkg/grpc/management/proto/management.proto +++ b/pkg/grpc/management/proto/management.proto @@ -409,6 +409,26 @@ service ManagementService { }; } + rpc GetPasswordless(UserID) returns (WebAuthNTokens) { + option (google.api.http) = { + get: "/users/{id}/passwordless" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "user.read" + }; + } + + rpc RemovePasswordless(WebAuthNTokenID) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/users/{user_id}/passwordless" + }; + + option (caos.zitadel.utils.v1.auth_option) = { + permission: "user.write" + }; + } + // Sends an Notification (Email/SMS) with a password reset Link rpc SendSetPasswordNotification(SetPasswordNotificationRequest) returns (google.protobuf.Empty) { option (google.api.http) = { @@ -1656,6 +1676,16 @@ message UserID { string id = 1 [(validate.rules).string.min_len = 1]; } +message WebAuthNTokens { + repeated WebAuthNToken tokens = 1; +} + +message WebAuthNToken { + string id = 1; + string name = 2; + MFAState state = 3; +} + message WebAuthNTokenID { string user_id = 1 [(validate.rules).string.min_len = 1]; string id = 2 [(validate.rules).string.min_len = 1]; @@ -3097,6 +3127,7 @@ enum PasswordlessType { PASSWORDLESSTYPE_ALLOWED = 1; } + message IdpProviderID { string idp_config_id = 1 [(validate.rules).string = {min_len: 1}]; }