diff --git a/server/db/models/user.go b/server/db/models/user.go index 3731f3b..f5fc61d 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -14,23 +14,24 @@ type User struct { Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"` - Email string `gorm:"unique" json:"email" bson:"email" cql:"email"` - EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"` - Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"` - SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"` - GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"` - FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"` - MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"` - Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"` - Gender *string `json:"gender" bson:"gender" cql:"gender"` - Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"` - PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"` - PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"` - Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"` - Roles string `json:"roles" bson:"roles" cql:"roles"` - RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"` - UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` - CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` + Email string `gorm:"unique" json:"email" bson:"email" cql:"email"` + EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"` + Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"` + SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"` + GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"` + FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"` + MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"` + Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"` + Gender *string `json:"gender" bson:"gender" cql:"gender"` + Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"` + PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"` + PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"` + Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"` + Roles string `json:"roles" bson:"roles" cql:"roles"` + RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"` + IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled" bson:"is_multi_factor_auth_enabled" cql:"is_multi_factor_auth_enabled"` + UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` + CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` } func (user *User) AsAPIUser() *model.User { @@ -42,23 +43,24 @@ func (user *User) AsAPIUser() *model.User { id = strings.TrimPrefix(id, Collections.WebhookLog+"/") } return &model.User{ - ID: id, - Email: user.Email, - EmailVerified: isEmailVerified, - SignupMethods: user.SignupMethods, - GivenName: user.GivenName, - FamilyName: user.FamilyName, - MiddleName: user.MiddleName, - Nickname: user.Nickname, - PreferredUsername: refs.NewStringRef(user.Email), - Gender: user.Gender, - Birthdate: user.Birthdate, - PhoneNumber: user.PhoneNumber, - PhoneNumberVerified: &isPhoneVerified, - Picture: user.Picture, - Roles: strings.Split(user.Roles, ","), - RevokedTimestamp: user.RevokedTimestamp, - CreatedAt: refs.NewInt64Ref(user.CreatedAt), - UpdatedAt: refs.NewInt64Ref(user.UpdatedAt), + ID: id, + Email: user.Email, + EmailVerified: isEmailVerified, + SignupMethods: user.SignupMethods, + GivenName: user.GivenName, + FamilyName: user.FamilyName, + MiddleName: user.MiddleName, + Nickname: user.Nickname, + PreferredUsername: refs.NewStringRef(user.Email), + Gender: user.Gender, + Birthdate: user.Birthdate, + PhoneNumber: user.PhoneNumber, + PhoneNumberVerified: &isPhoneVerified, + Picture: user.Picture, + Roles: strings.Split(user.Roles, ","), + RevokedTimestamp: user.RevokedTimestamp, + IsMultiFactorAuthEnabled: user.IsMultiFactorAuthEnabled, + CreatedAt: refs.NewInt64Ref(user.CreatedAt), + UpdatedAt: refs.NewInt64Ref(user.UpdatedAt), } } diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index e5a0469..9b35767 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -159,6 +159,12 @@ func NewProvider() (*provider, error) { if err != nil { return nil, err } + // add is_multi_factor_auth_enabled on users table + userTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD is_multi_factor_auth_enabled boolean;`, KeySpace, models.Collections.User) + err = session.Query(userTableAlterQuery).Exec() + if err != nil { + return nil, err + } // token is reserved keyword in cassandra, hence we need to use jwt_token verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest) diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 6b159e4..b9fec16 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -204,24 +204,25 @@ type ComplexityRoot struct { } User struct { - Birthdate func(childComplexity int) int - CreatedAt func(childComplexity int) int - Email func(childComplexity int) int - EmailVerified func(childComplexity int) int - FamilyName func(childComplexity int) int - Gender func(childComplexity int) int - GivenName func(childComplexity int) int - ID func(childComplexity int) int - MiddleName func(childComplexity int) int - Nickname func(childComplexity int) int - PhoneNumber func(childComplexity int) int - PhoneNumberVerified func(childComplexity int) int - Picture func(childComplexity int) int - PreferredUsername func(childComplexity int) int - RevokedTimestamp func(childComplexity int) int - Roles func(childComplexity int) int - SignupMethods func(childComplexity int) int - UpdatedAt func(childComplexity int) int + Birthdate func(childComplexity int) int + CreatedAt func(childComplexity int) int + Email func(childComplexity int) int + EmailVerified func(childComplexity int) int + FamilyName func(childComplexity int) int + Gender func(childComplexity int) int + GivenName func(childComplexity int) int + ID func(childComplexity int) int + IsMultiFactorAuthEnabled func(childComplexity int) int + MiddleName func(childComplexity int) int + Nickname func(childComplexity int) int + PhoneNumber func(childComplexity int) int + PhoneNumberVerified func(childComplexity int) int + Picture func(childComplexity int) int + PreferredUsername func(childComplexity int) int + RevokedTimestamp func(childComplexity int) int + Roles func(childComplexity int) int + SignupMethods func(childComplexity int) int + UpdatedAt func(childComplexity int) int } Users struct { @@ -1429,6 +1430,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.ID(childComplexity), true + case "User.is_multi_factor_auth_enabled": + if e.complexity.User.IsMultiFactorAuthEnabled == nil { + break + } + + return e.complexity.User.IsMultiFactorAuthEnabled(childComplexity), true + case "User.middle_name": if e.complexity.User.MiddleName == nil { break @@ -1836,6 +1844,7 @@ type User { created_at: Int64 updated_at: Int64 revoked_timestamp: Int64 + is_multi_factor_auth_enabled: Boolean } type Users { @@ -2052,6 +2061,7 @@ input SignUpInput { roles: [String!] scope: [String!] redirect_uri: String + is_multi_factor_auth_enabled: Boolean } input LoginInput { @@ -2083,6 +2093,7 @@ input UpdateProfileInput { birthdate: String phone_number: String picture: String + is_multi_factor_auth_enabled: Boolean } input UpdateUserInput { @@ -2098,6 +2109,7 @@ input UpdateUserInput { phone_number: String picture: String roles: [String] + is_multi_factor_auth_enabled: Boolean } input ForgotPasswordInput { @@ -7888,6 +7900,38 @@ func (ec *executionContext) _User_revoked_timestamp(ctx context.Context, field g return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) } +func (ec *executionContext) _User_is_multi_factor_auth_enabled(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsMultiFactorAuthEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10765,6 +10809,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i if err != nil { return it, err } + case "is_multi_factor_auth_enabled": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled")) + it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } } } @@ -11304,6 +11356,14 @@ func (ec *executionContext) unmarshalInputUpdateProfileInput(ctx context.Context if err != nil { return it, err } + case "is_multi_factor_auth_enabled": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled")) + it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } } } @@ -11415,6 +11475,14 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o if err != nil { return it, err } + case "is_multi_factor_auth_enabled": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled")) + it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } } } @@ -12482,6 +12550,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._User_updated_at(ctx, field, obj) case "revoked_timestamp": out.Values[i] = ec._User_revoked_timestamp(ctx, field, obj) + case "is_multi_factor_auth_enabled": + out.Values[i] = ec._User_is_multi_factor_auth_enabled(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 70762dc..0dcbae4 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -205,20 +205,21 @@ type SessionQueryInput struct { } type SignUpInput struct { - Email string `json:"email"` - GivenName *string `json:"given_name"` - FamilyName *string `json:"family_name"` - MiddleName *string `json:"middle_name"` - Nickname *string `json:"nickname"` - Gender *string `json:"gender"` - Birthdate *string `json:"birthdate"` - PhoneNumber *string `json:"phone_number"` - Picture *string `json:"picture"` - Password string `json:"password"` - ConfirmPassword string `json:"confirm_password"` - Roles []string `json:"roles"` - Scope []string `json:"scope"` - RedirectURI *string `json:"redirect_uri"` + Email string `json:"email"` + GivenName *string `json:"given_name"` + FamilyName *string `json:"family_name"` + MiddleName *string `json:"middle_name"` + Nickname *string `json:"nickname"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + PhoneNumber *string `json:"phone_number"` + Picture *string `json:"picture"` + Password string `json:"password"` + ConfirmPassword string `json:"confirm_password"` + Roles []string `json:"roles"` + Scope []string `json:"scope"` + RedirectURI *string `json:"redirect_uri"` + IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` } type TestEndpointRequest struct { @@ -285,33 +286,35 @@ type UpdateEnvInput struct { } type UpdateProfileInput struct { - OldPassword *string `json:"old_password"` - NewPassword *string `json:"new_password"` - ConfirmNewPassword *string `json:"confirm_new_password"` - Email *string `json:"email"` - GivenName *string `json:"given_name"` - FamilyName *string `json:"family_name"` - MiddleName *string `json:"middle_name"` - Nickname *string `json:"nickname"` - Gender *string `json:"gender"` - Birthdate *string `json:"birthdate"` - PhoneNumber *string `json:"phone_number"` - Picture *string `json:"picture"` + OldPassword *string `json:"old_password"` + NewPassword *string `json:"new_password"` + ConfirmNewPassword *string `json:"confirm_new_password"` + Email *string `json:"email"` + GivenName *string `json:"given_name"` + FamilyName *string `json:"family_name"` + MiddleName *string `json:"middle_name"` + Nickname *string `json:"nickname"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + PhoneNumber *string `json:"phone_number"` + Picture *string `json:"picture"` + IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` } type UpdateUserInput struct { - ID string `json:"id"` - Email *string `json:"email"` - EmailVerified *bool `json:"email_verified"` - GivenName *string `json:"given_name"` - FamilyName *string `json:"family_name"` - MiddleName *string `json:"middle_name"` - Nickname *string `json:"nickname"` - Gender *string `json:"gender"` - Birthdate *string `json:"birthdate"` - PhoneNumber *string `json:"phone_number"` - Picture *string `json:"picture"` - Roles []*string `json:"roles"` + ID string `json:"id"` + Email *string `json:"email"` + EmailVerified *bool `json:"email_verified"` + GivenName *string `json:"given_name"` + FamilyName *string `json:"family_name"` + MiddleName *string `json:"middle_name"` + Nickname *string `json:"nickname"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + PhoneNumber *string `json:"phone_number"` + Picture *string `json:"picture"` + Roles []*string `json:"roles"` + IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` } type UpdateWebhookRequest struct { @@ -323,24 +326,25 @@ type UpdateWebhookRequest struct { } type User struct { - ID string `json:"id"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` - SignupMethods string `json:"signup_methods"` - GivenName *string `json:"given_name"` - FamilyName *string `json:"family_name"` - MiddleName *string `json:"middle_name"` - Nickname *string `json:"nickname"` - PreferredUsername *string `json:"preferred_username"` - Gender *string `json:"gender"` - Birthdate *string `json:"birthdate"` - PhoneNumber *string `json:"phone_number"` - PhoneNumberVerified *bool `json:"phone_number_verified"` - Picture *string `json:"picture"` - Roles []string `json:"roles"` - CreatedAt *int64 `json:"created_at"` - UpdatedAt *int64 `json:"updated_at"` - RevokedTimestamp *int64 `json:"revoked_timestamp"` + ID string `json:"id"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + SignupMethods string `json:"signup_methods"` + GivenName *string `json:"given_name"` + FamilyName *string `json:"family_name"` + MiddleName *string `json:"middle_name"` + Nickname *string `json:"nickname"` + PreferredUsername *string `json:"preferred_username"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + PhoneNumber *string `json:"phone_number"` + PhoneNumberVerified *bool `json:"phone_number_verified"` + Picture *string `json:"picture"` + Roles []string `json:"roles"` + CreatedAt *int64 `json:"created_at"` + UpdatedAt *int64 `json:"updated_at"` + RevokedTimestamp *int64 `json:"revoked_timestamp"` + IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"` } type Users struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index ca0e7df..bcc91ac 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -47,6 +47,7 @@ type User { created_at: Int64 updated_at: Int64 revoked_timestamp: Int64 + is_multi_factor_auth_enabled: Boolean } type Users { @@ -263,6 +264,7 @@ input SignUpInput { roles: [String!] scope: [String!] redirect_uri: String + is_multi_factor_auth_enabled: Boolean } input LoginInput { @@ -294,6 +296,7 @@ input UpdateProfileInput { birthdate: String phone_number: String picture: String + is_multi_factor_auth_enabled: Boolean } input UpdateUserInput { @@ -309,6 +312,7 @@ input UpdateUserInput { phone_number: String picture: String roles: [String] + is_multi_factor_auth_enabled: Boolean } input ForgotPasswordInput { diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index dbd3652..71ffb93 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -157,6 +157,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR user.Picture = params.Picture } + if params.IsMultiFactorAuthEnabled != nil { + user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + } + user.SignupMethods = constants.AuthRecipeMethodBasicAuth isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) if err != nil { diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index 18058f2..d9c67b1 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -94,6 +94,10 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) user.Picture = params.Picture } + if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { + user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + } + isPasswordChanging := false if params.NewPassword != nil && params.ConfirmNewPassword == nil { isPasswordChanging = true diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index d2aa7b0..7cdf7bc 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -15,6 +15,7 @@ import ( "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/parsers" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/validators" @@ -56,38 +57,42 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod return res, fmt.Errorf(`User not found`) } - if params.GivenName != nil && user.GivenName != params.GivenName { + if params.GivenName != nil && refs.StringValue(user.GivenName) != refs.StringValue(params.GivenName) { user.GivenName = params.GivenName } - if params.FamilyName != nil && user.FamilyName != params.FamilyName { + if params.FamilyName != nil && refs.StringValue(user.FamilyName) != refs.StringValue(params.FamilyName) { user.FamilyName = params.FamilyName } - if params.MiddleName != nil && user.MiddleName != params.MiddleName { + if params.MiddleName != nil && refs.StringValue(user.MiddleName) != refs.StringValue(params.MiddleName) { user.MiddleName = params.MiddleName } - if params.Nickname != nil && user.Nickname != params.Nickname { + if params.Nickname != nil && refs.StringValue(user.Nickname) != refs.StringValue(params.Nickname) { user.Nickname = params.Nickname } - if params.Birthdate != nil && user.Birthdate != params.Birthdate { + if params.Birthdate != nil && refs.StringValue(user.Birthdate) != refs.StringValue(params.Birthdate) { user.Birthdate = params.Birthdate } - if params.Gender != nil && user.Gender != params.Gender { + if params.Gender != nil && refs.StringValue(user.Gender) != refs.StringValue(params.Gender) { user.Gender = params.Gender } - if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber { + if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(params.PhoneNumber) { user.PhoneNumber = params.PhoneNumber } - if params.Picture != nil && user.Picture != params.Picture { + if params.Picture != nil && refs.StringValue(user.Picture) != refs.StringValue(params.Picture) { user.Picture = params.Picture } + if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { + user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + } + if params.EmailVerified != nil { if *params.EmailVerified { now := time.Now().Unix()