From a3d80f93ff824241c34d895412141895812544c2 Mon Sep 17 00:00:00 2001 From: conblem Date: Thu, 2 Jan 2025 14:14:49 +0100 Subject: [PATCH] feat: v2 api add way to list authentication factors (#9065) # Which Problems Are Solved The v2 api currently has no endpoint the get all second factors of a user. # How the Problems Are Solved Our v1 api has the ListHumanAuthFactors which got added to the v2 api under the User resource. # Additional Changes # Additional Context Closes #8833 --------- Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> --- internal/api/grpc/object/v2/converter.go | 103 ++++++++ .../user/v2/integration_test/user_test.go | 243 ++++++++++++++++++ internal/api/grpc/user/v2/user.go | 33 +++ internal/integration/client.go | 20 +- internal/query/user_auth_method.go | 17 ++ proto/zitadel/user/v2/user.proto | 47 ++++ proto/zitadel/user/v2/user_service.proto | 57 ++++ 7 files changed, 518 insertions(+), 2 deletions(-) diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go index fe8aba5d6e..8cf0d8b1fa 100644 --- a/internal/api/grpc/object/v2/converter.go +++ b/internal/api/grpc/object/v2/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user_pb "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -70,3 +71,105 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func AuthMethodsToPb(mfas *query.AuthMethods) []*user_pb.AuthFactor { + factors := make([]*user_pb.AuthFactor, len(mfas.AuthMethods)) + for i, mfa := range mfas.AuthMethods { + factors[i] = AuthMethodToPb(mfa) + } + return factors +} + +func AuthMethodToPb(mfa *query.AuthMethod) *user_pb.AuthFactor { + factor := &user_pb.AuthFactor{ + State: MFAStateToPb(mfa.State), + } + switch mfa.Type { + case domain.UserAuthMethodTypeTOTP: + factor.Type = &user_pb.AuthFactor_Otp{ + Otp: &user_pb.AuthFactorOTP{}, + } + case domain.UserAuthMethodTypeU2F: + factor.Type = &user_pb.AuthFactor_U2F{ + U2F: &user_pb.AuthFactorU2F{ + Id: mfa.TokenID, + Name: mfa.Name, + }, + } + case domain.UserAuthMethodTypeOTPSMS: + factor.Type = &user_pb.AuthFactor_OtpSms{ + OtpSms: &user_pb.AuthFactorOTPSMS{}, + } + case domain.UserAuthMethodTypeOTPEmail: + factor.Type = &user_pb.AuthFactor_OtpEmail{ + OtpEmail: &user_pb.AuthFactorOTPEmail{}, + } + case domain.UserAuthMethodTypeUnspecified: + case domain.UserAuthMethodTypePasswordless: + case domain.UserAuthMethodTypePassword: + case domain.UserAuthMethodTypeIDP: + case domain.UserAuthMethodTypeOTP: + case domain.UserAuthMethodTypePrivateKey: + } + return factor +} + +func AuthFactorsToPb(authFactors []user_pb.AuthFactors) []domain.UserAuthMethodType { + factors := make([]domain.UserAuthMethodType, len(authFactors)) + for i, authFactor := range authFactors { + factors[i] = AuthFactorToPb(authFactor) + } + return factors +} + +func AuthFactorToPb(authFactor user_pb.AuthFactors) domain.UserAuthMethodType { + switch authFactor { + case user_pb.AuthFactors_OTP: + return domain.UserAuthMethodTypeTOTP + case user_pb.AuthFactors_OTP_SMS: + return domain.UserAuthMethodTypeOTPSMS + case user_pb.AuthFactors_OTP_EMAIL: + return domain.UserAuthMethodTypeOTPEmail + case user_pb.AuthFactors_U2F: + return domain.UserAuthMethodTypeU2F + default: + return domain.UserAuthMethodTypeUnspecified + } +} + +func AuthFactorStatesToPb(authFactorStates []user_pb.AuthFactorState) []domain.MFAState { + factorStates := make([]domain.MFAState, len(authFactorStates)) + for i, authFactorState := range authFactorStates { + factorStates[i] = AuthFactorStateToPb(authFactorState) + } + return factorStates +} + +func AuthFactorStateToPb(authFactorState user_pb.AuthFactorState) domain.MFAState { + switch authFactorState { + case user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED: + return domain.MFAStateUnspecified + case user_pb.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY: + return domain.MFAStateNotReady + case user_pb.AuthFactorState_AUTH_FACTOR_STATE_READY: + return domain.MFAStateReady + case user_pb.AuthFactorState_AUTH_FACTOR_STATE_REMOVED: + return domain.MFAStateRemoved + default: + return domain.MFAStateUnspecified + } +} + +func MFAStateToPb(state domain.MFAState) user_pb.AuthFactorState { + switch state { + case domain.MFAStateNotReady: + return user_pb.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY + case domain.MFAStateReady: + return user_pb.AuthFactorState_AUTH_FACTOR_STATE_READY + case domain.MFAStateUnspecified, domain.MFAStateRemoved: + // Handle all remaining cases so the linter succeeds + return user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED + default: + return user_pb.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 206183351e..8d4c254c6b 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/zitadel/logging" + "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" @@ -2629,6 +2631,247 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { } } +func TestServer_ListAuthenticationFactors(t *testing.T) { + tests := []struct { + name string + args *user.ListAuthenticationFactorsRequest + want *user.ListAuthenticationFactorsResponse + dep func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) + wantErr bool + ctx context.Context + }{ + { + name: "no auth", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: nil, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() + args.UserId = userIDWithoutAuth + }, + ctx: CTX, + }, + { + name: "with u2f", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithU2F := Instance.CreateHumanUser(CTX).GetUserId() + U2FId := Instance.RegisterUserU2F(CTX, userWithU2F) + + args.UserId = userWithU2F + want.Result[0].Type = &user.AuthFactor_U2F{ + U2F: &user.AuthFactorU2F{ + Id: U2FId, + Name: "nice name", + }, + } + }, + ctx: CTX, + }, + { + name: "with totp, u2f", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Type: &user.AuthFactor_Otp{ + Otp: &user.AuthFactorOTP{}, + }, + }, + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "secret").GetUserId() + U2FIdWithTOTP := Instance.RegisterUserU2F(CTX, userWithTOTP) + + args.UserId = userWithTOTP + want.Result[1].Type = &user.AuthFactor_U2F{ + U2F: &user.AuthFactorU2F{ + Id: U2FIdWithTOTP, + Name: "nice name", + }, + } + }, + ctx: CTX, + }, + { + name: "with totp, u2f filtered", + args: &user.ListAuthenticationFactorsRequest{ + AuthFactors: []user.AuthFactors{user.AuthFactors_U2F}, + }, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "secret").GetUserId() + U2FIdWithTOTP := Instance.RegisterUserU2F(CTX, userWithTOTP) + + args.UserId = userWithTOTP + want.Result[0].Type = &user.AuthFactor_U2F{ + U2F: &user.AuthFactorU2F{ + Id: U2FIdWithTOTP, + Name: "nice name", + }, + } + }, + ctx: CTX, + }, + { + name: "with sms", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Type: &user.AuthFactor_OtpSms{ + OtpSms: &user.AuthFactorOTPSMS{}, + }, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithSMS := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.GetId(), gofakeit.Email(), gofakeit.Phone()).GetUserId() + Instance.RegisterUserOTPSMS(CTX, userWithSMS) + + args.UserId = userWithSMS + }, + ctx: CTX, + }, + { + name: "with email", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_READY, + Type: &user.AuthFactor_OtpEmail{ + OtpEmail: &user.AuthFactorOTPEmail{}, + }, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithEmail := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.GetId(), gofakeit.Email(), gofakeit.Phone()).GetUserId() + Instance.RegisterUserOTPEmail(CTX, userWithEmail) + + args.UserId = userWithEmail + }, + ctx: CTX, + }, + { + name: "with not ready u2f", + args: &user.ListAuthenticationFactorsRequest{}, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{}, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithNotReadyU2F := Instance.CreateHumanUser(CTX).GetUserId() + _, err := Instance.Client.UserV2.RegisterU2F(CTX, &user.RegisterU2FRequest{ + UserId: userWithNotReadyU2F, + Domain: Instance.Domain, + }) + logging.OnError(err).Panic("Could not register u2f") + + args.UserId = userWithNotReadyU2F + }, + ctx: CTX, + }, + { + name: "with not ready u2f state filtered", + args: &user.ListAuthenticationFactorsRequest{ + States: []user.AuthFactorState{user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY}, + }, + want: &user.ListAuthenticationFactorsResponse{ + Result: []*user.AuthFactor{ + { + State: user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY, + }, + }, + }, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithNotReadyU2F := Instance.CreateHumanUser(CTX).GetUserId() + U2FNotReady, err := Instance.Client.UserV2.RegisterU2F(CTX, &user.RegisterU2FRequest{ + UserId: userWithNotReadyU2F, + Domain: Instance.Domain, + }) + logging.OnError(err).Panic("Could not register u2f") + + args.UserId = userWithNotReadyU2F + want.Result[0].Type = &user.AuthFactor_U2F{ + U2F: &user.AuthFactorU2F{ + Id: U2FNotReady.GetU2FId(), + Name: "", + }, + } + }, + ctx: CTX, + }, + { + name: "with no userId", + args: &user.ListAuthenticationFactorsRequest{ + UserId: "", + }, + ctx: CTX, + wantErr: true, + }, + { + name: "with no permission", + args: &user.ListAuthenticationFactorsRequest{}, + dep: func(args *user.ListAuthenticationFactorsRequest, want *user.ListAuthenticationFactorsResponse) { + userWithTOTP := Instance.CreateHumanUserWithTOTP(CTX, "totp").GetUserId() + + args.UserId = userWithTOTP + }, + ctx: UserCTX, + wantErr: true, + }, + { + name: "with unknown user", + args: &user.ListAuthenticationFactorsRequest{ + UserId: "unknown", + }, + want: &user.ListAuthenticationFactorsResponse{}, + ctx: CTX, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + tt.dep(tt.args, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListAuthenticationFactors(tt.ctx, tt.args) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) + + assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) + }, retryDuration, tick, "timeout waiting for expected auth methods result") + + }) + } +} + func TestServer_CreateInviteCode(t *testing.T) { type args struct { ctx context.Context diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index c0416f84aa..9d99f210e5 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -597,6 +597,39 @@ func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.Li }, nil } +func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAuthenticationFactorsRequest) (*user.ListAuthenticationFactorsResponse, error) { + query := new(query.UserAuthMethodSearchQueries) + + if err := query.AppendUserIDQuery(req.UserId); err != nil { + return nil, err + } + + authMethodsType := []domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail} + if len(req.GetAuthFactors()) > 0 { + authMethodsType = object.AuthFactorsToPb(req.GetAuthFactors()) + } + if err := query.AppendAuthMethodsQuery(authMethodsType...); err != nil { + return nil, err + } + + states := []domain.MFAState{domain.MFAStateReady} + if len(req.GetStates()) > 0 { + states = object.AuthFactorStatesToPb(req.GetStates()) + } + if err := query.AppendStatesQuery(states...); err != nil { + return nil, err + } + + authMethods, err := s.query.SearchUserAuthMethods(ctx, query, s.checkPermission) + if err != nil { + return nil, err + } + + return &user.ListAuthenticationFactorsResponse{ + Result: object.AuthMethodsToPb(authMethods), + }, nil +} + func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType { methods := make([]user.AuthenticationMethodType, len(methodTypes)) for i, method := range methodTypes { diff --git a/internal/integration/client.go b/internal/integration/client.go index c2297f7a09..af30f0e642 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -327,7 +327,7 @@ func (i *Instance) CreateUserIDPlink(ctx context.Context, userID, externalID, id ) } -func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) { +func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) string { reg, err := i.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user_v2.CreatePasskeyRegistrationLinkRequest{ UserId: userID, Medium: &user_v2.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, @@ -350,9 +350,10 @@ func (i *Instance) RegisterUserPasskey(ctx context.Context, userID string) { PasskeyName: "nice name", }) logging.OnError(err).Panic("create user passkey") + return pkr.GetPasskeyId() } -func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) { +func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) string { pkr, err := i.Client.UserV2.RegisterU2F(ctx, &user_v2.RegisterU2FRequest{ UserId: userID, Domain: i.Domain, @@ -368,6 +369,21 @@ func (i *Instance) RegisterUserU2F(ctx context.Context, userID string) { TokenName: "nice name", }) logging.OnError(err).Panic("create user u2f") + return pkr.GetU2FId() +} + +func (i *Instance) RegisterUserOTPSMS(ctx context.Context, userID string) { + _, err := i.Client.UserV2.AddOTPSMS(ctx, &user_v2.AddOTPSMSRequest{ + UserId: userID, + }) + logging.OnError(err).Panic("create user sms") +} + +func (i *Instance) RegisterUserOTPEmail(ctx context.Context, userID string) { + _, err := i.Client.UserV2.AddOTPEmail(ctx, &user_v2.AddOTPEmailRequest{ + UserId: userID, + }) + logging.OnError(err).Panic("create user email") } func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, changeRequired bool) *object.Details { diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 3ba794ee0f..0687545aef 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -270,6 +270,14 @@ func NewUserAuthMethodTypesSearchQuery(values ...domain.UserAuthMethodType) (Sea return NewListQuery(UserAuthMethodColumnMethodType, list, ListIn) } +func NewUserAuthMethodStatesSearchQuery(values ...domain.MFAState) (SearchQuery, error) { + list := make([]interface{}, len(values)) + for i, value := range values { + list[i] = value + } + return NewListQuery(UserAuthMethodColumnState, list, ListIn) +} + func (r *UserAuthMethodSearchQueries) AppendResourceOwnerQuery(orgID string) error { query, err := NewUserAuthMethodResourceOwnerSearchQuery(orgID) if err != nil { @@ -306,6 +314,15 @@ func (r *UserAuthMethodSearchQueries) AppendStateQuery(state domain.MFAState) er return nil } +func (r *UserAuthMethodSearchQueries) AppendStatesQuery(state ...domain.MFAState) error { + query, err := NewUserAuthMethodStatesSearchQuery(state...) + if err != nil { + return err + } + r.Queries = append(r.Queries, query) + return nil +} + func (r *UserAuthMethodSearchQueries) AppendAuthMethodQuery(authMethod domain.UserAuthMethodType) error { query, err := NewUserAuthMethodTypeSearchQuery(authMethod) if err != nil { diff --git a/proto/zitadel/user/v2/user.proto b/proto/zitadel/user/v2/user.proto index cfeebbf33d..b569b81bbd 100644 --- a/proto/zitadel/user/v2/user.proto +++ b/proto/zitadel/user/v2/user.proto @@ -276,6 +276,36 @@ message Passkey { ]; } +message AuthFactor { + AuthFactorState state = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the auth factor"; + } + ]; + oneof type { + AuthFactorOTP otp = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "TOTP second factor" + } + ]; + AuthFactorU2F u2f = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "U2F second factor" + } + ]; + AuthFactorOTPSMS otp_sms = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "SMS second factor" + } + ]; + AuthFactorOTPEmail otp_email = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Email second factor" + } + ]; + } +} + enum AuthFactorState { AUTH_FACTOR_STATE_UNSPECIFIED = 0; AUTH_FACTOR_STATE_NOT_READY = 1; @@ -283,6 +313,23 @@ enum AuthFactorState { AUTH_FACTOR_STATE_REMOVED = 3; } +message AuthFactorOTP {} +message AuthFactorOTPSMS {} +message AuthFactorOTPEmail {} + +message AuthFactorU2F { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + string name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"fido key\"" + } + ]; +} + message SendInviteCode { // Optionally set a url_template, which will be used in the invite mail sent by ZITADEL to guide the user to your invitation page. // If no template is set, the default ZITADEL url will be used. diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 8ae7c1bc08..7e5b8a02e8 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -1110,6 +1110,28 @@ service UserService { }; } + rpc ListAuthenticationFactors(ListAuthenticationFactorsRequest) returns (ListAuthenticationFactorsResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/authentication_factors/_search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + + } + // Create an invite code for a user // // Create an invite code for a user to initialize their first authentication method (password, passkeys, IdP) depending on the organization's available methods. @@ -2216,6 +2238,41 @@ enum AuthenticationMethodType { AUTHENTICATION_METHOD_TYPE_OTP_EMAIL = 7; } +message ListAuthenticationFactorsRequest{ + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + repeated AuthFactors auth_factors = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the Auth Factors you are interested in" + default: "All Auth Factors" + } + ]; + repeated AuthFactorState states = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the state of the Auth Factors" + default: "Auth Factors that are ready" + } + ]; +} + +enum AuthFactors { + OTP = 0; + OTP_SMS = 1; + OTP_EMAIL = 2; + U2F = 3; +} + +message ListAuthenticationFactorsResponse { + repeated zitadel.user.v2.AuthFactor result = 1; +} + message CreateInviteCodeRequest { string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200},