feat(api): list authentication method types in user api v2 (#6058)

This commit is contained in:
Livio Spring 2023-06-20 18:23:28 +02:00 committed by GitHub
parent 82e7333169
commit 7046194530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 523 additions and 1 deletions

View File

@ -47,6 +47,7 @@ import (
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/authz" "github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository" authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database" cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
@ -145,6 +146,11 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
keys.SAML, keys.SAML,
config.InternalAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings,
sessionTokenVerifier, sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck {
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
}
},
) )
if err != nil { if err != nil {
return fmt.Errorf("cannot start queries: %w", err) return fmt.Errorf("cannot start queries: %w", err)

View File

@ -221,3 +221,41 @@ func (s *Server) checkIntentToken(token string, intentID string) error {
} }
return nil return nil
} }
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
authMethods, err := s.query.ListActiveUserAuthMethodTypes(ctx, req.GetUserId(), false)
if err != nil {
return nil, err
}
return &user.ListAuthenticationMethodTypesResponse{
Details: object.ToListDetails(authMethods.SearchResponse),
AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes),
}, nil
}
func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType {
methods := make([]user.AuthenticationMethodType, len(methodTypes))
for i, method := range methodTypes {
methods[i] = authMethodTypeToPb(method)
}
return methods
}
func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType {
switch methodType {
case domain.UserAuthMethodTypeOTP:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP
case domain.UserAuthMethodTypeU2F:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F
case domain.UserAuthMethodTypePasswordless:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY
case domain.UserAuthMethodTypePassword:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD
case domain.UserAuthMethodTypeIDP:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP
case domain.UserAuthMethodTypeUnspecified:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
default:
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
}
}

View File

@ -24,6 +24,7 @@ import (
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idp"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
) )
@ -712,3 +713,109 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
}) })
} }
} }
func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
userIDWithoutAuth := Tester.CreateHumanUser(CTX).GetUserId()
userIDWithPasskey := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userIDWithPasskey)
userMultipleAuth := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userMultipleAuth)
provider, err := Tester.Client.Mgmt.AddGenericOIDCProvider(CTX, &mgmt.AddGenericOIDCProviderRequest{
Name: "ListAuthenticationMethodTypes",
Issuer: "https://example.com",
ClientId: "client_id",
ClientSecret: "client_secret",
})
require.NoError(t, err)
idpLink, err := Tester.Client.UserV2.AddIDPLink(CTX, &user.AddIDPLinkRequest{UserId: userMultipleAuth, IdpLink: &user.IDPLink{
IdpId: provider.GetId(),
UserId: "external-id",
UserName: "displayName",
}})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.ListAuthenticationMethodTypesRequest
}
tests := []struct {
name string
args args
want *user.ListAuthenticationMethodTypesResponse
}{
{
name: "no auth",
args: args{
CTX,
&user.ListAuthenticationMethodTypesRequest{
UserId: userIDWithoutAuth,
},
},
want: &user.ListAuthenticationMethodTypesResponse{
Details: &object.ListDetails{
TotalResult: 0,
},
},
},
{
name: "with auth (passkey)",
args: args{
CTX,
&user.ListAuthenticationMethodTypesRequest{
UserId: userIDWithPasskey,
},
},
want: &user.ListAuthenticationMethodTypesResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
AuthMethodTypes: []user.AuthenticationMethodType{
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
},
},
},
{
name: "multiple auth",
args: args{
CTX,
&user.ListAuthenticationMethodTypesRequest{
UserId: userMultipleAuth,
},
},
want: &user.ListAuthenticationMethodTypesResponse{
Details: &object.ListDetails{
TotalResult: 2,
},
AuthMethodTypes: []user.AuthenticationMethodType{
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got *user.ListAuthenticationMethodTypesResponse
var err error
for {
got, err = Client.ListAuthenticationMethodTypes(tt.args.ctx, tt.args.req)
if err == nil && got.GetDetails().GetProcessedSequence() >= idpLink.GetDetails().GetSequence() {
break
}
select {
case <-CTX.Done():
t.Fatal(CTX.Err(), err)
case <-time.After(time.Second):
t.Log("retrying ListAuthenticationMethodTypes")
continue
}
}
require.NoError(t, err)
assert.Equal(t, tt.want.GetDetails().GetTotalResult(), got.GetDetails().GetTotalResult())
require.Equal(t, tt.want.GetAuthMethodTypes(), got.GetAuthMethodTypes())
})
}
}

View File

@ -222,3 +222,75 @@ func Test_intentToIDPInformationPb(t *testing.T) {
}) })
} }
} }
func Test_authMethodTypesToPb(t *testing.T) {
tests := []struct {
name string
methodTypes []domain.UserAuthMethodType
want []user.AuthenticationMethodType
}{
{
"empty list",
nil,
[]user.AuthenticationMethodType{},
},
{
"list",
[]domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
},
[]user.AuthenticationMethodType{
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, authMethodTypesToPb(tt.methodTypes), "authMethodTypesToPb(%v)", tt.methodTypes)
})
}
}
func Test_authMethodTypeToPb(t *testing.T) {
tests := []struct {
name string
methodType domain.UserAuthMethodType
want user.AuthenticationMethodType
}{
{
"uspecified",
domain.UserAuthMethodTypeUnspecified,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED,
},
{
"(t)otp",
domain.UserAuthMethodTypeOTP,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP,
},
{
"u2f",
domain.UserAuthMethodTypeU2F,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F,
},
{
"passkey",
domain.UserAuthMethodTypePasswordless,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
},
{
"password",
domain.UserAuthMethodTypePassword,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD,
},
{
"idp",
domain.UserAuthMethodTypeIDP,
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, authMethodTypeToPb(tt.methodType), "authMethodTypeToPb(%v)", tt.methodType)
})
}
}

View File

@ -28,7 +28,7 @@ type PermissionCheck func(ctx context.Context, permission, orgID, resourceID str
const ( const (
PermissionUserWrite = "user.write" PermissionUserWrite = "user.write"
PermissionSessionRead = "session.read" PermissionUserRead = "user.read"
PermissionSessionWrite = "session.write" PermissionSessionWrite = "session.write"
PermissionSessionDelete = "session.delete" PermissionSessionDelete = "session.delete"
) )

View File

@ -51,6 +51,8 @@ const (
UserAuthMethodTypeOTP UserAuthMethodTypeOTP
UserAuthMethodTypeU2F UserAuthMethodTypeU2F
UserAuthMethodTypePasswordless UserAuthMethodTypePasswordless
UserAuthMethodTypePassword
UserAuthMethodTypeIDP
userAuthMethodTypeCount userAuthMethodTypeCount
) )

View File

@ -9,6 +9,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/zitadel/zitadel/pkg/grpc/admin" "github.com/zitadel/zitadel/pkg/grpc/admin"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha" session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
@ -17,6 +18,7 @@ import (
type Client struct { type Client struct {
CC *grpc.ClientConn CC *grpc.ClientConn
Admin admin.AdminServiceClient Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
UserV2 user.UserServiceClient UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient SessionV2 session.SessionServiceClient
} }
@ -25,6 +27,7 @@ func newClient(cc *grpc.ClientConn) Client {
return Client{ return Client{
CC: cc, CC: cc,
Admin: admin.NewAdminServiceClient(cc), Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
UserV2: user.NewUserServiceClient(cc), UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc), SessionV2: session.NewSessionServiceClient(cc),
} }

View File

@ -34,6 +34,7 @@ type Queries struct {
idpConfigEncryption crypto.EncryptionAlgorithm idpConfigEncryption crypto.EncryptionAlgorithm
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error) sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
checkPermission domain.PermissionCheck
DefaultLanguage language.Tag DefaultLanguage language.Tag
LoginDir http.FileSystem LoginDir http.FileSystem
@ -55,6 +56,7 @@ func StartQueries(
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm, idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
zitadelRoles []authz.RoleMapping, zitadelRoles []authz.RoleMapping,
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error), sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
permissionCheck func(q *Queries) domain.PermissionCheck,
) (repo *Queries, err error) { ) (repo *Queries, err error) {
statikLoginFS, err := fs.NewWithNamespace("login") statikLoginFS, err := fs.NewWithNamespace("login")
if err != nil { if err != nil {
@ -95,6 +97,8 @@ func StartQueries(
}, },
} }
repo.checkPermission = permissionCheck(repo)
err = projection.Create(ctx, sqlClient, es, projections, keyEncryptionAlgorithm, certEncryptionAlgorithm) err = projection.Create(ctx, sqlClient, es, projections, keyEncryptionAlgorithm, certEncryptionAlgorithm)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -6,6 +6,7 @@ import (
"time" "time"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/api/call"
@ -64,12 +65,27 @@ var (
name: projection.UserAuthMethodOwnerRemovedCol, name: projection.UserAuthMethodOwnerRemovedCol,
table: userAuthMethodTable, table: userAuthMethodTable,
} }
authMethodTypeTable = userAuthMethodTable.setAlias("auth_method_types")
authMethodTypeUserID = UserAuthMethodColumnUserID.setTable(authMethodTypeTable)
authMethodTypeInstanceID = UserAuthMethodColumnInstanceID.setTable(authMethodTypeTable)
authMethodTypeTypes = UserAuthMethodColumnMethodType.setTable(authMethodTypeTable)
authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable)
userIDPsCountTable = idpUserLinkTable.setAlias("user_idps_count")
userIDPsCountUserID = IDPUserLinkUserIDCol.setTable(userIDPsCountTable)
userIDPsCountInstanceID = IDPUserLinkInstanceIDCol.setTable(userIDPsCountTable)
userIDPsCountCount = Column{
name: "count",
table: userIDPsCountTable,
}
) )
type AuthMethods struct { type AuthMethods struct {
SearchResponse SearchResponse
AuthMethods []*AuthMethod AuthMethods []*AuthMethod
} }
type AuthMethod struct { type AuthMethod struct {
UserID string UserID string
CreationDate time.Time CreationDate time.Time
@ -83,6 +99,11 @@ type AuthMethod struct {
Type domain.UserAuthMethodType Type domain.UserAuthMethodType
} }
type AuthMethodTypes struct {
SearchResponse
AuthMethodTypes []domain.UserAuthMethodType
}
type UserAuthMethodSearchQueries struct { type UserAuthMethodSearchQueries struct {
SearchRequest SearchRequest
Queries []SearchQuery Queries []SearchQuery
@ -114,6 +135,41 @@ func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMe
return userAuthMethods, err return userAuthMethods, err
} }
func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes *AuthMethodTypes, err error) {
ctxData := authz.GetCtxData(ctx)
if ctxData.UserID != userID {
if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
return nil, err
}
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareActiveUserAuthMethodTypesQuery(ctx, q.client)
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[UserOwnerRemovedCol.identifier()] = false
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-Sfdrg", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil || rows.Err() != nil {
return nil, errors.ThrowInternal(err, "QUERY-SDgr3", "Errors.Internal")
}
userAuthMethodTypes, err = scan(rows)
if err != nil {
return nil, err
}
userAuthMethodTypes.LatestSequence, err = q.latestSequence(ctx, userTable, notifyTable, userAuthMethodTable, idpUserLinkTable)
return userAuthMethodTypes, err
}
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) { func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals) return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
} }
@ -253,3 +309,80 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se
}, nil }, nil
} }
} }
func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) {
authMethodsQuery, authMethodsArgs, err := sq.Select(
"DISTINCT("+authMethodTypeTypes.identifier()+")",
authMethodTypeUserID.identifier(),
authMethodTypeInstanceID.identifier()).
From(authMethodTypeTable.identifier()).
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
ToSql()
if err != nil {
return sq.SelectBuilder{}, nil
}
idpsQuery, _, err := sq.Select(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
From(userIDPsCountTable.identifier()).
GroupBy(
userIDPsCountUserID.identifier(),
userIDPsCountInstanceID.identifier(),
).
ToSql()
if err != nil {
return sq.SelectBuilder{}, nil
}
return sq.Select(
NotifyPasswordSetCol.identifier(),
authMethodTypeTypes.identifier(),
userIDPsCountCount.identifier()).
From(userTable.identifier()).
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
authMethodsArgs...).
LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*AuthMethodTypes, error) {
userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
var passwordSet sql.NullBool
var idp sql.NullInt64
for rows.Next() {
var authMethodType sql.NullInt16
err := rows.Scan(
&passwordSet,
&authMethodType,
&idp,
)
if err != nil {
return nil, err
}
if authMethodType.Valid {
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
}
}
if passwordSet.Valid && passwordSet.Bool {
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
}
if idp.Valid && idp.Int64 > 0 {
logging.Error("IDP", idp.Int64)
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-3n9fl", "Errors.Query.CloseRows")
}
return &AuthMethodTypes{
AuthMethodTypes: userAuthMethodTypes,
SearchResponse: SearchResponse{
Count: uint64(len(userAuthMethodTypes)),
},
}, nil
}
}

View File

@ -36,6 +36,23 @@ var (
"method_type", "method_type",
"count", "count",
} }
prepareActiveAuthMethodTypesStmt = `SELECT projections.users8_notifications.password_set,` +
` auth_method_types.method_type,` +
` user_idps_count.count` +
` FROM projections.users8` +
` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_notifications.instance_id` +
` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
` WHERE auth_method_types.state = $1) AS auth_method_types` +
` ON auth_method_types.user_id = projections.users8.id AND auth_method_types.instance_id = projections.users8.instance_id` +
` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
` ON user_idps_count.user_id = projections.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms`
prepareActiveAuthMethodTypesCols = []string{
"password_set",
"method_type",
"idps_count",
}
) )
func Test_UserAuthMethodPrepares(t *testing.T) { func Test_UserAuthMethodPrepares(t *testing.T) {
@ -182,6 +199,95 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
}, },
object: nil, object: nil,
}, },
{
name: "prepareActiveUserAuthMethodTypesQuery no result",
prepare: prepareActiveUserAuthMethodTypesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
nil,
nil,
),
},
object: &AuthMethodTypes{AuthMethodTypes: []domain.UserAuthMethodType{}},
},
{
name: "prepareActiveUserAuthMethodTypesQuery one second factor",
prepare: prepareActiveUserAuthMethodTypesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
prepareActiveAuthMethodTypesCols,
[][]driver.Value{
{
true,
domain.UserAuthMethodTypePasswordless,
1,
},
},
),
},
object: &AuthMethodTypes{
SearchResponse: SearchResponse{
Count: 3,
},
AuthMethodTypes: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
},
},
{
name: "prepareActiveUserAuthMethodTypesQuery multiple second factors",
prepare: prepareActiveUserAuthMethodTypesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
prepareActiveAuthMethodTypesCols,
[][]driver.Value{
{
true,
domain.UserAuthMethodTypePasswordless,
1,
},
{
true,
domain.UserAuthMethodTypeOTP,
1,
},
},
),
},
object: &AuthMethodTypes{
SearchResponse: SearchResponse{
Count: 4,
},
AuthMethodTypes: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypePassword,
domain.UserAuthMethodTypeIDP,
},
},
},
{
name: "prepareActiveUserAuthMethodTypesQuery sql err",
prepare: prepareActiveUserAuthMethodTypesQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: nil,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -440,6 +440,30 @@ service UserService {
}; };
}; };
} }
// List all possible authentication methods of a user
rpc ListAuthenticationMethodTypes (ListAuthenticationMethodTypesRequest) returns (ListAuthenticationMethodTypesResponse) {
option (google.api.http) = {
get: "/v2alpha/users/{user_id}/authentication_methods"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "List all possible authentication methods of a user";
description: "List all possible authentication methods of a user like password, passwordless, (T)OTP and more";
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
} }
message AddHumanUserRequest{ message AddHumanUserRequest{
@ -857,6 +881,7 @@ message AddIDPLinkResponse {
zitadel.object.v2alpha.Details details = 1; zitadel.object.v2alpha.Details details = 1;
} }
message PasswordResetRequest{ message PasswordResetRequest{
string user_id = 1 [ string user_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200}, (validate.rules).string = {min_len: 1, max_len: 200},
@ -919,3 +944,29 @@ message SetPasswordRequest{
message SetPasswordResponse{ message SetPasswordResponse{
zitadel.object.v2alpha.Details details = 1; zitadel.object.v2alpha.Details details = 1;
} }
message ListAuthenticationMethodTypesRequest{
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\"";
}
];
}
message ListAuthenticationMethodTypesResponse{
zitadel.object.v2alpha.ListDetails details = 1;
repeated AuthenticationMethodType auth_method_types = 2;
}
enum AuthenticationMethodType {
AUTHENTICATION_METHOD_TYPE_UNSPECIFIED = 0;
AUTHENTICATION_METHOD_TYPE_PASSWORD = 1;
AUTHENTICATION_METHOD_TYPE_PASSKEY = 2;
AUTHENTICATION_METHOD_TYPE_IDP = 3;
AUTHENTICATION_METHOD_TYPE_TOTP = 4;
AUTHENTICATION_METHOD_TYPE_U2F = 5;
}