From f5e9d4f57f5545b5b9b435e1da874a1984dd9a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 23 May 2024 07:35:10 +0200 Subject: [PATCH] fix(oidc): IDP and machine user auth methods (#7992) # Which Problems Are Solved After https://github.com/zitadel/zitadel/pull/7822 was merged we discovered that v2 tokens that where obtained through an IDP using the v1 login, can't be used for zitadel API calls. - Because we used to store the AMR claim on the auth request, but internally use the domain.UserAuthMethod type. AMR has no notion of an IDP login, so that "factor" was lost during conversion. Rendering those v2 tokens invalid on the zitadel API. - A wrong check on machine user tokens falsly allowed some tokens to be valid - The client ID was set to tokens from client credentials and JWT profile, which made client queries fail in the validation middleware. The middleware expects client ID unset for machine users. # How the Problems Are Solved Store the domain.AuthMethods directly in the auth requests and session, instead of using AMR claims with lossy conversion. - IDPs have seperate auth method, which is not an AMR claim - Machine users are treated specialy, eg auth methods are not required. - Do not set the client ID for client credentials and JWT profile # Additional Changes Cleaned up mostly unused `oidc.getInfoFromRequest()`. # Additional Context - Bugs were introduced in https://github.com/zitadel/zitadel/pull/7822 and not yet part of a release. - Reported internally. --- internal/api/oidc/auth_request.go | 51 ++++--------- internal/api/oidc/auth_request_converter.go | 2 +- internal/api/oidc/token_client_credentials.go | 2 +- internal/api/oidc/token_code.go | 38 ++++++---- internal/api/oidc/token_jwt_profile.go | 6 +- .../eventsourcing/eventstore/auth_request.go | 3 + .../eventstore/token_verifier.go | 11 ++- internal/domain/auth_request.go | 15 ++++ internal/domain/user.go | 1 + internal/query/user_auth_method.go | 37 +++++++--- internal/query/user_auth_method_test.go | 72 ++++++++----------- 11 files changed, 126 insertions(+), 112 deletions(-) diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 130b51525..1abe59dc8 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -12,7 +12,6 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" - "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -199,23 +198,6 @@ func (*OPStorage) panicErr(method string) error { return fmt.Errorf("OPStorage.%s should not be called anymore. This is a bug. Please report https://github.com/zitadel/zitadel/issues", method) } -func getInfoFromRequest(req op.TokenRequest) (agentID, clientID, userOrgID string, authTime time.Time, amr []string, preferredLanguage *language.Tag, reason domain.TokenReason, actor *domain.TokenActor) { - switch r := req.(type) { - case *AuthRequest: - return r.AgentID, r.ApplicationID, r.UserOrgID, r.AuthTime, r.GetAMR(), r.PreferredLanguage, domain.TokenReasonAuthRequest, nil - case *RefreshTokenRequest: - return r.UserAgentID, r.ClientID, "", r.AuthTime, r.AuthMethodsReferences, nil, domain.TokenReasonRefresh, r.Actor - case op.IDTokenRequest: - return "", r.GetClientID(), "", r.GetAuthTime(), r.GetAMR(), nil, domain.TokenReasonAuthRequest, nil - case *oidc.JWTTokenRequest: - return "", "", "", r.GetAuthTime(), nil, nil, domain.TokenReasonJWTProfile, nil - case *clientCredentialsRequest: - return "", "", "", time.Time{}, nil, nil, domain.TokenReasonClientCredentials, nil - default: - return "", "", "", time.Time{}, nil, nil, domain.TokenReasonAuthRequest, nil - } -} - func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (_ op.RefreshTokenRequest, err error) { panic("TokenRequestByRefreshToken should not be called anymore. This is a bug. Please report https://github.com/zitadel/zitadel/issues") } @@ -511,8 +493,8 @@ func implicitFlowComplianceChecker() command.AuthRequestComplianceChecker { func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request) { authorizer := s.Provider() - authReq, err := func() (authReq op.AuthRequest, err error) { - ctx, span := tracing.NewSpan(r.Context()) + authReq, err := func(ctx context.Context) (authReq *AuthRequest, err error) { + ctx, span := tracing.NewSpan(ctx) r = r.WithContext(ctx) defer func() { span.EndWithError(err) }() @@ -520,7 +502,7 @@ func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request if err != nil { return nil, err } - authReq, err = authorizer.Storage().AuthRequestByID(r.Context(), id) + authReq, err = s.getAuthRequestV1ByID(ctx, id) if err != nil { return nil, err } @@ -528,13 +510,13 @@ func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request return authReq, oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required.") } return authReq, s.authResponse(authReq, authorizer, w, r) - }() + }(r.Context()) if err != nil { op.AuthRequestError(w, r, authReq, err, authorizer) } } -func (s *Server) authResponse(authReq op.AuthRequest, authorizer op.Authorizer, w http.ResponseWriter, r *http.Request) (err error) { +func (s *Server) authResponse(authReq *AuthRequest, authorizer op.Authorizer, w http.ResponseWriter, r *http.Request) (err error) { ctx, span := tracing.NewSpan(r.Context()) r = r.WithContext(ctx) defer func() { span.EndWithError(err) }() @@ -551,7 +533,7 @@ func (s *Server) authResponse(authReq op.AuthRequest, authorizer op.Authorizer, return s.authResponseToken(authReq, authorizer, client, w, r) } -func (s *Server) authResponseToken(authReq op.AuthRequest, authorizer op.Authorizer, opClient op.Client, w http.ResponseWriter, r *http.Request) (err error) { +func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorizer, opClient op.Client, w http.ResponseWriter, r *http.Request) (err error) { ctx, span := tracing.NewSpan(r.Context()) r = r.WithContext(ctx) defer func() { span.EndWithError(err) }() @@ -561,23 +543,20 @@ func (s *Server) authResponseToken(authReq op.AuthRequest, authorizer op.Authori return zerrors.ThrowInternal(nil, "OIDC-waeN6", "Error.Internal") } - userAgentID, _, userOrgID, authTime, authMethodsReferences, preferredLanguage, reason, actor := getInfoFromRequest(authReq) scope := authReq.GetScopes() session, err := s.command.CreateOIDCSession(ctx, - authReq.GetSubject(), - userOrgID, + authReq.UserID, + authReq.UserOrgID, client.client.ClientID, scope, - authReq.GetAudience(), - AMRToAuthMethodTypes(authMethodsReferences), - authTime, + authReq.Audience, + authReq.AuthMethods(), + authReq.AuthTime, authReq.GetNonce(), - preferredLanguage, - &domain.UserAgent{ - FingerprintID: &userAgentID, - }, - reason, - actor, + authReq.PreferredLanguage, + authReq.BrowserInfo.ToUserAgent(), + domain.TokenReasonAuthRequest, + nil, slices.Contains(scope, oidc.ScopeOfflineAccess), ) if err != nil { diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 76cbc655d..9a1aee2aa 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -94,7 +94,7 @@ func (a *AuthRequest) oidc() *domain.AuthRequestOIDC { return a.Request.(*domain.AuthRequestOIDC) } -func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ op.AuthRequest, err error) { +func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ *AuthRequest, err error) { if _, ok := authReq.Request.(*domain.AuthRequestOIDC); !ok { return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-Haz7A", "auth request is not of type oidc") } diff --git a/internal/api/oidc/token_client_credentials.go b/internal/api/oidc/token_client_credentials.go index 3765d58ad..e0cb29770 100644 --- a/internal/api/oidc/token_client_credentials.go +++ b/internal/api/oidc/token_client_credentials.go @@ -34,7 +34,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ session, err := s.command.CreateOIDCSession(ctx, client.user.ID, client.user.ResourceOwner, - r.Data.ClientID, + "", scope, domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope), []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go index add1fc1c2..85aa84757 100644 --- a/internal/api/oidc/token_code.go +++ b/internal/api/oidc/token_code.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -69,36 +70,33 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A if req.RedirectURI != authReq.GetRedirectURI() { return nil, "", oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond") } - userAgentID, _, userOrgID, authTime, authMethodsReferences, preferredLanguage, reason, actor := getInfoFromRequest(authReq) scope := authReq.GetScopes() session, err = s.command.CreateOIDCSession(ctx, - authReq.GetSubject(), - userOrgID, + authReq.UserID, + authReq.UserOrgID, client.client.ClientID, scope, - authReq.GetAudience(), - AMRToAuthMethodTypes(authMethodsReferences), - authTime, + authReq.Audience, + authReq.AuthMethods(), + authReq.AuthTime, authReq.GetNonce(), - preferredLanguage, - &domain.UserAgent{ - FingerprintID: &userAgentID, - }, - reason, - actor, + authReq.PreferredLanguage, + authReq.BrowserInfo.ToUserAgent(), + domain.TokenReasonAuthRequest, + nil, slices.Contains(scope, oidc.ScopeOfflineAccess), ) if err != nil { return nil, "", err } - return session, authReq.GetState(), s.repo.DeleteAuthRequest(ctx, authReq.GetID()) + return session, authReq.TransferState, s.repo.DeleteAuthRequest(ctx, authReq.ID) } // getAuthRequestV1ByCode finds the v1 auth request by code. // code needs to be the encrypted version of the ID, // this is required by the underlying repo. -func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (op.AuthRequest, error) { +func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (*AuthRequest, error) { authReq, err := s.repo.AuthRequestByCode(ctx, code) if err != nil { return nil, err @@ -106,6 +104,18 @@ func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (op.Au return AuthRequestFromBusiness(authReq) } +func (s *Server) getAuthRequestV1ByID(ctx context.Context, id string) (*AuthRequest, error) { + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) + if !ok { + return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-TiTu7", "no user agent id") + } + resp, err := s.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID) + if err != nil { + return nil, err + } + return AuthRequestFromBusiness(resp) +} + func codeExchangeComplianceChecker(client *Client, req *oidc.AccessTokenRequest) command.AuthRequestComplianceChecker { return func(ctx context.Context, authReq *command.AuthRequestWriteModel) error { if authReq.CodeChallenge != nil || client.AuthMethod() == oidc.AuthMethodNone { diff --git a/internal/api/oidc/token_jwt_profile.go b/internal/api/oidc/token_jwt_profile.go index b53a63ca1..b23a24f77 100644 --- a/internal/api/oidc/token_jwt_profile.go +++ b/internal/api/oidc/token_jwt_profile.go @@ -42,15 +42,15 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr session, err := s.command.CreateOIDCSession(ctx, user.ID, user.ResourceOwner, - jwtReq.Subject, + "", scope, domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope), - nil, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePrivateKey}, time.Now(), "", nil, nil, - domain.TokenReasonClientCredentials, + domain.TokenReasonJWTProfile, nil, false, ) diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 76102a916..9de07b742 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -269,6 +269,7 @@ func (repo *AuthRequestRepo) CheckExternalUserLogin(ctx context.Context, authReq return err } + request.IDPLoginChecked = true err = repo.Command.UserIDPLoginChecked(ctx, request.UserOrgID, request.UserID, request.WithCurrentInfo(info)) if err != nil { return err @@ -516,6 +517,7 @@ func (repo *AuthRequestRepo) LinkExternalUsers(ctx context.Context, authReqID, u return err } request.LinkingUsers = nil + request.IDPLoginChecked = true return repo.AuthRequests.UpdateAuthRequest(ctx, request) } @@ -560,6 +562,7 @@ func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, regis request.SetUserInfo(human.ID, human.Username, human.Username, human.DisplayName, "", resourceOwner) request.SelectedIDPConfigID = externalIDP.IDPConfigID request.LinkingUsers = nil + request.IDPLoginChecked = true err = repo.Command.UserIDPLoginChecked(ctx, request.UserOrgID, request.UserID, request.WithCurrentInfo(info)) if err != nil { return err diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index d3a820147..c02e9cb32 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -180,11 +180,18 @@ func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMeth if domain.HasMFA(authMethods) { return nil } - availableAuthMethods, forceMFA, forceMFALocalOnly, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID) + requirements, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID) if err != nil { return err } - if domain.RequiresMFA(forceMFA, forceMFALocalOnly, hasIDPAuthentication(authMethods)) || domain.HasMFA(availableAuthMethods) { + if requirements.UserType == domain.UserTypeMachine { + return nil + } + if domain.RequiresMFA( + requirements.ForceMFA, + requirements.ForceMFALocalOnly, + !hasIDPAuthentication(authMethods)) || + domain.HasMFA(requirements.AuthMethods) { return zerrors.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required") } return nil diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 7bdd7bcb8..b7e463ff9 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -45,6 +45,7 @@ type AuthRequest struct { LinkingUsers []*ExternalUser PossibleSteps []NextStep `json:"-"` PasswordVerified bool + IDPLoginChecked bool MFAsVerified []MFAType Audience []string AuthTime time.Time @@ -69,6 +70,20 @@ func (a *AuthRequest) PolicyOrgID() string { return a.policyOrgID } +func (a *AuthRequest) AuthMethods() []UserAuthMethodType { + list := make([]UserAuthMethodType, 0, len(a.MFAsVerified)+2) + if a.PasswordVerified { + list = append(list, UserAuthMethodTypePassword) + } + if a.IDPLoginChecked { + list = append(list, UserAuthMethodTypeIDP) + } + for _, mfa := range a.MFAsVerified { + list = append(list, mfa.UserAuthMethodType()) + } + return list +} + type ExternalUser struct { IDPConfigID string ExternalUserID string diff --git a/internal/domain/user.go b/internal/domain/user.go index 8677a997f..3204d658d 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -43,6 +43,7 @@ const ( UserAuthMethodTypeOTPSMS UserAuthMethodTypeOTPEmail UserAuthMethodTypeOTP // generic OTP when parsing AMR from OIDC + UserAuthMethodTypePrivateKey userAuthMethodTypeCount ) diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index e39fb2a8e..3c24948d8 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -172,11 +172,18 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri return userAuthMethodTypes, err } -func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error) { +type UserAuthMethodRequirements struct { + UserType domain.UserType + AuthMethods []domain.UserAuthMethodType + ForceMFA bool + ForceMFALocalOnly bool +} + +func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string) (requirements *UserAuthMethodRequirements, err error) { ctxData := authz.GetCtxData(ctx) if ctxData.UserID != userID { if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil { - return nil, false, false, err + return nil, err } } ctx, span := tracing.NewSpan(ctx) @@ -189,17 +196,17 @@ func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID st } stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, false, false, zerrors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest") + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest") } err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { - userAuthMethodTypes, forceMFA, forceMFALocalOnly, err = scan(rows) + requirements, err = scan(rows) return err }, stmt, args...) if err != nil { - return nil, false, false, zerrors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal") + return nil, zerrors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal") } - return userAuthMethodTypes, forceMFA, forceMFALocalOnly, nil + return requirements, nil } func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) { @@ -404,7 +411,7 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba } } -func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (_ []domain.UserAuthMethodType, forceMFA, forceMFALocalOnly bool, err error)) { +func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) { loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery() if err != nil { return sq.SelectBuilder{}, nil @@ -421,6 +428,7 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData NotifyPasswordSetCol.identifier(), authMethodTypeTypes.identifier(), userIDPsCountCount.identifier(), + UserTypeCol.identifier(), forceMFAForce.identifier(), forceMFAForceLocalOnly.identifier()). From(userTable.identifier()). @@ -436,10 +444,11 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData "(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " + forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, bool, error) { + func(rows *sql.Rows) (*UserAuthMethodRequirements, error) { userAuthMethodTypes := make([]domain.UserAuthMethodType, 0) var passwordSet sql.NullBool var idp sql.NullInt64 + var userType sql.NullInt32 var forceMFA sql.NullBool var forceMFALocalOnly sql.NullBool for rows.Next() { @@ -448,11 +457,12 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData &passwordSet, &authMethodType, &idp, + &userType, &forceMFA, &forceMFALocalOnly, ) if err != nil { - return nil, false, false, err + return nil, err } if authMethodType.Valid { userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16)) @@ -467,10 +477,15 @@ func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareData } if err := rows.Close(); err != nil { - return nil, false, false, zerrors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows") + return nil, zerrors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows") } - return userAuthMethodTypes, forceMFA.Bool, forceMFALocalOnly.Bool, nil + return &UserAuthMethodRequirements{ + UserType: domain.UserType(userType.Int32), + AuthMethods: userAuthMethodTypes, + ForceMFA: forceMFA.Bool, + ForceMFALocalOnly: forceMFALocalOnly.Bool, + }, nil } } diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index a4a66cac0..578b14cec 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -59,6 +59,7 @@ var ( prepareAuthMethodTypesRequiredStmt = `SELECT projections.users12_notifications.password_set,` + ` auth_method_types.method_type,` + ` user_idps_count.count,` + + ` projections.users12.type,` + ` auth_methods_force_mfa.force_mfa,` + ` auth_methods_force_mfa.force_mfa_local_only` + ` FROM projections.users12` + @@ -75,6 +76,7 @@ var ( ` prepareAuthMethodTypesRequiredCols = []string{ "password_set", + "type", "method_type", "idps_count", "force_mfa", @@ -317,14 +319,10 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, { name: "prepareUserAuthMethodTypesRequiredQuery no result", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) { + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) { builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) { - authMethods, forceMFA, forceMFALocalOnly, err := scan(rows) - if err != nil { - return nil, err - } - return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil + return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) { + return scan(rows) } }, want: want{ @@ -334,18 +332,14 @@ func Test_UserAuthMethodPrepares(t *testing.T) { nil, ), }, - object: &testUserAuthMethodTypesRequired{authMethods: []domain.UserAuthMethodType{}, forceMFA: false}, + object: &UserAuthMethodRequirements{AuthMethods: []domain.UserAuthMethodType{}, ForceMFA: false}, }, { name: "prepareUserAuthMethodTypesRequiredQuery one second factor", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) { + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) { builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) { - authMethods, forceMFA, forceMFALocalOnly, err := scan(rows) - if err != nil { - return nil, err - } - return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil + return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) { + return scan(rows) } }, want: want{ @@ -357,32 +351,30 @@ func Test_UserAuthMethodPrepares(t *testing.T) { true, domain.UserAuthMethodTypePasswordless, 1, + domain.UserTypeHuman, true, true, }, }, ), }, - object: &testUserAuthMethodTypesRequired{ - authMethods: []domain.UserAuthMethodType{ + object: &UserAuthMethodRequirements{ + UserType: domain.UserTypeHuman, + AuthMethods: []domain.UserAuthMethodType{ domain.UserAuthMethodTypePasswordless, domain.UserAuthMethodTypePassword, domain.UserAuthMethodTypeIDP, }, - forceMFA: true, - forceMFALocalOnly: true, + ForceMFA: true, + ForceMFALocalOnly: true, }, }, { name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) { + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) { builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) { - authMethods, forceMFA, forceMFALocalOnly, err := scan(rows) - if err != nil { - return nil, err - } - return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil + return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) { + return scan(rows) } }, want: want{ @@ -394,6 +386,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) { true, domain.UserAuthMethodTypePasswordless, 1, + domain.UserTypeHuman, true, true, }, @@ -401,6 +394,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) { true, domain.UserAuthMethodTypeTOTP, 1, + domain.UserTypeHuman, true, true, }, @@ -408,27 +402,24 @@ func Test_UserAuthMethodPrepares(t *testing.T) { ), }, - object: &testUserAuthMethodTypesRequired{ - authMethods: []domain.UserAuthMethodType{ + object: &UserAuthMethodRequirements{ + UserType: domain.UserTypeHuman, + AuthMethods: []domain.UserAuthMethodType{ domain.UserAuthMethodTypePasswordless, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypePassword, domain.UserAuthMethodTypeIDP, }, - forceMFA: true, - forceMFALocalOnly: true, + ForceMFA: true, + ForceMFALocalOnly: true, }, }, { name: "prepareUserAuthMethodTypesRequiredQuery sql err", - prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) { + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*UserAuthMethodRequirements, error)) { builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db) - return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) { - authMethods, forceMFA, forceMFALocalOnly, err := scan(rows) - if err != nil { - return nil, err - } - return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA, forceMFALocalOnly: forceMFALocalOnly}, nil + return builder, func(rows *sql.Rows) (*UserAuthMethodRequirements, error) { + return scan(rows) } }, want: want{ @@ -452,10 +443,3 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }) } } - -// testUserAuthMethodTypesRequired is required as assetPrepare is only able to return a single object from scan -type testUserAuthMethodTypesRequired struct { - authMethods []domain.UserAuthMethodType - forceMFA bool - forceMFALocalOnly bool -}