From 9ef5f33f7a5859a98d4b5e43b9aea8c48c9e26d7 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 15:26:44 +0530 Subject: [PATCH 01/20] feat: add is_multi_factor_auth_enabled --- server/db/models/user.go | 72 ++++++------ server/db/providers/cassandradb/provider.go | 6 + server/graph/generated/generated.go | 106 +++++++++++++++--- server/graph/model/models_gen.go | 116 ++++++++++---------- server/graph/schema.graphqls | 4 + server/resolvers/signup.go | 4 + server/resolvers/update_profile.go | 4 + server/resolvers/update_user.go | 21 ++-- 8 files changed, 216 insertions(+), 117 deletions(-) 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() From f6c67243b9884c0624e00bf60206b715d608d1b2 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 15:55:06 +0530 Subject: [PATCH 02/20] feat: add otp model + implementation for sql --- server/db/models/model.go | 2 + server/db/models/otp.go | 12 ++++ server/db/providers/arangodb/otp.go | 38 +++++++++++++ server/db/providers/cassandradb/otp.go | 38 +++++++++++++ server/db/providers/mongodb/otp.go | 38 +++++++++++++ server/db/providers/provider_template/otp.go | 32 +++++++++++ server/db/providers/providers.go | 9 +++ server/db/providers/sql/otp.go | 60 ++++++++++++++++++++ server/db/providers/sql/provider.go | 2 +- 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 server/db/models/otp.go create mode 100644 server/db/providers/arangodb/otp.go create mode 100644 server/db/providers/cassandradb/otp.go create mode 100644 server/db/providers/mongodb/otp.go create mode 100644 server/db/providers/provider_template/otp.go create mode 100644 server/db/providers/sql/otp.go diff --git a/server/db/models/model.go b/server/db/models/model.go index bdbdaa3..c041482 100644 --- a/server/db/models/model.go +++ b/server/db/models/model.go @@ -9,6 +9,7 @@ type CollectionList struct { Webhook string WebhookLog string EmailTemplate string + OTP string } var ( @@ -23,5 +24,6 @@ var ( Webhook: Prefix + "webhook", WebhookLog: Prefix + "webhook_log", EmailTemplate: Prefix + "email_template", + OTP: Prefix + "otps", } ) diff --git a/server/db/models/otp.go b/server/db/models/otp.go new file mode 100644 index 0000000..cf0300b --- /dev/null +++ b/server/db/models/otp.go @@ -0,0 +1,12 @@ +package models + +// OTP model for database +type OTP 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"` + Otp string `json:"otp" bson:"otp" cql:"otp"` + ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at"` + CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` + UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` +} diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go new file mode 100644 index 0000000..77fc675 --- /dev/null +++ b/server/db/providers/arangodb/otp.go @@ -0,0 +1,38 @@ +package arangodb + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// AddOTP to add otp +func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + if otp.ID == "" { + otp.ID = uuid.New().String() + } + + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + otp.UpdatedAt = time.Now().Unix() + + return otp, nil +} + +// UpdateOTP to update otp for a given email address +func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + otp.UpdatedAt = time.Now().Unix() + return otp, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { + return nil, nil +} + +// DeleteOTP to delete otp +func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + return nil +} diff --git a/server/db/providers/cassandradb/otp.go b/server/db/providers/cassandradb/otp.go new file mode 100644 index 0000000..a057f7d --- /dev/null +++ b/server/db/providers/cassandradb/otp.go @@ -0,0 +1,38 @@ +package cassandradb + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// AddOTP to add otp +func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + if otp.ID == "" { + otp.ID = uuid.New().String() + } + + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + otp.UpdatedAt = time.Now().Unix() + + return otp, nil +} + +// UpdateOTP to update otp for a given email address +func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + otp.UpdatedAt = time.Now().Unix() + return otp, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { + return nil, nil +} + +// DeleteOTP to delete otp +func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + return nil +} diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go new file mode 100644 index 0000000..20e8aae --- /dev/null +++ b/server/db/providers/mongodb/otp.go @@ -0,0 +1,38 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// AddOTP to add otp +func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + if otp.ID == "" { + otp.ID = uuid.New().String() + } + + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + otp.UpdatedAt = time.Now().Unix() + + return otp, nil +} + +// UpdateOTP to update otp for a given email address +func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + otp.UpdatedAt = time.Now().Unix() + return otp, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { + return nil, nil +} + +// DeleteOTP to delete otp +func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + return nil +} diff --git a/server/db/providers/provider_template/otp.go b/server/db/providers/provider_template/otp.go new file mode 100644 index 0000000..e58c5eb --- /dev/null +++ b/server/db/providers/provider_template/otp.go @@ -0,0 +1,32 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// AddOTP to add otp +func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + if otp.ID == "" { + otp.ID = uuid.New().String() + } + + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + otp.UpdatedAt = time.Now().Unix() + + return otp, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { + return nil, nil +} + +// DeleteOTP to delete otp +func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + return nil +} diff --git a/server/db/providers/providers.go b/server/db/providers/providers.go index 6f8c074..cb3493a 100644 --- a/server/db/providers/providers.go +++ b/server/db/providers/providers.go @@ -72,4 +72,13 @@ type Provider interface { GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) // DeleteEmailTemplate to delete EmailTemplate DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error + + // AddOTP to add otp + AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) + // UpdateOTP to update otp for a given email address + UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) + // GetOTPByEmail to get otp for a given email address + GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) + // DeleteOTP to delete otp + DeleteOTP(ctx context.Context, otp *models.OTP) error } diff --git a/server/db/providers/sql/otp.go b/server/db/providers/sql/otp.go new file mode 100644 index 0000000..164a87d --- /dev/null +++ b/server/db/providers/sql/otp.go @@ -0,0 +1,60 @@ +package sql + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// AddOTP to add otp +func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + if otp.ID == "" { + otp.ID = uuid.New().String() + } + + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + otp.UpdatedAt = time.Now().Unix() + + res := p.db.Create(&otp) + if res.Error != nil { + return nil, res.Error + } + + return otp, nil +} + +// UpdateOTP to update otp for a given email address +func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + otp.UpdatedAt = time.Now().Unix() + + res := p.db.Save(&otp) + if res.Error != nil { + return nil, res.Error + } + return otp, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { + var otp models.OTP + + result := p.db.Where("email = ?", emailAddress).First(&otp) + if result.Error != nil { + return nil, result.Error + } + return &otp, nil +} + +// DeleteOTP to delete otp +func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + result := p.db.Delete(&models.OTP{ + ID: otp.ID, + }) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/server/db/providers/sql/provider.go b/server/db/providers/sql/provider.go index 70dc669..394bff3 100644 --- a/server/db/providers/sql/provider.go +++ b/server/db/providers/sql/provider.go @@ -60,7 +60,7 @@ func NewProvider() (*provider, error) { return nil, err } - err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}) + err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{}) if err != nil { return nil, err } From 1a27d91957812471a6e3d6bb101464155b043bfa Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 16:01:46 +0530 Subject: [PATCH 03/20] feat: add otp implementation for mongodb --- server/db/providers/mongodb/otp.go | 31 ++++++++++++++++++++++++- server/db/providers/mongodb/provider.go | 9 +++++++ server/db/providers/sql/otp.go | 6 ++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go index 20e8aae..715b02b 100644 --- a/server/db/providers/mongodb/otp.go +++ b/server/db/providers/mongodb/otp.go @@ -6,6 +6,8 @@ import ( "github.com/authorizerdev/authorizer/server/db/models" "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" ) // AddOTP to add otp @@ -18,21 +20,48 @@ func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, er otp.CreatedAt = time.Now().Unix() otp.UpdatedAt = time.Now().Unix() + otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) + _, err := otpCollection.InsertOne(ctx, otp) + if err != nil { + return nil, err + } + return otp, nil } // UpdateOTP to update otp for a given email address func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { otp.UpdatedAt = time.Now().Unix() + + otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) + _, err := otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions()) + if err != nil { + return nil, err + } + return otp, nil } // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - return nil, nil + var otp *models.OTP + + otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) + err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(otp) + if err != nil { + return nil, err + } + + return otp, nil } // DeleteOTP to delete otp func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) + _, err := otpCollection.DeleteOne(nil, bson.M{"_id": otp.ID}, options.Delete()) + if err != nil { + return err + } + return nil } diff --git a/server/db/providers/mongodb/provider.go b/server/db/providers/mongodb/provider.go index dd79284..7a969a9 100644 --- a/server/db/providers/mongodb/provider.go +++ b/server/db/providers/mongodb/provider.go @@ -110,6 +110,15 @@ func NewProvider() (*provider, error) { }, }, options.CreateIndexes()) + mongodb.CreateCollection(ctx, models.Collections.OTP, options.CreateCollection()) + otpCollection := mongodb.Collection(models.Collections.OTP, options.Collection()) + otpCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{ + { + Keys: bson.M{"email": 1}, + Options: options.Index().SetUnique(true).SetSparse(true), + }, + }, options.CreateIndexes()) + return &provider{ db: mongodb, }, nil diff --git a/server/db/providers/sql/otp.go b/server/db/providers/sql/otp.go index 164a87d..c83394b 100644 --- a/server/db/providers/sql/otp.go +++ b/server/db/providers/sql/otp.go @@ -39,13 +39,13 @@ func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp models.OTP + var otp *models.OTP - result := p.db.Where("email = ?", emailAddress).First(&otp) + result := p.db.Where("email = ?", emailAddress).First(otp) if result.Error != nil { return nil, result.Error } - return &otp, nil + return otp, nil } // DeleteOTP to delete otp From 22ae3bca540978cd2381db2afb422173e24134d9 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 16:06:52 +0530 Subject: [PATCH 04/20] feat: add otp implementation for arangodb --- server/db/providers/arangodb/otp.go | 49 +++++++++++++++++++++++- server/db/providers/arangodb/provider.go | 14 +++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go index 77fc675..0b546c5 100644 --- a/server/db/providers/arangodb/otp.go +++ b/server/db/providers/arangodb/otp.go @@ -2,6 +2,7 @@ package arangodb import ( "context" + "fmt" "time" "github.com/authorizerdev/authorizer/server/db/models" @@ -18,21 +19,67 @@ func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, er otp.CreatedAt = time.Now().Unix() otp.UpdatedAt = time.Now().Unix() + otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) + _, err := otpCollection.CreateDocument(ctx, otp) + if err != nil { + return nil, err + } + return otp, nil } // UpdateOTP to update otp for a given email address func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { otp.UpdatedAt = time.Now().Unix() + + otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) + meta, err := otpCollection.UpdateDocument(ctx, otp.Key, otp) + if err != nil { + return nil, err + } + + otp.Key = meta.Key + otp.ID = meta.ID.String() return otp, nil } // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - return nil, nil + var otp *models.OTP + query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP) + bindVars := map[string]interface{}{ + "email": emailAddress, + } + + cursor, err := p.db.Query(ctx, query, bindVars) + if err != nil { + return nil, err + } + defer cursor.Close() + + for { + if !cursor.HasMore() { + if otp.Key == "" { + return nil, fmt.Errorf("email template not found") + } + break + } + _, err := cursor.ReadDocument(ctx, otp) + if err != nil { + return nil, err + } + } + + return otp, nil } // DeleteOTP to delete otp func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) + _, err := otpCollection.RemoveDocument(ctx, otp.ID) + if err != nil { + return err + } + return nil } diff --git a/server/db/providers/arangodb/provider.go b/server/db/providers/arangodb/provider.go index a228cf8..6c26525 100644 --- a/server/db/providers/arangodb/provider.go +++ b/server/db/providers/arangodb/provider.go @@ -148,6 +148,20 @@ func NewProvider() (*provider, error) { Sparse: true, }) + otpCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.OTP) + if !otpCollectionExists { + _, err = arangodb.CreateCollection(ctx, models.Collections.OTP, nil) + if err != nil { + return nil, err + } + } + + otpCollection, _ := arangodb.Collection(nil, models.Collections.OTP) + otpCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{ + Unique: true, + Sparse: true, + }) + return &provider{ db: arangodb, }, err From f6029fb7bf711470ab263fdcc68828e6b9429b5b Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 16:39:35 +0530 Subject: [PATCH 05/20] feat: use upsert for otp + implement otp methods for cassandradb --- server/db/providers/arangodb/otp.go | 48 ++++++++++---------- server/db/providers/cassandradb/otp.go | 45 +++++++++++++----- server/db/providers/cassandradb/provider.go | 11 +++++ server/db/providers/mongodb/otp.go | 21 ++------- server/db/providers/provider_template/otp.go | 16 ++----- server/db/providers/providers.go | 6 +-- server/db/providers/sql/otp.go | 21 +++------ 7 files changed, 86 insertions(+), 82 deletions(-) diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go index 0b546c5..0e24095 100644 --- a/server/db/providers/arangodb/otp.go +++ b/server/db/providers/arangodb/otp.go @@ -9,40 +9,40 @@ import ( "github.com/google/uuid" ) -// AddOTP to add otp -func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - if otp.ID == "" { +// UpsertOTP to add or update otp +func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) { + otp, _ := p.GetOTPByEmail(ctx, otpParam.Email) + shouldCreate := false + if otp == nil { + shouldCreate = true otp.ID = uuid.New().String() + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + } else { + otp = otpParam } - otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() otp.UpdatedAt = time.Now().Unix() - otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) - _, err := otpCollection.CreateDocument(ctx, otp) - if err != nil { - return nil, err + + if shouldCreate { + _, err := otpCollection.CreateDocument(ctx, otp) + if err != nil { + return nil, err + } + } else { + meta, err := otpCollection.UpdateDocument(ctx, otp.Key, otp) + if err != nil { + return nil, err + } + + otp.Key = meta.Key + otp.ID = meta.ID.String() } return otp, nil } -// UpdateOTP to update otp for a given email address -func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - otp.UpdatedAt = time.Now().Unix() - - otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) - meta, err := otpCollection.UpdateDocument(ctx, otp.Key, otp) - if err != nil { - return nil, err - } - - otp.Key = meta.Key - otp.ID = meta.ID.String() - return otp, nil -} - // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { var otp *models.OTP diff --git a/server/db/providers/cassandradb/otp.go b/server/db/providers/cassandradb/otp.go index a057f7d..7ead206 100644 --- a/server/db/providers/cassandradb/otp.go +++ b/server/db/providers/cassandradb/otp.go @@ -2,37 +2,60 @@ package cassandradb import ( "context" + "fmt" "time" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/gocql/gocql" "github.com/google/uuid" ) -// AddOTP to add otp -func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - if otp.ID == "" { +// UpsertOTP to add or update otp +func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) { + otp, _ := p.GetOTPByEmail(ctx, otpParam.Email) + shouldCreate := false + if otp == nil { + shouldCreate = true otp.ID = uuid.New().String() + otp.Key = otp.ID + otp.CreatedAt = time.Now().Unix() + } else { + otp = otpParam } - otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() otp.UpdatedAt = time.Now().Unix() + query := "" - return otp, nil -} + if shouldCreate { + query = fmt.Sprintf(`INSERT INTO %s (id, email, otp, expires_at, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d, %d)`, KeySpace+"."+models.Collections.OTP, otp.ID, otp.Email, otp.Otp, otp.ExpiresAt, otp.CreatedAt, otp.UpdatedAt) + } else { + query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE email = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.Email) + } + err := p.db.Query(query).Exec() + if err != nil { + return nil, err + } -// UpdateOTP to update otp for a given email address -func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - otp.UpdatedAt = time.Now().Unix() return otp, nil } // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - return nil, nil + var otp models.OTP + query := fmt.Sprintf(`SELECT id, email, otp, expires_at, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.OTP, emailAddress) + err := p.db.Query(query).Consistency(gocql.One).Scan(&otp.ID, &otp.Email, &otp.Otp, &otp.ExpiresAt, &otp.CreatedAt, &otp.UpdatedAt) + if err != nil { + return nil, err + } + return &otp, nil } // DeleteOTP to delete otp func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error { + query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.OTP, otp.ID) + err := p.db.Query(query).Exec() + if err != nil { + return err + } return nil } diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index 9b35767..80a9cb6 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -221,6 +221,17 @@ func NewProvider() (*provider, error) { return nil, err } + otpCollection := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, email text, otp text, expires_at bigint, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.OTP) + err = session.Query(otpCollection).Exec() + if err != nil { + return nil, err + } + otpIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_otp_email ON %s.%s (email)", KeySpace, models.Collections.OTP) + err = session.Query(otpIndexQuery).Exec() + if err != nil { + return nil, err + } + return &provider{ db: session, }, err diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go index 715b02b..c3f637e 100644 --- a/server/db/providers/mongodb/otp.go +++ b/server/db/providers/mongodb/otp.go @@ -10,31 +10,20 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) -// AddOTP to add otp -func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { +// UpsertOTP to add or update otp +func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { if otp.ID == "" { otp.ID = uuid.New().String() } otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() - otp.UpdatedAt = time.Now().Unix() - - otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) - _, err := otpCollection.InsertOne(ctx, otp) - if err != nil { - return nil, err + if otp.CreatedAt <= 0 { + otp.CreatedAt = time.Now().Unix() } - - return otp, nil -} - -// UpdateOTP to update otp for a given email address -func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { otp.UpdatedAt = time.Now().Unix() otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) - _, err := otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions()) + _, err := otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions().SetUpsert(true)) if err != nil { return nil, err } diff --git a/server/db/providers/provider_template/otp.go b/server/db/providers/provider_template/otp.go index e58c5eb..d8685e7 100644 --- a/server/db/providers/provider_template/otp.go +++ b/server/db/providers/provider_template/otp.go @@ -2,23 +2,13 @@ package provider_template import ( "context" - "time" "github.com/authorizerdev/authorizer/server/db/models" - "github.com/google/uuid" ) -// AddOTP to add otp -func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - if otp.ID == "" { - otp.ID = uuid.New().String() - } - - otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() - otp.UpdatedAt = time.Now().Unix() - - return otp, nil +// UpsertOTP to add or update otp +func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { + return nil, nil } // GetOTPByEmail to get otp for a given email address diff --git a/server/db/providers/providers.go b/server/db/providers/providers.go index cb3493a..da72190 100644 --- a/server/db/providers/providers.go +++ b/server/db/providers/providers.go @@ -73,10 +73,8 @@ type Provider interface { // DeleteEmailTemplate to delete EmailTemplate DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error - // AddOTP to add otp - AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) - // UpdateOTP to update otp for a given email address - UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) + // UpsertOTP to add or update otp + UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) // GetOTPByEmail to get otp for a given email address GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) // DeleteOTP to delete otp diff --git a/server/db/providers/sql/otp.go b/server/db/providers/sql/otp.go index c83394b..8fd7780 100644 --- a/server/db/providers/sql/otp.go +++ b/server/db/providers/sql/otp.go @@ -6,10 +6,11 @@ import ( "github.com/authorizerdev/authorizer/server/db/models" "github.com/google/uuid" + "gorm.io/gorm/clause" ) -// AddOTP to add otp -func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { +// UpsertOTP to add or update otp +func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { if otp.ID == "" { otp.ID = uuid.New().String() } @@ -18,7 +19,10 @@ func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, er otp.CreatedAt = time.Now().Unix() otp.UpdatedAt = time.Now().Unix() - res := p.db.Create(&otp) + res := p.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "email"}}, + DoUpdates: clause.AssignmentColumns([]string{"otp", "expires_at", "updated_at"}), + }).Create(&otp) if res.Error != nil { return nil, res.Error } @@ -26,17 +30,6 @@ func (p *provider) AddOTP(ctx context.Context, otp *models.OTP) (*models.OTP, er return otp, nil } -// UpdateOTP to update otp for a given email address -func (p *provider) UpdateOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - otp.UpdatedAt = time.Now().Unix() - - res := p.db.Save(&otp) - if res.Error != nil { - return nil, res.Error - } - return otp, nil -} - // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { var otp *models.OTP From 44280be25a3e7710664001c9b224bb8901db4889 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 23 Jul 2022 16:44:39 +0530 Subject: [PATCH 06/20] feat: add resolver for verify_otp --- server/graph/generated/generated.go | 118 ++++++++++++++++++++++++++++ server/graph/model/models_gen.go | 5 ++ server/graph/schema.graphqls | 6 ++ server/graph/schema.resolvers.go | 10 ++- server/resolvers/verify_otp.go | 12 +++ 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 server/resolvers/verify_otp.go diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index b9fec16..850c964 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -170,6 +170,7 @@ type ComplexityRoot struct { UpdateUser func(childComplexity int, params model.UpdateUserInput) int UpdateWebhook func(childComplexity int, params model.UpdateWebhookRequest) int VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int + VerifyOtp func(childComplexity int, params model.VerifyOTPRequest) int } Pagination struct { @@ -293,6 +294,7 @@ type MutationResolver interface { ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) + VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -1201,6 +1203,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.VerifyEmail(childComplexity, args["params"].(model.VerifyEmailInput)), true + case "Mutation.verify_otp": + if e.complexity.Mutation.VerifyOtp == nil { + break + } + + args, err := ec.field_Mutation_verify_otp_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.VerifyOtp(childComplexity, args["params"].(model.VerifyOTPRequest)), true + case "Pagination.limit": if e.complexity.Pagination.Limit == nil { break @@ -2218,6 +2232,11 @@ input DeleteEmailTemplateRequest { id: ID! } +input VerifyOTPRequest { + email: String! + otp: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -2229,6 +2248,7 @@ type Mutation { forgot_password(params: ForgotPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response! revoke(params: OAuthRevokeInput!): Response! + verify_otp(params: VerifyOTPRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -2647,6 +2667,21 @@ func (ec *executionContext) field_Mutation_verify_email_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_verify_otp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.VerifyOTPRequest + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyOTPRequest(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5816,6 +5851,48 @@ func (ec *executionContext) _Mutation_revoke(ctx context.Context, field graphql. return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_verify_otp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_verify_otp_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().VerifyOtp(rctx, args["params"].(model.VerifyOTPRequest)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.AuthResponse) + fc.Result = res + return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -11606,6 +11683,37 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, obj interface{}) (model.VerifyOTPRequest, error) { + var it model.VerifyOTPRequest + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "email": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + it.Email, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "otp": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("otp")) + it.Otp, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputWebhookRequest(ctx context.Context, obj interface{}) (model.WebhookRequest, error) { var it model.WebhookRequest asMap := map[string]interface{}{} @@ -12099,6 +12207,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "verify_otp": + out.Values[i] = ec._Mutation_verify_otp(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "_delete_user": out.Values[i] = ec._Mutation__delete_user(ctx, field) if out.Values[i] == graphql.Null { @@ -13621,6 +13734,11 @@ func (ec *executionContext) unmarshalNVerifyEmailInput2githubᚗcomᚋauthorizer return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyOTPRequest(ctx context.Context, v interface{}) (model.VerifyOTPRequest, error) { + res, err := ec.unmarshalInputVerifyOTPRequest(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNWebhook2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐWebhook(ctx context.Context, sel ast.SelectionSet, v model.Webhook) graphql.Marshaler { return ec._Webhook(ctx, sel, &v) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 0dcbae4..e9addef 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -383,6 +383,11 @@ type VerifyEmailInput struct { Token string `json:"token"` } +type VerifyOTPRequest struct { + Email string `json:"email"` + Otp string `json:"otp"` +} + type Webhook struct { ID string `json:"id"` EventName *string `json:"event_name"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index bcc91ac..d8bc668 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -421,6 +421,11 @@ input DeleteEmailTemplateRequest { id: ID! } +input VerifyOTPRequest { + email: String! + otp: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -432,6 +437,7 @@ type Mutation { forgot_password(params: ForgotPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response! revoke(params: OAuthRevokeInput!): Response! + verify_otp(params: VerifyOTPRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 2cf0acf..193e1f0 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -51,6 +51,10 @@ func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeI return resolvers.RevokeResolver(ctx, params) } +func (r *mutationResolver) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) { + return resolvers.VerifyOtpResolver(ctx, params) +} + func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) } @@ -173,5 +177,7 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } +type ( + mutationResolver struct{ *Resolver } + queryResolver struct{ *Resolver } +) diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go new file mode 100644 index 0000000..213e028 --- /dev/null +++ b/server/resolvers/verify_otp.go @@ -0,0 +1,12 @@ +package resolvers + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/graph/model" +) + +// VerifyOtpResolver resolver for verify otp mutation +func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) { + return nil, nil +} From 49cc6033ab1357fb5e077a980980c30300340848 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Sat, 23 Jul 2022 18:32:31 +0530 Subject: [PATCH 07/20] update: verify otp resolver and test added --- server/db/providers/arangodb/otp.go | 6 +- server/db/providers/mongodb/otp.go | 6 +- server/db/providers/sql/otp.go | 6 +- server/resolvers/verify_otp.go | 102 +++++++++++++++++++++++++++- server/test/resolvers_test.go | 2 +- server/test/verify_otp_test.go | 42 ++++++++++++ 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 server/test/verify_otp_test.go diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go index 0e24095..076990b 100644 --- a/server/db/providers/arangodb/otp.go +++ b/server/db/providers/arangodb/otp.go @@ -45,7 +45,7 @@ func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP) bindVars := map[string]interface{}{ "email": emailAddress, @@ -64,13 +64,13 @@ func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*mod } break } - _, err := cursor.ReadDocument(ctx, otp) + _, err := cursor.ReadDocument(ctx, &otp) if err != nil { return nil, err } } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go index c3f637e..bbf0426 100644 --- a/server/db/providers/mongodb/otp.go +++ b/server/db/providers/mongodb/otp.go @@ -33,15 +33,15 @@ func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) - err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(otp) + err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(&otp) if err != nil { return nil, err } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/db/providers/sql/otp.go b/server/db/providers/sql/otp.go index 8fd7780..9aabcab 100644 --- a/server/db/providers/sql/otp.go +++ b/server/db/providers/sql/otp.go @@ -32,13 +32,13 @@ func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, // GetOTPByEmail to get otp for a given email address func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) { - var otp *models.OTP + var otp models.OTP - result := p.db.Where("email = ?", emailAddress).First(otp) + result := p.db.Where("email = ?", emailAddress).First(&otp) if result.Error != nil { return nil, result.Error } - return otp, nil + return &otp, nil } // DeleteOTP to delete otp diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 213e028..506f61f 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -2,11 +2,111 @@ package resolvers import ( "context" + "fmt" + "strings" + "time" + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" + log "github.com/sirupsen/logrus" ) // VerifyOtpResolver resolver for verify otp mutation func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) { - return nil, nil + var res *model.AuthResponse + gc, err := utils.GinContextFromContext(ctx) + + if err != nil { + log.Debug("Failed to get GinContext: ", err) + return res, err + } + + otp, err := db.Provider.GetOTPByEmail(ctx, params.Email) + + if err != nil { + log.Debug("Failed to get otp request by email: ", err) + return res, fmt.Errorf(`invalid email: %s`, err.Error()) + } + + expiresIn := otp.ExpiresAt - time.Now().Unix() + + if params.Otp != otp.Otp || expiresIn < 0 { + log.Debug("Failed to verify otp request: ", err) + return res, fmt.Errorf(`invalid otp: %s`, err.Error()) + } + + user, err := db.Provider.GetUserByEmail(ctx, params.Email) + + if err != nil { + log.Debug("Failed to get user by email: ", err) + return res, err + } + + isSignUp := user.EmailVerifiedAt == nil + + // TODO - Add Login method in DB + + loginMethod := constants.AuthRecipeMethodBasicAuth + + roles := strings.Split(user.Roles, ",") + scope := []string{"openid", "email", "profile"} + authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) + + if err != nil { + log.Debug("Failed to create auth token: ", err) + return res, err + } + + go func() { + err = db.Provider.DeleteOTP(gc, otp) + + if err != nil { + log.Debug("Failed to delete otp: ", err) + } + if isSignUp { + utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) + } else { + utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user) + } + + db.Provider.AddSession(ctx, models.Session{ + UserID: user.ID, + UserAgent: utils.GetUserAgent(gc.Request), + IP: utils.GetIP(gc.Request), + }) + }() + + authTokenExpiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if authTokenExpiresIn <= 0 { + authTokenExpiresIn = 1 + } + + if authTokenExpiresIn <= 0 { + authTokenExpiresIn = 1 + } + + res = &model.AuthResponse{ + Message: `OTP verified successfully.`, + AccessToken: &authToken.AccessToken.Token, + IDToken: &authToken.IDToken.Token, + ExpiresIn: &authTokenExpiresIn, + User: user.AsAPIUser(), + } + + sessionKey := loginMethod + ":" + user.ID + cookie.SetSession(gc, authToken.FingerPrintHash) + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash) + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token) + + if authToken.RefreshToken != nil { + res.RefreshToken = &authToken.RefreshToken.Token + memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token) + } + return res, nil } diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index 1a49632..a76522e 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -114,7 +114,7 @@ func TestResolvers(t *testing.T) { metaTests(t, s) inviteUserTest(t, s) validateJwtTokenTest(t, s) - + verifyOTPTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done deleteWebhookTest(t, s) // delete webhooks (admin resolver) }) diff --git a/server/test/verify_otp_test.go b/server/test/verify_otp_test.go new file mode 100644 index 0000000..d236644 --- /dev/null +++ b/server/test/verify_otp_test.go @@ -0,0 +1,42 @@ +package test + +import ( + "testing" + "time" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func verifyOTPTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should verify otp`, func(t *testing.T) { + _, ctx := createContext(s) + email := "verify_otp." + s.TestInfo.Email + res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ + Email: email, + Password: s.TestInfo.Password, + ConfirmPassword: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + otp, err := db.Provider.UpsertOTP(ctx, &models.OTP{ + Otp: "123456", + Email: email, + ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + }) + assert.Equal(t, email, otp.Email) + assert.Nil(t, res.AccessToken, "access token should be nil") + + verifyRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + Otp: "123456", + Email: email, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") + cleanData(email) + }) +} From 480438fb7a3aa3ad815cddb1f6c2e407ee97dbd9 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Sat, 23 Jul 2022 20:04:39 +0530 Subject: [PATCH 08/20] fix: remove duplicate code in verify otp resolver --- server/resolvers/verify_otp.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 506f61f..97647b3 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -87,10 +87,6 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod authTokenExpiresIn = 1 } - if authTokenExpiresIn <= 0 { - authTokenExpiresIn = 1 - } - res = &model.AuthResponse{ Message: `OTP verified successfully.`, AccessToken: &authToken.AccessToken.Token, From ef22318d5ccb4200f613a14ec08bff3d6d516902 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Sun, 24 Jul 2022 10:40:37 +0530 Subject: [PATCH 09/20] feat: add generate_otp util --- server/utils/generate_otp.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 server/utils/generate_otp.go diff --git a/server/utils/generate_otp.go b/server/utils/generate_otp.go new file mode 100644 index 0000000..eb26e97 --- /dev/null +++ b/server/utils/generate_otp.go @@ -0,0 +1,24 @@ +package utils + +import ( + "math/rand" + "time" +) + +func GenerateOTP() string { + code := "" + codeLength := 6 + charSet := "ABCDEFGHJKLMNPQRSTUVWXYZ123456789" + charSetLength := int32(len(charSet)) + for i := 0; i < codeLength; i++ { + index := randomNumber(0, charSetLength) + code += string(charSet[index]) + } + + return code +} + +func randomNumber(min, max int32) int32 { + rand.Seed(time.Now().UnixNano()) + return min + int32(rand.Intn(int(max-min))) +} From 4e23e49de4aa43878a6bf6128e4a3c1abc430974 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 25 Jul 2022 18:08:07 +0530 Subject: [PATCH 10/20] fix: syntax --- server/resolvers/verify_otp.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 506f61f..22a79a3 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -21,14 +21,12 @@ import ( func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) { var res *model.AuthResponse gc, err := utils.GinContextFromContext(ctx) - if err != nil { log.Debug("Failed to get GinContext: ", err) return res, err } otp, err := db.Provider.GetOTPByEmail(ctx, params.Email) - if err != nil { log.Debug("Failed to get otp request by email: ", err) return res, fmt.Errorf(`invalid email: %s`, err.Error()) @@ -42,7 +40,6 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } user, err := db.Provider.GetUserByEmail(ctx, params.Email) - if err != nil { log.Debug("Failed to get user by email: ", err) return res, err @@ -57,7 +54,6 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod roles := strings.Split(user.Roles, ",") scope := []string{"openid", "email", "profile"} authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod) - if err != nil { log.Debug("Failed to create auth token: ", err) return res, err From 9fae8215d215c3bd38148f42467fc23dded8fbd0 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Wed, 27 Jul 2022 12:18:32 +0530 Subject: [PATCH 11/20] feat: dashboard - add actions to update is_multi_factor_auth_enabled --- .../components/EnvComponents/OAuthConfig.tsx | 8 +-- dashboard/src/graphql/queries/index.ts | 1 + dashboard/src/pages/Users.tsx | 56 +++++++++++++++++++ server/resolvers/update_profile.go | 2 +- server/resolvers/update_user.go | 2 +- 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/dashboard/src/components/EnvComponents/OAuthConfig.tsx b/dashboard/src/components/EnvComponents/OAuthConfig.tsx index 4536ade..4537997 100644 --- a/dashboard/src/components/EnvComponents/OAuthConfig.tsx +++ b/dashboard/src/components/EnvComponents/OAuthConfig.tsx @@ -108,7 +108,7 @@ const OAuthConfig = ({ fieldVisibility={fieldVisibility} setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.GOOGLE_CLIENT_SECRET} - placeholder="Google Secret" + placeholder="Google Client Secret" /> @@ -146,7 +146,7 @@ const OAuthConfig = ({ fieldVisibility={fieldVisibility} setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.GITHUB_CLIENT_SECRET} - placeholder="Github Secret" + placeholder="Github Client Secret" /> @@ -184,7 +184,7 @@ const OAuthConfig = ({ fieldVisibility={fieldVisibility} setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET} - placeholder="Facebook Secret" + placeholder="Facebook Client Secret" /> @@ -260,7 +260,7 @@ const OAuthConfig = ({ fieldVisibility={fieldVisibility} setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.APPLE_CLIENT_SECRET} - placeholder="Apple CLient Secret" + placeholder="Apple Client Secret" /> diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index ac9ba15..a4632c3 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -89,6 +89,7 @@ export const UserDetailsQuery = ` roles created_at revoked_timestamp + is_multi_factor_auth_enabled } } } diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 6da5c83..c32a62d 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -68,6 +68,7 @@ interface userDataTypes { roles: [string]; created_at: number; revoked_timestamp: number; + is_multi_factor_auth_enabled?: boolean; } const enum updateAccessActions { @@ -250,6 +251,34 @@ export default function Users() { break; } }; + const multiFactorAuthUpdateHandler = async (user: userDataTypes) => { + const res = await client + .mutation(UpdateUser, { + params: { + id: user.id, + is_multi_factor_auth_enabled: !user.is_multi_factor_auth_enabled, + }, + }) + .toPromise(); + if (res.data?._update_user?.id) { + toast({ + title: `Multi factor authentication ${ + user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled' + } for user`, + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + updateUserList(); + return; + } + toast({ + title: 'Multi factor authentication update failed for user', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + }; return ( @@ -273,6 +302,7 @@ export default function Users() { Roles Verified Access + MFA Actions @@ -305,6 +335,19 @@ export default function Users() { {user.revoked_timestamp ? 'Revoked' : 'Enabled'} + + + {user.is_multi_factor_auth_enabled + ? 'Enabled' + : 'Disabled'} + + @@ -357,6 +400,19 @@ export default function Users() { Revoke Access )} + {user.is_multi_factor_auth_enabled ? ( + multiFactorAuthUpdateHandler(user)} + > + Disable MFA + + ) : ( + multiFactorAuthUpdateHandler(user)} + > + Enable MFA + + )} diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index d9c67b1..ac2947f 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -46,7 +46,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) } // validate if all params are not empty - if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.NewPassword == nil && params.ConfirmNewPassword == nil { + if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.NewPassword == nil && params.ConfirmNewPassword == nil && params.IsMultiFactorAuthEnabled == nil { log.Debug("All params are empty") return res, fmt.Errorf("please enter at least one param to update") } diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index 7cdf7bc..da9c58d 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -46,7 +46,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod "user_id": params.ID, }) - if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil { + if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil && params.IsMultiFactorAuthEnabled == nil { log.Debug("No params to update") return res, fmt.Errorf("please enter atleast one param to update") } From f12491e42d227a294aa66cbd2b25137a0ab7d812 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Wed, 27 Jul 2022 15:28:12 +0530 Subject: [PATCH 12/20] fix: auth response schema updated --- server/graph/schema.graphqls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 9079ab1..3a843fe 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -79,8 +79,7 @@ type Error { type AuthResponse { message: String! - has_multi_factor_auth_enabled: Boolean - is_otp_sent: Boolean + should_show_otp_screen: Boolean access_token: String id_token: String refresh_token: String From e3c58ffbb060494ec7196315593e08170116285b Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Thu, 28 Jul 2022 11:18:06 +0530 Subject: [PATCH 13/20] fix: login resolver multifactor auth --- dashboard/package-lock.json | 12 +++---- server/graph/generated/generated.go | 55 +++++++++++++++++++++++++---- server/graph/model/models_gen.go | 13 +++---- server/graph/schema.resolvers.go | 6 ++-- server/resolvers/login.go | 8 +++++ 5 files changed, 70 insertions(+), 24 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c04cac0..41d31f9 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2529,8 +2529,7 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", - "requires": {} + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3134,8 +3133,7 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@popperjs/core": { "version": "2.11.0", @@ -3845,8 +3843,7 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", - "requires": {} + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "16.13.1", @@ -4032,8 +4029,7 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", - "requires": {} + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" }, "use-sidecar": { "version": "1.0.5", diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 850c964..535821f 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -44,12 +44,13 @@ type DirectiveRoot struct { type ComplexityRoot struct { AuthResponse struct { - AccessToken func(childComplexity int) int - ExpiresIn func(childComplexity int) int - IDToken func(childComplexity int) int - Message func(childComplexity int) int - RefreshToken func(childComplexity int) int - User func(childComplexity int) int + AccessToken func(childComplexity int) int + ExpiresIn func(childComplexity int) int + IDToken func(childComplexity int) int + Message func(childComplexity int) int + RefreshToken func(childComplexity int) int + ShouldShowOtpScreen func(childComplexity int) int + User func(childComplexity int) int } EmailTemplate struct { @@ -378,6 +379,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.RefreshToken(childComplexity), true + case "AuthResponse.should_show_otp_screen": + if e.complexity.AuthResponse.ShouldShowOtpScreen == nil { + break + } + + return e.complexity.AuthResponse.ShouldShowOtpScreen(childComplexity), true + case "AuthResponse.user": if e.complexity.AuthResponse.User == nil { break @@ -1890,6 +1898,7 @@ type Error { type AuthResponse { message: String! + should_show_otp_screen: Boolean access_token: String id_token: String refresh_token: String @@ -2890,6 +2899,38 @@ func (ec *executionContext) _AuthResponse_message(ctx context.Context, field gra return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _AuthResponse_should_show_otp_screen(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AuthResponse", + 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.ShouldShowOtpScreen, 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) _AuthResponse_access_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -11761,6 +11802,8 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection if out.Values[i] == graphql.Null { invalids++ } + case "should_show_otp_screen": + out.Values[i] = ec._AuthResponse_should_show_otp_screen(ctx, field, obj) case "access_token": out.Values[i] = ec._AuthResponse_access_token(ctx, field, obj) case "id_token": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index e9addef..bbdb2f7 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -23,12 +23,13 @@ type AdminSignupInput struct { } type AuthResponse struct { - Message string `json:"message"` - AccessToken *string `json:"access_token"` - IDToken *string `json:"id_token"` - RefreshToken *string `json:"refresh_token"` - ExpiresIn *int64 `json:"expires_in"` - User *User `json:"user"` + Message string `json:"message"` + ShouldShowOtpScreen *bool `json:"should_show_otp_screen"` + AccessToken *string `json:"access_token"` + IDToken *string `json:"id_token"` + RefreshToken *string `json:"refresh_token"` + ExpiresIn *int64 `json:"expires_in"` + User *User `json:"user"` } type DeleteEmailTemplateRequest struct { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 193e1f0..e224137 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -177,7 +177,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index e55f984..c163528 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -15,6 +15,7 @@ import ( "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/validators" @@ -97,6 +98,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes scope = params.Scope } + if refs.BoolValue(user.IsMultiFactorAuthEnabled) { + return &model.AuthResponse{ + Message: "Please check the OTP in your inbox", + ShouldShowOtpScreen: refs.NewBoolRef(true), + }, nil + } + authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) if err != nil { log.Debug("Failed to create auth token", err) From 4e3d73e76761943e63dfd80b8bff50eef1cba3bb Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Fri, 29 Jul 2022 13:49:46 +0530 Subject: [PATCH 14/20] feat: otp resolvers updated --- server/graph/generated/generated.go | 109 ++++++++++++++++++++++++++++ server/graph/model/models_gen.go | 4 + server/graph/schema.graphqls | 5 ++ server/graph/schema.resolvers.go | 4 + server/resolvers/login.go | 6 ++ server/resolvers/resend_otp.go | 59 +++++++++++++++ server/resolvers/verify_otp.go | 11 ++- 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 server/resolvers/resend_otp.go diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 535821f..44c181f 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -159,6 +159,7 @@ type ComplexityRoot struct { Login func(childComplexity int, params model.LoginInput) int Logout func(childComplexity int) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int + ResendOtp func(childComplexity int, params model.ResendOTPRequest) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int Revoke func(childComplexity int, params model.OAuthRevokeInput) int @@ -296,6 +297,7 @@ type MutationResolver interface { ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) + ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -1067,6 +1069,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.MagicLinkLogin(childComplexity, args["params"].(model.MagicLinkLoginInput)), true + case "Mutation.resend_otp": + if e.complexity.Mutation.ResendOtp == nil { + break + } + + args, err := ec.field_Mutation_resend_otp_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ResendOtp(childComplexity, args["params"].(model.ResendOTPRequest)), true + case "Mutation.resend_verify_email": if e.complexity.Mutation.ResendVerifyEmail == nil { break @@ -2246,6 +2260,10 @@ input VerifyOTPRequest { otp: String! } +input ResendOTPRequest { + email: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -2258,6 +2276,7 @@ type Mutation { reset_password(params: ResetPasswordInput!): Response! revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! + resend_otp(params: ResendOTPRequest!): Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -2586,6 +2605,21 @@ func (ec *executionContext) field_Mutation_magic_link_login_args(ctx context.Con return args, nil } +func (ec *executionContext) field_Mutation_resend_otp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.ResendOTPRequest + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNResendOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendOTPRequest(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_resend_verify_email_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -5934,6 +5968,48 @@ func (ec *executionContext) _Mutation_verify_otp(ctx context.Context, field grap return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_resend_otp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_resend_otp_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ResendOtp(rctx, args["params"].(model.ResendOTPRequest)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Response) + fc.Result = res + return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10705,6 +10781,29 @@ func (ec *executionContext) unmarshalInputPaginationInput(ctx context.Context, o return it, nil } +func (ec *executionContext) unmarshalInputResendOTPRequest(ctx context.Context, obj interface{}) (model.ResendOTPRequest, error) { + var it model.ResendOTPRequest + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "email": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + it.Email, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) { var it model.ResendVerifyEmailInput asMap := map[string]interface{}{} @@ -12255,6 +12354,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "resend_otp": + out.Values[i] = ec._Mutation_resend_otp(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "_delete_user": out.Values[i] = ec._Mutation__delete_user(ctx, field) if out.Values[i] == graphql.Null { @@ -13484,6 +13588,11 @@ func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdev return ec._Pagination(ctx, sel, v) } +func (ec *executionContext) unmarshalNResendOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendOTPRequest(ctx context.Context, v interface{}) (model.ResendOTPRequest, error) { + res, err := ec.unmarshalInputResendOTPRequest(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNResendVerifyEmailInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendVerifyEmailInput(ctx context.Context, v interface{}) (model.ResendVerifyEmailInput, error) { res, err := ec.unmarshalInputResendVerifyEmailInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index bbdb2f7..b193c0d 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -185,6 +185,10 @@ type PaginationInput struct { Page *int64 `json:"page"` } +type ResendOTPRequest struct { + Email string `json:"email"` +} + type ResendVerifyEmailInput struct { Email string `json:"email"` Identifier string `json:"identifier"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 3a843fe..e3d6908 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -427,6 +427,10 @@ input VerifyOTPRequest { otp: String! } +input ResendOTPRequest { + email: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -439,6 +443,7 @@ type Mutation { reset_password(params: ResetPasswordInput!): Response! revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! + resend_otp(params: ResendOTPRequest!): Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index e224137..8d5b55b 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -55,6 +55,10 @@ func (r *mutationResolver) VerifyOtp(ctx context.Context, params model.VerifyOTP return resolvers.VerifyOtpResolver(ctx, params) } +func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) { + return resolvers.ResendOTPResolver(ctx, params) +} + func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index c163528..c7fafe3 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -99,6 +99,12 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if refs.BoolValue(user.IsMultiFactorAuthEnabled) { + //TODO - send email based on email config + db.Provider.UpsertOTP(ctx, &models.OTP{ + Email: user.Email, + Otp: utils.GenerateOTP(), + ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + }) return &model.AuthResponse{ Message: "Please check the OTP in your inbox", ShouldShowOtpScreen: refs.NewBoolRef(true), diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go new file mode 100644 index 0000000..1eb1333 --- /dev/null +++ b/server/resolvers/resend_otp.go @@ -0,0 +1,59 @@ +package resolvers + +import ( + "context" + "fmt" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/utils" +) + +// ResendOTPResolver is a resolver for resend otp mutation +func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) { + var res *model.Response + + log := log.WithFields(log.Fields{ + "email": params.Email, + }) + params.Email = strings.ToLower(params.Email) + user, err := db.Provider.GetUserByEmail(ctx, params.Email) + if err != nil { + log.Debug("Failed to get user by email: ", err) + return res, fmt.Errorf(`user with this email not found`) + } + + if user.RevokedTimestamp != nil { + log.Debug("User access is revoked") + return res, fmt.Errorf(`user access has been revoked`) + } + + if user.EmailVerifiedAt == nil { + log.Debug("User email is not verified") + return res, fmt.Errorf(`email not verified`) + } + + if !refs.BoolValue(user.IsMultiFactorAuthEnabled) { + log.Debug("User multi factor authentication is not enabled") + return res, fmt.Errorf(`multi factor authentication not enabled`) + } + + //TODO - send email based on email config + db.Provider.UpsertOTP(ctx, &models.OTP{ + Email: user.Email, + Otp: utils.GenerateOTP(), + ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + }) + + res = &model.Response{ + Message: `OTP has been sent. Please check your inbox`, + } + + return res, nil +} diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index f252dd0..95bb78d 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -32,11 +32,16 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, fmt.Errorf(`invalid email: %s`, err.Error()) } + if params.Otp != otp.Otp { + log.Debug("Failed to verify otp request: Incorrect value") + return res, fmt.Errorf(`invalid otp`) + } + expiresIn := otp.ExpiresAt - time.Now().Unix() - if params.Otp != otp.Otp || expiresIn < 0 { - log.Debug("Failed to verify otp request: ", err) - return res, fmt.Errorf(`invalid otp: %s`, err.Error()) + if expiresIn < 0 { + log.Debug("Failed to verify otp request: Timeout") + return res, fmt.Errorf("otp expired") } user, err := db.Provider.GetUserByEmail(ctx, params.Email) From 0fc9e8ccaa7a4c125c0413eee8d9918ae2e7bbec Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 29 Jul 2022 16:00:12 +0530 Subject: [PATCH 15/20] feat: add EnvKeyIsEmailServiceEnabled --- server/constants/env.go | 2 ++ server/env/env.go | 5 +++++ server/env/persist_env.go | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/constants/env.go b/server/constants/env.go index 7b0b06f..dc71bc6 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -47,6 +47,8 @@ const ( EnvKeySmtpPassword = "SMTP_PASSWORD" // EnvKeySenderEmail key for env variable SENDER_EMAIL EnvKeySenderEmail = "SENDER_EMAIL" + // EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED + EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED" // EnvKeyJwtType key for env variable JWT_TYPE EnvKeyJwtType = "JWT_TYPE" // EnvKeyJwtSecret key for env variable JWT_SECRET diff --git a/server/env/env.go b/server/env/env.go index 8d6c3c4..c492b13 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -494,6 +494,11 @@ func InitAllEnv() error { if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" { envData[constants.EnvKeyDisableEmailVerification] = true envData[constants.EnvKeyDisableMagicLinkLogin] = true + envData[constants.EnvKeyIsEmailServiceEnabled] = false + } + + if envData[constants.EnvKeySmtpHost] != "" || envData[constants.EnvKeySmtpUsername] != "" || envData[constants.EnvKeySmtpPassword] != "" || envData[constants.EnvKeySenderEmail] != "" && envData[constants.EnvKeySmtpPort] != "" { + envData[constants.EnvKeyIsEmailServiceEnabled] = true } if envData[constants.EnvKeyDisableEmailVerification].(bool) { diff --git a/server/env/persist_env.go b/server/env/persist_env.go index e9b849d..355ef7d 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -201,7 +201,7 @@ func PersistEnv() error { envValue := strings.TrimSpace(os.Getenv(key)) if envValue != "" { switch key { - case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword: + case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled: if envValueBool, err := strconv.ParseBool(envValue); err == nil { if value.(bool) != envValueBool { storeData[key] = envValueBool @@ -223,6 +223,7 @@ func PersistEnv() error { if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" { if !storeData[constants.EnvKeyDisableEmailVerification].(bool) { storeData[constants.EnvKeyDisableEmailVerification] = true + storeData[constants.EnvKeyIsEmailServiceEnabled] = false hasChanged = true } From d89be44fe585dbf100e30cc417c46c56b992d851 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 29 Jul 2022 19:49:50 +0530 Subject: [PATCH 16/20] feat: add sending otp --- app/package-lock.json | 30 ++--- app/package.json | 2 +- dashboard/package-lock.json | 12 +- server/email/otp.go | 118 ++++++++++++++++++++ server/env/persist_env.go | 3 +- server/memorystore/memory_store.go | 1 + server/memorystore/providers/redis/store.go | 2 +- server/resolvers/invite_members.go | 6 +- server/resolvers/login.go | 25 ++++- server/resolvers/resend_otp.go | 49 +++++--- server/resolvers/update_env.go | 5 + server/resolvers/update_profile.go | 8 ++ server/resolvers/update_user.go | 8 ++ server/resolvers/verify_otp.go | 9 +- server/utils/gin_context.go | 2 +- 15 files changed, 231 insertions(+), 49 deletions(-) create mode 100644 server/email/otp.go diff --git a/app/package-lock.json b/app/package-lock.json index 4a9978e..26598e3 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.25.0", + "@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -26,9 +26,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", - "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", + "version": "0.17.0-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", + "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -37,11 +37,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", - "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", + "version": "0.26.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", + "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.14.0", + "@authorizerdev/authorizer-js": "^0.17.0-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -852,19 +852,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", - "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", + "version": "0.17.0-beta.1", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz", + "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", - "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", + "version": "0.26.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz", + "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==", "requires": { - "@authorizerdev/authorizer-js": "^0.14.0", + "@authorizerdev/authorizer-js": "^0.17.0-beta.1", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index 8c5b77e..c3234c9 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^0.25.0", + "@authorizerdev/authorizer-react": "^0.26.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 41d31f9..c04cac0 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2529,7 +2529,8 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", + "requires": {} }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3133,7 +3134,8 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "requires": {} }, "@popperjs/core": { "version": "2.11.0", @@ -3843,7 +3845,8 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", + "requires": {} }, "react-is": { "version": "16.13.1", @@ -4029,7 +4032,8 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", + "requires": {} }, "use-sidecar": { "version": "1.0.5", diff --git a/server/email/otp.go b/server/email/otp.go new file mode 100644 index 0000000..181a1e0 --- /dev/null +++ b/server/email/otp.go @@ -0,0 +1,118 @@ +package email + +import ( + log "github.com/sirupsen/logrus" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/memorystore" +) + +// SendOtpMail to send otp email +func SendOtpMail(toEmail, otp string) error { + // The receiver needs to be in slice as the receive supports multiple receiver + Receiver := []string{toEmail} + + Subject := "OTP for your multi factor authentication" + message := ` + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+
+
+ + + ` + data := make(map[string]interface{}, 3) + var err error + data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) + if err != nil { + return err + } + data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) + if err != nil { + return err + } + data["otp"] = otp + message = addEmailTemplate(message, data, "otp.tmpl") + // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) + + err = SendMail(Receiver, Subject, message) + if err != nil { + log.Warn("error sending email: ", err) + } + return err +} diff --git a/server/env/persist_env.go b/server/env/persist_env.go index 355ef7d..d783b93 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -221,9 +221,10 @@ func PersistEnv() error { // handle derivative cases like disabling email verification & magic login // in case SMTP is off but env is set to true if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" { + storeData[constants.EnvKeyIsEmailServiceEnabled] = false + if !storeData[constants.EnvKeyDisableEmailVerification].(bool) { storeData[constants.EnvKeyDisableEmailVerification] = true - storeData[constants.EnvKeyIsEmailServiceEnabled] = false hasChanged = true } diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index ad6a418..9cbbbb4 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -31,6 +31,7 @@ func InitMemStore() error { constants.EnvKeyDisableLoginPage: false, constants.EnvKeyDisableSignUp: false, constants.EnvKeyDisableStrongPassword: false, + constants.EnvKeyIsEmailServiceEnabled: false, } requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index 36b4b0c..d6ee1df 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) { return nil, err } for key, value := range data { - if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword { + if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled { boolValue, err := strconv.ParseBool(value) if err != nil { return res, err diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 2316456..914e7bb 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -35,13 +35,13 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } // this feature is only allowed if email server is configured - isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) + EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) if err != nil { log.Debug("Error getting email verification disabled: ", err) - isEmailVerificationDisabled = true + EnvKeyIsEmailServiceEnabled = false } - if isEmailVerificationDisabled { + if !EnvKeyIsEmailServiceEnabled { log.Debug("Email server is not configured") return nil, errors.New("email sending is disabled") } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index c7fafe3..7d1f28e 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -13,6 +14,7 @@ import ( "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/refs" @@ -99,12 +101,29 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if refs.BoolValue(user.IsMultiFactorAuthEnabled) { - //TODO - send email based on email config - db.Provider.UpsertOTP(ctx, &models.OTP{ + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled") + } + otp := utils.GenerateOTP() + otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, - Otp: utils.GenerateOTP(), + Otp: otp, ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), }) + if err != nil { + log.Debug("Failed to add otp: ", err) + return nil, err + } + + go func() { + err := email.SendOtpMail(user.Email, otpData.Otp) + if err != nil { + log.Debug("Failed to send otp email: ", err) + } + }() + return &model.AuthResponse{ Message: "Please check the OTP in your inbox", ShouldShowOtpScreen: refs.NewBoolRef(true), diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go index 1eb1333..60367c1 100644 --- a/server/resolvers/resend_otp.go +++ b/server/resolvers/resend_otp.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -10,6 +11,7 @@ import ( "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/utils" @@ -17,8 +19,6 @@ import ( // ResendOTPResolver is a resolver for resend otp mutation func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) { - var res *model.Response - log := log.WithFields(log.Fields{ "email": params.Email, }) @@ -26,34 +26,57 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod user, err := db.Provider.GetUserByEmail(ctx, params.Email) if err != nil { log.Debug("Failed to get user by email: ", err) - return res, fmt.Errorf(`user with this email not found`) + return nil, fmt.Errorf(`user with this email not found`) } if user.RevokedTimestamp != nil { log.Debug("User access is revoked") - return res, fmt.Errorf(`user access has been revoked`) + return nil, fmt.Errorf(`user access has been revoked`) } if user.EmailVerifiedAt == nil { log.Debug("User email is not verified") - return res, fmt.Errorf(`email not verified`) + return nil, fmt.Errorf(`email not verified`) } if !refs.BoolValue(user.IsMultiFactorAuthEnabled) { log.Debug("User multi factor authentication is not enabled") - return res, fmt.Errorf(`multi factor authentication not enabled`) + return nil, fmt.Errorf(`multi factor authentication not enabled`) } - //TODO - send email based on email config - db.Provider.UpsertOTP(ctx, &models.OTP{ + // get otp by email + otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email) + if err != nil { + log.Debug("Failed to get otp for given email: ", err) + return nil, err + } + + if otpData == nil { + log.Debug("No otp found for given email: ", params.Email) + return &model.Response{ + Message: "Failed to get for given email", + }, errors.New("failed to get otp for given email") + } + + otp := utils.GenerateOTP() + otpData, err = db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, - Otp: utils.GenerateOTP(), + Otp: otp, ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), }) - - res = &model.Response{ - Message: `OTP has been sent. Please check your inbox`, + if err != nil { + log.Debug("Error generating new otp: ", err) + return nil, err } - return res, nil + go func() { + err := email.SendOtpMail(params.Email, otp) + if err != nil { + log.Debug("Error sending otp email: ", otp) + } + }() + + return &model.Response{ + Message: `OTP has been sent. Please check your inbox`, + }, nil } diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 508d47e..30abe9e 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -234,6 +234,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model // handle derivative cases like disabling email verification & magic login // in case SMTP is off but env is set to true if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" { + updatedData[constants.EnvKeyIsEmailServiceEnabled] = false if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { updatedData[constants.EnvKeyDisableEmailVerification] = true } @@ -243,6 +244,10 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model } } + if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" { + updatedData[constants.EnvKeyIsEmailServiceEnabled] = true + } + // check the roles change if len(params.Roles) > 0 { if len(params.DefaultRoles) > 0 { diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index ac2947f..0a47376 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -96,6 +97,13 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + if refs.BoolValue(params.IsMultiFactorAuthEnabled) { + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled, so cannot enable multi factor authentication") + } + } } isPasswordChanging := false diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index da9c58d..d20e4a9 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "fmt" "strings" "time" @@ -91,6 +92,13 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled + if refs.BoolValue(params.IsMultiFactorAuthEnabled) { + isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEnvServiceEnabled { + log.Debug("Email service not enabled:") + return nil, errors.New("email service not enabled, so cannot enable multi factor authentication") + } + } } if params.EmailVerified != nil { diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 95bb78d..b792adb 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -52,8 +52,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod isSignUp := user.EmailVerifiedAt == nil - // TODO - Add Login method in DB - + // TODO - Add Login method in DB when we introduce OTP for social media login loginMethod := constants.AuthRecipeMethodBasicAuth roles := strings.Split(user.Roles, ",") @@ -65,11 +64,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } go func() { - err = db.Provider.DeleteOTP(gc, otp) - - if err != nil { - log.Debug("Failed to delete otp: ", err) - } + db.Provider.DeleteOTP(gc, otp) if isSignUp { utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) } else { diff --git a/server/utils/gin_context.go b/server/utils/gin_context.go index 72fd480..7e3ced6 100644 --- a/server/utils/gin_context.go +++ b/server/utils/gin_context.go @@ -7,7 +7,7 @@ import ( "github.com/gin-gonic/gin" ) -// TODO renamae GinContextKey -> GinContext +// TODO re-name GinContextKey -> GinContext // GinContext to get gin context from context func GinContextFromContext(ctx context.Context) (*gin.Context, error) { From 236045ac54c4530ed98c93b6cb53dc140417f75a Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 30 Jul 2022 01:12:20 +0530 Subject: [PATCH 17/20] feat: add resend otp test --- server/test/resend_otp_test.go | 99 ++++++++++++++++++++++++++++++++++ server/test/resolvers_test.go | 1 + server/test/test.go | 5 ++ server/test/verify_otp_test.go | 57 +++++++++++++++----- 4 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 server/test/resend_otp_test.go diff --git a/server/test/resend_otp_test.go b/server/test/resend_otp_test.go new file mode 100644 index 0000000..3202d9e --- /dev/null +++ b/server/test/resend_otp_test.go @@ -0,0 +1,99 @@ +package test + +import ( + "context" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func resendOTPTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should verify otp`, func(t *testing.T) { + req, ctx := createContext(s) + email := "verify_otp." + s.TestInfo.Email + res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ + Email: email, + Password: s.TestInfo.Password, + ConfirmPassword: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, res) + + // Login should fail as email is not verified + loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{ + Email: email, + Password: s.TestInfo.Password, + }) + assert.Error(t, err) + assert.Nil(t, loginRes) + verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) + assert.Nil(t, err) + assert.Equal(t, email, verificationRequest.Email) + verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ + Token: verificationRequest.Token, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") + + // Using access token update profile + s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken)) + ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) + _, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + }) + + // Resend otp should return error as no initial opt is being sent + resendOtpRes, err := resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{ + Email: email, + }) + assert.Error(t, err) + assert.Nil(t, resendOtpRes) + + // Login should not return error but access token should be empty as otp should have been sent + loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ + Email: email, + Password: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, loginRes) + assert.Nil(t, loginRes.AccessToken) + + // Get otp from db + otp, err := db.Provider.GetOTPByEmail(ctx, email) + assert.NoError(t, err) + assert.NotEmpty(t, otp.Otp) + + // resend otp + resendOtpRes, err = resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{ + Email: email, + }) + assert.NoError(t, err) + assert.NotEmpty(t, resendOtpRes.Message) + + newOtp, err := db.Provider.GetOTPByEmail(ctx, email) + assert.NoError(t, err) + assert.NotEmpty(t, newOtp.Otp) + assert.NotEqual(t, otp.Otp, newOtp) + + // Should return error for older otp + verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + Email: email, + Otp: otp.Otp, + }) + assert.Error(t, err) + + verifyOtpRes, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + Email: email, + Otp: newOtp.Otp, + }) + assert.NoError(t, err) + assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") + cleanData(email) + }) +} diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index a76522e..f10576e 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -115,6 +115,7 @@ func TestResolvers(t *testing.T) { inviteUserTest(t, s) validateJwtTokenTest(t, s) verifyOTPTest(t, s) + resendOTPTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done deleteWebhookTest(t, s) // delete webhooks (admin resolver) }) diff --git a/server/test/test.go b/server/test/test.go index b2288ff..1e5105b 100644 --- a/server/test/test.go +++ b/server/test/test.go @@ -57,6 +57,11 @@ func cleanData(email string) { err = db.Provider.DeleteVerificationRequest(ctx, verificationRequest) } + otp, err := db.Provider.GetOTPByEmail(ctx, email) + if err == nil { + err = db.Provider.DeleteOTP(ctx, otp) + } + dbUser, err := db.Provider.GetUserByEmail(ctx, email) if err == nil { db.Provider.DeleteUser(ctx, dbUser) diff --git a/server/test/verify_otp_test.go b/server/test/verify_otp_test.go index d236644..afb7e2e 100644 --- a/server/test/verify_otp_test.go +++ b/server/test/verify_otp_test.go @@ -1,12 +1,13 @@ package test import ( + "context" "testing" - "time" + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db" - "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/resolvers" "github.com/stretchr/testify/assert" ) @@ -14,7 +15,7 @@ import ( func verifyOTPTest(t *testing.T, s TestSetup) { t.Helper() t.Run(`should verify otp`, func(t *testing.T) { - _, ctx := createContext(s) + req, ctx := createContext(s) email := "verify_otp." + s.TestInfo.Email res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ Email: email, @@ -23,20 +24,50 @@ func verifyOTPTest(t *testing.T, s TestSetup) { }) assert.NoError(t, err) assert.NotNil(t, res) - otp, err := db.Provider.UpsertOTP(ctx, &models.OTP{ - Otp: "123456", - Email: email, - ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), - }) - assert.Equal(t, email, otp.Email) - assert.Nil(t, res.AccessToken, "access token should be nil") - verifyRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - Otp: "123456", - Email: email, + // Login should fail as email is not verified + loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{ + Email: email, + Password: s.TestInfo.Password, + }) + assert.Error(t, err) + assert.Nil(t, loginRes) + verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) + assert.Nil(t, err) + assert.Equal(t, email, verificationRequest.Email) + verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ + Token: verificationRequest.Token, }) assert.Nil(t, err) assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") + + // Using access token update profile + s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken)) + ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) + _, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + IsMultiFactorAuthEnabled: refs.NewBoolRef(true), + }) + + // Login should not return error but access token should be empty as otp should have been sent + loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ + Email: email, + Password: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, loginRes) + assert.Nil(t, loginRes.AccessToken) + + // Get otp from db + otp, err := db.Provider.GetOTPByEmail(ctx, email) + assert.NoError(t, err) + assert.NotEmpty(t, otp.Otp) + + verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ + Email: email, + Otp: otp.Otp, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty") cleanData(email) }) } From 587828b59b5fdcdc3558174fa82eb1adbcc6971f Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 2 Aug 2022 14:12:36 +0530 Subject: [PATCH 18/20] feat: add helper for updating all users --- Makefile | 14 +- server/constants/env.go | 2 + server/db/models/user.go | 10 +- server/db/models/verification_requests.go | 4 +- .../db/providers/arangodb/email_template.go | 1 + server/db/providers/arangodb/env.go | 1 + server/db/providers/arangodb/otp.go | 37 ++-- server/db/providers/arangodb/session.go | 1 + server/db/providers/arangodb/user.go | 41 +++- .../arangodb/verification_requests.go | 1 + server/db/providers/arangodb/webhook.go | 1 + server/db/providers/arangodb/webhook_log.go | 1 + server/db/providers/cassandradb/otp.go | 18 +- server/db/providers/cassandradb/provider.go | 7 +- server/db/providers/cassandradb/user.go | 100 +++++++++- server/db/providers/mongodb/otp.go | 34 +++- server/db/providers/mongodb/user.go | 26 +++ server/db/providers/provider_template/user.go | 9 + server/db/providers/providers.go | 3 + server/db/providers/sql/provider.go | 1 + server/db/providers/sql/user.go | 20 ++ server/env/env.go | 18 ++ server/env/persist_env.go | 2 +- server/go.mod | 3 +- server/go.sum | 4 + server/graph/generated/generated.go | 154 ++++++++++----- server/graph/model/models_gen.go | 176 +++++++++--------- server/graph/schema.graphqls | 2 + server/memorystore/memory_store.go | 15 +- .../inmemory/stores/session_store.go | 5 + server/memorystore/providers/redis/store.go | 2 +- server/resolvers/env.go | 1 + server/resolvers/login.go | 14 +- server/resolvers/resend_otp.go | 8 + server/resolvers/update_env.go | 4 +- server/resolvers/update_profile.go | 3 +- server/test/resend_otp_test.go | 4 +- server/test/resolvers_test.go | 4 +- server/test/update_all_users_tests.go | 67 +++++++ server/test/verify_otp_test.go | 4 +- server/utils/webhook.go | 17 ++ 41 files changed, 629 insertions(+), 210 deletions(-) create mode 100644 server/test/update_all_users_tests.go diff --git a/Makefile b/Makefile index b5539f7..ea87ffa 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,26 @@ clean: rm -rf build test: rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && TEST_DBS="sqlite" go test -p 1 -v ./test +test-mongodb: + docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15 + cd server && go clean --testcache && TEST_DBS="mongodb" go test -p 1 -v ./test + docker rm -vf authorizer_mongodb_db +test-scylladb: + docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla + cd server && go clean --testcache && TEST_DBS="scylladb" go test -p 1 -v ./test + docker rm -vf authorizer_scylla_db +test-arangodb: + docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4 + cd server && go clean --testcache && TEST_DBS="arangodb" go test -p 1 -v ./test + docker rm -vf authorizer_arangodb test-all-db: rm -rf server/test/test.db && rm -rf test.db docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15 docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4 cd server && go clean --testcache && TEST_DBS="sqlite,mongodb,arangodb,scylladb" go test -p 1 -v ./test - docker rm -vf authorizer_mongodb_db docker rm -vf authorizer_scylla_db + docker rm -vf authorizer_mongodb_db docker rm -vf authorizer_arangodb generate: cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate diff --git a/server/constants/env.go b/server/constants/env.go index dc71bc6..2b26688 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -119,6 +119,8 @@ const ( EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV" // EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD" + // EnvKeyEnforceMultiFactorAuthentication is key for env variable ENFORCE_MULTI_FACTOR_AUTHENTICATION + EnvKeyEnforceMultiFactorAuthentication = "ENFORCE_MULTI_FACTOR_AUTHENTICATION" // Slice variables // EnvKeyRoles key for env variable ROLES diff --git a/server/db/models/user.go b/server/db/models/user.go index f5fc61d..bc5bc04 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -38,12 +38,12 @@ func (user *User) AsAPIUser() *model.User { isEmailVerified := user.EmailVerifiedAt != nil isPhoneVerified := user.PhoneNumberVerifiedAt != nil - id := user.ID - if strings.Contains(id, Collections.WebhookLog+"/") { - id = strings.TrimPrefix(id, Collections.WebhookLog+"/") - } + // id := user.ID + // if strings.Contains(id, Collections.User+"/") { + // id = strings.TrimPrefix(id, Collections.User+"/") + // } return &model.User{ - ID: id, + ID: user.ID, Email: user.Email, EmailVerified: isEmailVerified, SignupMethods: user.SignupMethods, diff --git a/server/db/models/verification_requests.go b/server/db/models/verification_requests.go index 3e13a7f..992d9d8 100644 --- a/server/db/models/verification_requests.go +++ b/server/db/models/verification_requests.go @@ -25,8 +25,8 @@ type VerificationRequest struct { func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { id := v.ID - if strings.Contains(id, Collections.WebhookLog+"/") { - id = strings.TrimPrefix(id, Collections.WebhookLog+"/") + if strings.Contains(id, Collections.VerificationRequest+"/") { + id = strings.TrimPrefix(id, Collections.VerificationRequest+"/") } return &model.VerificationRequest{ diff --git a/server/db/providers/arangodb/email_template.go b/server/db/providers/arangodb/email_template.go index 4e64762..70dd474 100644 --- a/server/db/providers/arangodb/email_template.go +++ b/server/db/providers/arangodb/email_template.go @@ -16,6 +16,7 @@ import ( func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) { if emailTemplate.ID == "" { emailTemplate.ID = uuid.New().String() + emailTemplate.Key = emailTemplate.ID } emailTemplate.Key = emailTemplate.ID diff --git a/server/db/providers/arangodb/env.go b/server/db/providers/arangodb/env.go index 2c884d4..29687a8 100644 --- a/server/db/providers/arangodb/env.go +++ b/server/db/providers/arangodb/env.go @@ -15,6 +15,7 @@ import ( func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) { if env.ID == "" { env.ID = uuid.New().String() + env.Key = env.ID } env.CreatedAt = time.Now().Unix() diff --git a/server/db/providers/arangodb/otp.go b/server/db/providers/arangodb/otp.go index 076990b..29f265a 100644 --- a/server/db/providers/arangodb/otp.go +++ b/server/db/providers/arangodb/otp.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/arangodb/go-driver" "github.com/authorizerdev/authorizer/server/db/models" "github.com/google/uuid" ) @@ -14,32 +15,38 @@ func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models otp, _ := p.GetOTPByEmail(ctx, otpParam.Email) shouldCreate := false if otp == nil { + id := uuid.NewString() + otp = &models.OTP{ + ID: id, + Key: id, + Otp: otpParam.Otp, + Email: otpParam.Email, + ExpiresAt: otpParam.ExpiresAt, + CreatedAt: time.Now().Unix(), + } shouldCreate = true - otp.ID = uuid.New().String() - otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() } else { - otp = otpParam + otp.Otp = otpParam.Otp + otp.ExpiresAt = otpParam.ExpiresAt } otp.UpdatedAt = time.Now().Unix() otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP) + var meta driver.DocumentMeta + var err error if shouldCreate { - _, err := otpCollection.CreateDocument(ctx, otp) - if err != nil { - return nil, err - } + meta, err = otpCollection.CreateDocument(ctx, otp) } else { - meta, err := otpCollection.UpdateDocument(ctx, otp.Key, otp) - if err != nil { - return nil, err - } - - otp.Key = meta.Key - otp.ID = meta.ID.String() + meta, err = otpCollection.UpdateDocument(ctx, otp.Key, otp) } + if err != nil { + return nil, err + } + + otp.Key = meta.Key + otp.ID = meta.ID.String() return otp, nil } diff --git a/server/db/providers/arangodb/session.go b/server/db/providers/arangodb/session.go index 96896e5..9bc46ca 100644 --- a/server/db/providers/arangodb/session.go +++ b/server/db/providers/arangodb/session.go @@ -12,6 +12,7 @@ import ( func (p *provider) AddSession(ctx context.Context, session models.Session) error { if session.ID == "" { session.ID = uuid.New().String() + session.Key = session.ID } session.CreatedAt = time.Now().Unix() diff --git a/server/db/providers/arangodb/user.go b/server/db/providers/arangodb/user.go index abc3ec0..945de33 100644 --- a/server/db/providers/arangodb/user.go +++ b/server/db/providers/arangodb/user.go @@ -2,22 +2,26 @@ package arangodb import ( "context" + "encoding/json" "fmt" + "strings" "time" "github.com/arangodb/go-driver" arangoDriver "github.com/arangodb/go-driver" + "github.com/google/uuid" + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" - "github.com/google/uuid" ) // AddUser to save user information in database func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) { if user.ID == "" { user.ID = uuid.New().String() + user.Key = user.ID } if user.Roles == "" { @@ -65,7 +69,7 @@ func (p *provider) DeleteUser(ctx context.Context, user models.User) error { query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session) bindVars := map[string]interface{}{ - "user_id": user.ID, + "user_id": user.Key, } cursor, err := p.db.Query(ctx, query, bindVars) if err != nil { @@ -174,3 +178,36 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err return user, nil } + +// UpdateUsers to update multiple users, with parameters of user IDs slice +// If ids set to nil / empty all the users will be updated +func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { + // set updated_at time for all users + data["updated_at"] = time.Now().Unix() + + userInfoBytes, err := json.Marshal(data) + if err != nil { + return err + } + + query := "" + if ids != nil && len(ids) > 0 { + keysArray := "" + for _, id := range ids { + keysArray += fmt.Sprintf("'%s', ", id) + } + keysArray = strings.Trim(keysArray, " ") + keysArray = strings.TrimSuffix(keysArray, ",") + query = fmt.Sprintf("FOR u IN %s FILTER u._id IN [%s] UPDATE u._key with %s IN %s", models.Collections.User, keysArray, string(userInfoBytes), models.Collections.User) + } else { + query = fmt.Sprintf("FOR u IN %s UPDATE u._key with %s IN %s", models.Collections.User, string(userInfoBytes), models.Collections.User) + } + + _, err = p.db.Query(ctx, query, nil) + + if err != nil { + return err + } + + return nil +} diff --git a/server/db/providers/arangodb/verification_requests.go b/server/db/providers/arangodb/verification_requests.go index a1dbfa2..8722bad 100644 --- a/server/db/providers/arangodb/verification_requests.go +++ b/server/db/providers/arangodb/verification_requests.go @@ -15,6 +15,7 @@ import ( func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) { if verificationRequest.ID == "" { verificationRequest.ID = uuid.New().String() + verificationRequest.Key = verificationRequest.ID } verificationRequest.CreatedAt = time.Now().Unix() diff --git a/server/db/providers/arangodb/webhook.go b/server/db/providers/arangodb/webhook.go index 302eb61..2fd62da 100644 --- a/server/db/providers/arangodb/webhook.go +++ b/server/db/providers/arangodb/webhook.go @@ -16,6 +16,7 @@ import ( func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) { if webhook.ID == "" { webhook.ID = uuid.New().String() + webhook.Key = webhook.ID } webhook.Key = webhook.ID diff --git a/server/db/providers/arangodb/webhook_log.go b/server/db/providers/arangodb/webhook_log.go index bc758c4..35565e1 100644 --- a/server/db/providers/arangodb/webhook_log.go +++ b/server/db/providers/arangodb/webhook_log.go @@ -16,6 +16,7 @@ import ( func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) { if webhookLog.ID == "" { webhookLog.ID = uuid.New().String() + webhookLog.Key = webhookLog.ID } webhookLog.Key = webhookLog.ID diff --git a/server/db/providers/cassandradb/otp.go b/server/db/providers/cassandradb/otp.go index 7ead206..bfe481d 100644 --- a/server/db/providers/cassandradb/otp.go +++ b/server/db/providers/cassandradb/otp.go @@ -16,21 +16,27 @@ func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models shouldCreate := false if otp == nil { shouldCreate = true - otp.ID = uuid.New().String() - otp.Key = otp.ID - otp.CreatedAt = time.Now().Unix() + otp = &models.OTP{ + ID: uuid.NewString(), + Otp: otpParam.Otp, + Email: otpParam.Email, + ExpiresAt: otpParam.ExpiresAt, + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + } } else { - otp = otpParam + otp.Otp = otpParam.Otp + otp.ExpiresAt = otpParam.ExpiresAt } otp.UpdatedAt = time.Now().Unix() query := "" - if shouldCreate { query = fmt.Sprintf(`INSERT INTO %s (id, email, otp, expires_at, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d, %d)`, KeySpace+"."+models.Collections.OTP, otp.ID, otp.Email, otp.Otp, otp.ExpiresAt, otp.CreatedAt, otp.UpdatedAt) } else { - query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE email = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.Email) + query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE id = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.ID) } + err := p.db.Query(query).Exec() if err != nil { return nil, err diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index 80a9cb6..329dad2 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -13,6 +13,7 @@ import ( "github.com/authorizerdev/authorizer/server/memorystore" "github.com/gocql/gocql" cansandraDriver "github.com/gocql/gocql" + log "github.com/sirupsen/logrus" ) type provider struct { @@ -99,6 +100,7 @@ func NewProvider() (*provider, error) { cassandraClient.Consistency = gocql.LocalQuorum cassandraClient.ConnectTimeout = 10 * time.Second cassandraClient.ProtoVersion = 4 + cassandraClient.Timeout = 30 * time.Minute // for large data session, err := cassandraClient.CreateSession() if err != nil { @@ -160,10 +162,11 @@ func NewProvider() (*provider, error) { 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) + 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 + log.Debug("Failed to alter table as column exists: ", err) + // return nil, err } // token is reserved keyword in cassandra, hence we need to use jwt_token diff --git a/server/db/providers/cassandradb/user.go b/server/db/providers/cassandradb/user.go index 9489cdd..4da7ec9 100644 --- a/server/db/providers/cassandradb/user.go +++ b/server/db/providers/cassandradb/user.go @@ -107,7 +107,7 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use } if value == nil { - updateFields += fmt.Sprintf("%s = null,", key) + updateFields += fmt.Sprintf("%s = null, ", key) continue } @@ -122,7 +122,6 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use updateFields = strings.TrimSuffix(updateFields, ",") query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID) - err = p.db.Query(query).Exec() if err != nil { return user, err @@ -173,14 +172,14 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) ( // there is no offset in cassandra // so we fetch till limit + offset // and return the results from offset to limit - query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset) + query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset) scanner := p.db.Query(query).Iter().Scanner() counter := int64(0) for scanner.Next() { if counter >= pagination.Offset { var user models.User - err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) + err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt) if err != nil { return nil, err } @@ -197,8 +196,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) ( // GetUserByEmail to get user information from database using email address func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) { var user models.User - query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) - err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) + query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) + err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt) if err != nil { return user, err } @@ -208,10 +207,95 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.Use // GetUserByID to get user information from database using user ID func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) { var user models.User - query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) - err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) + query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) + err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt) if err != nil { return user, err } return user, nil } + +// UpdateUsers to update multiple users, with parameters of user IDs slice +// If ids set to nil / empty all the users will be updated +func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { + // set updated_at time for all users + data["updated_at"] = time.Now().Unix() + + updateFields := "" + for key, value := range data { + if key == "_id" { + continue + } + + if key == "_key" { + continue + } + + if value == nil { + updateFields += fmt.Sprintf("%s = null,", key) + continue + } + + valueType := reflect.TypeOf(value) + if valueType.Name() == "string" { + updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string)) + } else { + updateFields += fmt.Sprintf("%s = %v, ", key, value) + } + } + updateFields = strings.Trim(updateFields, " ") + updateFields = strings.TrimSuffix(updateFields, ",") + + query := "" + if ids != nil && len(ids) > 0 { + idsString := "" + for _, id := range ids { + idsString += fmt.Sprintf("'%s', ", id) + } + idsString = strings.Trim(idsString, " ") + idsString = strings.TrimSuffix(idsString, ",") + query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idsString) + err := p.db.Query(query).Exec() + if err != nil { + return err + } + } else { + // get all ids + getUserIDsQuery := fmt.Sprintf(`SELECT id FROM %s`, KeySpace+"."+models.Collections.User) + scanner := p.db.Query(getUserIDsQuery).Iter().Scanner() + // only 100 ids are allowed in 1 query + // hence we need create multiple update queries + idsString := "" + idsStringArray := []string{idsString} + counter := 1 + for scanner.Next() { + var id string + err := scanner.Scan(&id) + if err == nil { + idsString += fmt.Sprintf("'%s', ", id) + } + counter++ + if counter > 100 { + idsStringArray = append(idsStringArray, idsString) + counter = 1 + idsString = "" + } else { + // update the last index of array when count is less than 100 + idsStringArray[len(idsStringArray)-1] = idsString + } + } + + for _, idStr := range idsStringArray { + idStr = strings.Trim(idStr, " ") + idStr = strings.TrimSuffix(idStr, ",") + query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idStr) + err := p.db.Query(query).Exec() + if err != nil { + return err + } + } + + } + + return nil +} diff --git a/server/db/providers/mongodb/otp.go b/server/db/providers/mongodb/otp.go index bbf0426..d6ff2df 100644 --- a/server/db/providers/mongodb/otp.go +++ b/server/db/providers/mongodb/otp.go @@ -11,19 +11,33 @@ import ( ) // UpsertOTP to add or update otp -func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) { - if otp.ID == "" { - otp.ID = uuid.New().String() - } - - otp.Key = otp.ID - if otp.CreatedAt <= 0 { - otp.CreatedAt = time.Now().Unix() +func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) { + otp, _ := p.GetOTPByEmail(ctx, otpParam.Email) + shouldCreate := false + if otp == nil { + id := uuid.NewString() + otp = &models.OTP{ + ID: id, + Key: id, + Otp: otpParam.Otp, + Email: otpParam.Email, + ExpiresAt: otpParam.ExpiresAt, + CreatedAt: time.Now().Unix(), + } + shouldCreate = true + } else { + otp.Otp = otpParam.Otp + otp.ExpiresAt = otpParam.ExpiresAt } otp.UpdatedAt = time.Now().Unix() - otpCollection := p.db.Collection(models.Collections.OTP, options.Collection()) - _, err := otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions().SetUpsert(true)) + + var err error + if shouldCreate { + _, err = otpCollection.InsertOne(ctx, otp) + } else { + _, err = otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions()) + } if err != nil { return nil, err } diff --git a/server/db/providers/mongodb/user.go b/server/db/providers/mongodb/user.go index 7518fd9..6e90a40 100644 --- a/server/db/providers/mongodb/user.go +++ b/server/db/providers/mongodb/user.go @@ -9,7 +9,9 @@ import ( "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/google/uuid" + log "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -129,3 +131,27 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err return user, nil } + +// UpdateUsers to update multiple users, with parameters of user IDs slice +// If ids set to nil / empty all the users will be updated +func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { + // set updated_at time for all users + data["updated_at"] = time.Now().Unix() + + userCollection := p.db.Collection(models.Collections.User, options.Collection()) + + var res *mongo.UpdateResult + var err error + if ids != nil && len(ids) > 0 { + res, err = userCollection.UpdateMany(ctx, bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": data}) + } else { + res, err = userCollection.UpdateMany(ctx, bson.M{}, bson.M{"$set": data}) + } + + if err != nil { + return err + } else { + log.Info("Updated users: ", res.ModifiedCount) + } + return nil +} diff --git a/server/db/providers/provider_template/user.go b/server/db/providers/provider_template/user.go index 00b2db8..2b167db 100644 --- a/server/db/providers/provider_template/user.go +++ b/server/db/providers/provider_template/user.go @@ -60,3 +60,12 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err return user, nil } + +// UpdateUsers to update multiple users, with parameters of user IDs slice +// If ids set to nil / empty all the users will be updated +func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { + // set updated_at time for all users + data["updated_at"] = time.Now().Unix() + + return nil +} diff --git a/server/db/providers/providers.go b/server/db/providers/providers.go index da72190..a578396 100644 --- a/server/db/providers/providers.go +++ b/server/db/providers/providers.go @@ -20,6 +20,9 @@ type Provider interface { GetUserByEmail(ctx context.Context, email string) (models.User, error) // GetUserByID to get user information from database using user ID GetUserByID(ctx context.Context, id string) (models.User, error) + // UpdateUsers to update multiple users, with parameters of user IDs slice + // If ids set to nil / empty all the users will be updated + UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error // AddVerification to save verification request in database AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) diff --git a/server/db/providers/sql/provider.go b/server/db/providers/sql/provider.go index 394bff3..712f3d1 100644 --- a/server/db/providers/sql/provider.go +++ b/server/db/providers/sql/provider.go @@ -40,6 +40,7 @@ func NewProvider() (*provider, error) { NamingStrategy: schema.NamingStrategy{ TablePrefix: models.Prefix, }, + AllowGlobalUpdate: true, } dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType diff --git a/server/db/providers/sql/user.go b/server/db/providers/sql/user.go index c5953ce..c191935 100644 --- a/server/db/providers/sql/user.go +++ b/server/db/providers/sql/user.go @@ -9,6 +9,7 @@ import ( "github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/google/uuid" + "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -121,3 +122,22 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err return user, nil } + +// UpdateUsers to update multiple users, with parameters of user IDs slice +// If ids set to nil / empty all the users will be updated +func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { + // set updated_at time for all users + data["updated_at"] = time.Now().Unix() + + var res *gorm.DB + if ids != nil && len(ids) > 0 { + res = p.db.Model(&models.User{}).Where("id in ?", ids).Updates(data) + } else { + res = p.db.Model(&models.User{}).Updates(data) + } + + if res.Error != nil { + return res.Error + } + return nil +} diff --git a/server/env/env.go b/server/env/env.go index c492b13..e8fe863 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -84,6 +84,7 @@ func InitAllEnv() error { osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp) osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv) osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword) + osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication) // os slice vars osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins) @@ -490,6 +491,19 @@ func InitAllEnv() error { } } + if _, ok := envData[constants.EnvKeyEnforceMultiFactorAuthentication]; !ok { + envData[constants.EnvKeyEnforceMultiFactorAuthentication] = osEnforceMultiFactorAuthentication == "true" + } + if osEnforceMultiFactorAuthentication != "" { + boolValue, err := strconv.ParseBool(osEnforceMultiFactorAuthentication) + if err != nil { + return err + } + if boolValue != envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) { + envData[constants.EnvKeyEnforceMultiFactorAuthentication] = boolValue + } + } + // no need to add nil check as its already done above if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" { envData[constants.EnvKeyDisableEmailVerification] = true @@ -501,6 +515,10 @@ func InitAllEnv() error { envData[constants.EnvKeyIsEmailServiceEnabled] = true } + if envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) { + return errors.New("to enable multi factor authentication, please enable email service") + } + if envData[constants.EnvKeyDisableEmailVerification].(bool) { envData[constants.EnvKeyDisableMagicLinkLogin] = true } diff --git a/server/env/persist_env.go b/server/env/persist_env.go index d783b93..e4ec275 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -201,7 +201,7 @@ func PersistEnv() error { envValue := strings.TrimSpace(os.Getenv(key)) if envValue != "" { switch key { - case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled: + case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication: if envValueBool, err := strconv.ParseBool(envValue); err == nil { if value.(bool) != envValueBool { storeData[key] = envValueBool diff --git a/server/go.mod b/server/go.mod index 13e3e52..98dcc94 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,11 +5,12 @@ go 1.16 require ( github.com/99designs/gqlgen v0.14.0 github.com/arangodb/go-driver v1.2.1 + github.com/coreos/etcd v3.3.27+incompatible github.com/coreos/go-oidc/v3 v3.1.0 github.com/gin-gonic/gin v1.7.2 github.com/go-playground/validator/v10 v10.8.0 // indirect github.com/go-redis/redis/v8 v8.11.0 - github.com/gocql/gocql v1.0.0 + github.com/gocql/gocql v1.2.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.3.0 diff --git a/server/go.sum b/server/go.sum index 9392d6f..7a1bbcb 100644 --- a/server/go.sum +++ b/server/go.sum @@ -62,6 +62,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/etcd v3.3.27+incompatible h1:QIudLb9KeBsE5zyYxd1mjzRSkzLg9Wf9QlRwFgd6oTA= +github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.3/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= @@ -112,6 +114,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE= +github.com/gocql/gocql v1.2.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 44c181f..0d247f3 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -67,54 +67,55 @@ type ComplexityRoot struct { } Env struct { - AccessTokenExpiryTime func(childComplexity int) int - AdminSecret func(childComplexity int) int - AllowedOrigins func(childComplexity int) int - AppURL func(childComplexity int) int - AppleClientID func(childComplexity int) int - AppleClientSecret func(childComplexity int) int - ClientID func(childComplexity int) int - ClientSecret func(childComplexity int) int - CustomAccessTokenScript func(childComplexity int) int - DatabaseHost func(childComplexity int) int - DatabaseName func(childComplexity int) int - DatabasePassword func(childComplexity int) int - DatabasePort func(childComplexity int) int - DatabaseType func(childComplexity int) int - DatabaseURL func(childComplexity int) int - DatabaseUsername func(childComplexity int) int - DefaultRoles func(childComplexity int) int - DisableBasicAuthentication func(childComplexity int) int - DisableEmailVerification func(childComplexity int) int - DisableLoginPage func(childComplexity int) int - DisableMagicLinkLogin func(childComplexity int) int - DisableRedisForEnv func(childComplexity int) int - DisableSignUp func(childComplexity int) int - DisableStrongPassword func(childComplexity int) int - FacebookClientID func(childComplexity int) int - FacebookClientSecret func(childComplexity int) int - GithubClientID func(childComplexity int) int - GithubClientSecret func(childComplexity int) int - GoogleClientID func(childComplexity int) int - GoogleClientSecret func(childComplexity int) int - JwtPrivateKey func(childComplexity int) int - JwtPublicKey func(childComplexity int) int - JwtRoleClaim func(childComplexity int) int - JwtSecret func(childComplexity int) int - JwtType func(childComplexity int) int - LinkedinClientID func(childComplexity int) int - LinkedinClientSecret func(childComplexity int) int - OrganizationLogo func(childComplexity int) int - OrganizationName func(childComplexity int) int - ProtectedRoles func(childComplexity int) int - RedisURL func(childComplexity int) int - ResetPasswordURL func(childComplexity int) int - Roles func(childComplexity int) int - SMTPHost func(childComplexity int) int - SMTPPassword func(childComplexity int) int - SMTPPort func(childComplexity int) int - SMTPUsername func(childComplexity int) int - SenderEmail func(childComplexity int) int + AccessTokenExpiryTime func(childComplexity int) int + AdminSecret func(childComplexity int) int + AllowedOrigins func(childComplexity int) int + AppURL func(childComplexity int) int + AppleClientID func(childComplexity int) int + AppleClientSecret func(childComplexity int) int + ClientID func(childComplexity int) int + ClientSecret func(childComplexity int) int + CustomAccessTokenScript func(childComplexity int) int + DatabaseHost func(childComplexity int) int + DatabaseName func(childComplexity int) int + DatabasePassword func(childComplexity int) int + DatabasePort func(childComplexity int) int + DatabaseType func(childComplexity int) int + DatabaseURL func(childComplexity int) int + DatabaseUsername func(childComplexity int) int + DefaultRoles func(childComplexity int) int + DisableBasicAuthentication func(childComplexity int) int + DisableEmailVerification func(childComplexity int) int + DisableLoginPage func(childComplexity int) int + DisableMagicLinkLogin func(childComplexity int) int + DisableRedisForEnv func(childComplexity int) int + DisableSignUp func(childComplexity int) int + DisableStrongPassword func(childComplexity int) int + EnforceMultiFactorAuthentication func(childComplexity int) int + FacebookClientID func(childComplexity int) int + FacebookClientSecret func(childComplexity int) int + GithubClientID func(childComplexity int) int + GithubClientSecret func(childComplexity int) int + GoogleClientID func(childComplexity int) int + GoogleClientSecret func(childComplexity int) int + JwtPrivateKey func(childComplexity int) int + JwtPublicKey func(childComplexity int) int + JwtRoleClaim func(childComplexity int) int + JwtSecret func(childComplexity int) int + JwtType func(childComplexity int) int + LinkedinClientID func(childComplexity int) int + LinkedinClientSecret func(childComplexity int) int + OrganizationLogo func(childComplexity int) int + OrganizationName func(childComplexity int) int + ProtectedRoles func(childComplexity int) int + RedisURL func(childComplexity int) int + ResetPasswordURL func(childComplexity int) int + Roles func(childComplexity int) int + SMTPHost func(childComplexity int) int + SMTPPassword func(childComplexity int) int + SMTPPort func(childComplexity int) int + SMTPUsername func(childComplexity int) int + SenderEmail func(childComplexity int) int } Error struct { @@ -612,6 +613,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.DisableStrongPassword(childComplexity), true + case "Env.ENFORCE_MULTI_FACTOR_AUTHENTICATION": + if e.complexity.Env.EnforceMultiFactorAuthentication == nil { + break + } + + return e.complexity.Env.EnforceMultiFactorAuthentication(childComplexity), true + case "Env.FACEBOOK_CLIENT_ID": if e.complexity.Env.FacebookClientID == nil { break @@ -1957,6 +1965,7 @@ type Env { DISABLE_SIGN_UP: Boolean! DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_STRONG_PASSWORD: Boolean! + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -2057,6 +2066,7 @@ input UpdateEnvInput { DISABLE_SIGN_UP: Boolean DISABLE_REDIS_FOR_ENV: Boolean DISABLE_STRONG_PASSWORD: Boolean + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -4415,6 +4425,41 @@ func (ec *executionContext) _Env_DISABLE_STRONG_PASSWORD(ctx context.Context, fi return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + 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.EnforceMultiFactorAuthentication, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ROLES(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -11334,6 +11379,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "ENFORCE_MULTI_FACTOR_AUTHENTICATION": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ENFORCE_MULTI_FACTOR_AUTHENTICATION")) + it.EnforceMultiFactorAuthentication, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } case "ROLES": var err error @@ -12099,6 +12152,11 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "ENFORCE_MULTI_FACTOR_AUTHENTICATION": + out.Values[i] = ec._Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "ROLES": out.Values[i] = ec._Env_ROLES(ctx, field, obj) case "PROTECTED_ROLES": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index b193c0d..3c5f883 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -54,54 +54,55 @@ type EmailTemplates struct { } type Env struct { - AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` - AdminSecret *string `json:"ADMIN_SECRET"` - DatabaseName *string `json:"DATABASE_NAME"` - DatabaseURL *string `json:"DATABASE_URL"` - DatabaseType *string `json:"DATABASE_TYPE"` - DatabaseUsername *string `json:"DATABASE_USERNAME"` - DatabasePassword *string `json:"DATABASE_PASSWORD"` - DatabaseHost *string `json:"DATABASE_HOST"` - DatabasePort *string `json:"DATABASE_PORT"` - ClientID string `json:"CLIENT_ID"` - ClientSecret string `json:"CLIENT_SECRET"` - CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` - SMTPHost *string `json:"SMTP_HOST"` - SMTPPort *string `json:"SMTP_PORT"` - SMTPUsername *string `json:"SMTP_USERNAME"` - SMTPPassword *string `json:"SMTP_PASSWORD"` - SenderEmail *string `json:"SENDER_EMAIL"` - JwtType *string `json:"JWT_TYPE"` - JwtSecret *string `json:"JWT_SECRET"` - JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` - JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` - AllowedOrigins []string `json:"ALLOWED_ORIGINS"` - AppURL *string `json:"APP_URL"` - RedisURL *string `json:"REDIS_URL"` - ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` - DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"` - DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"` - DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"` - DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"` - DisableSignUp bool `json:"DISABLE_SIGN_UP"` - DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` - DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"` - Roles []string `json:"ROLES"` - ProtectedRoles []string `json:"PROTECTED_ROLES"` - DefaultRoles []string `json:"DEFAULT_ROLES"` - JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` - GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` - GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` - GithubClientID *string `json:"GITHUB_CLIENT_ID"` - GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` - FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` - FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` - LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` - LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` - AppleClientID *string `json:"APPLE_CLIENT_ID"` - AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` - OrganizationName *string `json:"ORGANIZATION_NAME"` - OrganizationLogo *string `json:"ORGANIZATION_LOGO"` + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` + AdminSecret *string `json:"ADMIN_SECRET"` + DatabaseName *string `json:"DATABASE_NAME"` + DatabaseURL *string `json:"DATABASE_URL"` + DatabaseType *string `json:"DATABASE_TYPE"` + DatabaseUsername *string `json:"DATABASE_USERNAME"` + DatabasePassword *string `json:"DATABASE_PASSWORD"` + DatabaseHost *string `json:"DATABASE_HOST"` + DatabasePort *string `json:"DATABASE_PORT"` + ClientID string `json:"CLIENT_ID"` + ClientSecret string `json:"CLIENT_SECRET"` + CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` + SMTPHost *string `json:"SMTP_HOST"` + SMTPPort *string `json:"SMTP_PORT"` + SMTPUsername *string `json:"SMTP_USERNAME"` + SMTPPassword *string `json:"SMTP_PASSWORD"` + SenderEmail *string `json:"SENDER_EMAIL"` + JwtType *string `json:"JWT_TYPE"` + JwtSecret *string `json:"JWT_SECRET"` + JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` + JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` + AllowedOrigins []string `json:"ALLOWED_ORIGINS"` + AppURL *string `json:"APP_URL"` + RedisURL *string `json:"REDIS_URL"` + ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` + DisableEmailVerification bool `json:"DISABLE_EMAIL_VERIFICATION"` + DisableBasicAuthentication bool `json:"DISABLE_BASIC_AUTHENTICATION"` + DisableMagicLinkLogin bool `json:"DISABLE_MAGIC_LINK_LOGIN"` + DisableLoginPage bool `json:"DISABLE_LOGIN_PAGE"` + DisableSignUp bool `json:"DISABLE_SIGN_UP"` + DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` + DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"` + EnforceMultiFactorAuthentication bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"` + Roles []string `json:"ROLES"` + ProtectedRoles []string `json:"PROTECTED_ROLES"` + DefaultRoles []string `json:"DEFAULT_ROLES"` + JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` + GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` + GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` + GithubClientID *string `json:"GITHUB_CLIENT_ID"` + GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` + FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` + FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` + LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` + LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` + AppleClientID *string `json:"APPLE_CLIENT_ID"` + AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` + OrganizationName *string `json:"ORGANIZATION_NAME"` + OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } type Error struct { @@ -249,45 +250,46 @@ type UpdateEmailTemplateRequest struct { } type UpdateEnvInput struct { - AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` - AdminSecret *string `json:"ADMIN_SECRET"` - CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` - OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` - SMTPHost *string `json:"SMTP_HOST"` - SMTPPort *string `json:"SMTP_PORT"` - SMTPUsername *string `json:"SMTP_USERNAME"` - SMTPPassword *string `json:"SMTP_PASSWORD"` - SenderEmail *string `json:"SENDER_EMAIL"` - JwtType *string `json:"JWT_TYPE"` - JwtSecret *string `json:"JWT_SECRET"` - JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` - JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` - AllowedOrigins []string `json:"ALLOWED_ORIGINS"` - AppURL *string `json:"APP_URL"` - ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` - DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"` - DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"` - DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"` - DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"` - DisableSignUp *bool `json:"DISABLE_SIGN_UP"` - DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"` - DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"` - Roles []string `json:"ROLES"` - ProtectedRoles []string `json:"PROTECTED_ROLES"` - DefaultRoles []string `json:"DEFAULT_ROLES"` - JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` - GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` - GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` - GithubClientID *string `json:"GITHUB_CLIENT_ID"` - GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` - FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` - FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` - LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` - LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` - AppleClientID *string `json:"APPLE_CLIENT_ID"` - AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` - OrganizationName *string `json:"ORGANIZATION_NAME"` - OrganizationLogo *string `json:"ORGANIZATION_LOGO"` + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` + AdminSecret *string `json:"ADMIN_SECRET"` + CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` + OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` + SMTPHost *string `json:"SMTP_HOST"` + SMTPPort *string `json:"SMTP_PORT"` + SMTPUsername *string `json:"SMTP_USERNAME"` + SMTPPassword *string `json:"SMTP_PASSWORD"` + SenderEmail *string `json:"SENDER_EMAIL"` + JwtType *string `json:"JWT_TYPE"` + JwtSecret *string `json:"JWT_SECRET"` + JwtPrivateKey *string `json:"JWT_PRIVATE_KEY"` + JwtPublicKey *string `json:"JWT_PUBLIC_KEY"` + AllowedOrigins []string `json:"ALLOWED_ORIGINS"` + AppURL *string `json:"APP_URL"` + ResetPasswordURL *string `json:"RESET_PASSWORD_URL"` + DisableEmailVerification *bool `json:"DISABLE_EMAIL_VERIFICATION"` + DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"` + DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"` + DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"` + DisableSignUp *bool `json:"DISABLE_SIGN_UP"` + DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"` + DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"` + EnforceMultiFactorAuthentication *bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"` + Roles []string `json:"ROLES"` + ProtectedRoles []string `json:"PROTECTED_ROLES"` + DefaultRoles []string `json:"DEFAULT_ROLES"` + JwtRoleClaim *string `json:"JWT_ROLE_CLAIM"` + GoogleClientID *string `json:"GOOGLE_CLIENT_ID"` + GoogleClientSecret *string `json:"GOOGLE_CLIENT_SECRET"` + GithubClientID *string `json:"GITHUB_CLIENT_ID"` + GithubClientSecret *string `json:"GITHUB_CLIENT_SECRET"` + FacebookClientID *string `json:"FACEBOOK_CLIENT_ID"` + FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` + LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` + LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` + AppleClientID *string `json:"APPLE_CLIENT_ID"` + AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` + OrganizationName *string `json:"ORGANIZATION_NAME"` + OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } type UpdateProfileInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index e3d6908..ddab91c 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -124,6 +124,7 @@ type Env { DISABLE_SIGN_UP: Boolean! DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_STRONG_PASSWORD: Boolean! + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -224,6 +225,7 @@ input UpdateEnvInput { DISABLE_SIGN_UP: Boolean DISABLE_REDIS_FOR_ENV: Boolean DISABLE_STRONG_PASSWORD: Boolean + ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index 9cbbbb4..dc7a195 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -25,13 +25,14 @@ func InitMemStore() error { constants.EnvKeyOrganizationLogo: "https://www.authorizer.dev/images/logo.png", // boolean envs - constants.EnvKeyDisableBasicAuthentication: false, - constants.EnvKeyDisableMagicLinkLogin: false, - constants.EnvKeyDisableEmailVerification: false, - constants.EnvKeyDisableLoginPage: false, - constants.EnvKeyDisableSignUp: false, - constants.EnvKeyDisableStrongPassword: false, - constants.EnvKeyIsEmailServiceEnabled: false, + constants.EnvKeyDisableBasicAuthentication: false, + constants.EnvKeyDisableMagicLinkLogin: false, + constants.EnvKeyDisableEmailVerification: false, + constants.EnvKeyDisableLoginPage: false, + constants.EnvKeyDisableSignUp: false, + constants.EnvKeyDisableStrongPassword: false, + constants.EnvKeyIsEmailServiceEnabled: false, + constants.EnvKeyEnforceMultiFactorAuthentication: false, } requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() diff --git a/server/memorystore/providers/inmemory/stores/session_store.go b/server/memorystore/providers/inmemory/stores/session_store.go index ad617af..d035312 100644 --- a/server/memorystore/providers/inmemory/stores/session_store.go +++ b/server/memorystore/providers/inmemory/stores/session_store.go @@ -39,6 +39,7 @@ func (s *SessionStore) Set(key string, subKey, value string) { func (s *SessionStore) RemoveAll(key string) { s.mutex.Lock() defer s.mutex.Unlock() + delete(s.store, key) } @@ -53,6 +54,9 @@ func (s *SessionStore) Remove(key, subKey string) { // Get all the values for given key func (s *SessionStore) GetAll(key string) map[string]string { + s.mutex.Lock() + defer s.mutex.Unlock() + if _, ok := s.store[key]; !ok { s.store[key] = make(map[string]string) } @@ -63,6 +67,7 @@ func (s *SessionStore) GetAll(key string) map[string]string { func (s *SessionStore) RemoveByNamespace(namespace string) error { s.mutex.Lock() defer s.mutex.Unlock() + for key := range s.store { if strings.Contains(key, namespace+":") { delete(s.store, key) diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index d6ee1df..4fb1206 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) { return nil, err } for key, value := range data { - if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled { + if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication { boolValue, err := strconv.ParseBool(value) if err != nil { return res, err diff --git a/server/resolvers/env.go b/server/resolvers/env.go index 3abda07..e97fac4 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -170,6 +170,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool) res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool) res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool) + res.EnforceMultiFactorAuthentication = store[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) return res, nil } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 7d1f28e..6325a6a 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -2,7 +2,6 @@ package resolvers import ( "context" - "errors" "fmt" "strings" "time" @@ -100,12 +99,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes scope = params.Scope } - if refs.BoolValue(user.IsMultiFactorAuthEnabled) { - isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) - if err != nil || !isEnvServiceEnabled { - log.Debug("Email service not enabled:") - return nil, errors.New("email service not enabled") - } + isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEmailServiceEnabled { + log.Debug("Email service not enabled: ", err) + } + + // If email service is not enabled continue the process in any way + if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled { otp := utils.GenerateOTP() otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go index 60367c1..edd7445 100644 --- a/server/resolvers/resend_otp.go +++ b/server/resolvers/resend_otp.go @@ -9,10 +9,12 @@ import ( log "github.com/sirupsen/logrus" + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/email" "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/utils" ) @@ -44,6 +46,12 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod return nil, fmt.Errorf(`multi factor authentication not enabled`) } + isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEmailServiceEnabled { + log.Debug("Email service not enabled: ", err) + return nil, errors.New("email service not enabled") + } + // get otp by email otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email) if err != nil { diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index 30abe9e..deecc20 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -270,8 +270,6 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model } } - go clearSessionIfRequired(currentData, updatedData) - // Update local store memorystore.Provider.UpdateEnvStore(updatedData) jwk, err := crypto.GenerateJWKBasedOnEnv() @@ -325,6 +323,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model return res, err } + go clearSessionIfRequired(currentData, updatedData) + res = &model.Response{ Message: "configurations updated successfully", } diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index 0a47376..ebc4718 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -96,7 +96,6 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) } if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) { - user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled if refs.BoolValue(params.IsMultiFactorAuthEnabled) { isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) if err != nil || !isEnvServiceEnabled { @@ -104,6 +103,8 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) return nil, errors.New("email service not enabled, so cannot enable multi factor authentication") } } + + user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled } isPasswordChanging := false diff --git a/server/test/resend_otp_test.go b/server/test/resend_otp_test.go index 3202d9e..2ba256c 100644 --- a/server/test/resend_otp_test.go +++ b/server/test/resend_otp_test.go @@ -14,9 +14,9 @@ import ( func resendOTPTest(t *testing.T, s TestSetup) { t.Helper() - t.Run(`should verify otp`, func(t *testing.T) { + t.Run(`should resend otp`, func(t *testing.T) { req, ctx := createContext(s) - email := "verify_otp." + s.TestInfo.Email + email := "resend_otp." + s.TestInfo.Email res, err := resolvers.SignupResolver(ctx, model.SignUpInput{ Email: email, Password: s.TestInfo.Password, diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index f10576e..17373fc 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -33,7 +33,7 @@ func TestResolvers(t *testing.T) { if utils.StringSliceContains(testDBs, constants.DbTypeSqlite) && len(testDBs) == 1 { // do nothing } else { - t.Log("waiting for docker containers to spun up") + t.Log("waiting for docker containers to start...") // wait for docker containers to spun up time.Sleep(30 * time.Second) } @@ -116,6 +116,8 @@ func TestResolvers(t *testing.T) { validateJwtTokenTest(t, s) verifyOTPTest(t, s) resendOTPTest(t, s) + + updateAllUsersTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done deleteWebhookTest(t, s) // delete webhooks (admin resolver) }) diff --git a/server/test/update_all_users_tests.go b/server/test/update_all_users_tests.go new file mode 100644 index 0000000..6473908 --- /dev/null +++ b/server/test/update_all_users_tests.go @@ -0,0 +1,67 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/utils" + "github.com/stretchr/testify/assert" +) + +func updateAllUsersTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run("Should update all users", func(t *testing.T) { + _, ctx := createContext(s) + + users := []models.User{} + for i := 0; i < 10; i++ { + user := models.User{ + Email: fmt.Sprintf("update_all_user_%d_%s", i, s.TestInfo.Email), + SignupMethods: constants.AuthRecipeMethodBasicAuth, + Roles: "user", + } + users = append(users, user) + u, err := db.Provider.AddUser(ctx, user) + assert.NoError(t, err) + assert.NotNil(t, u) + } + + err := db.Provider.UpdateUsers(ctx, map[string]interface{}{ + "is_multi_factor_auth_enabled": true, + }, nil) + assert.NoError(t, err) + + listUsers, err := db.Provider.ListUsers(ctx, model.Pagination{ + Limit: 20, + Offset: 0, + }) + assert.NoError(t, err) + for _, u := range listUsers.Users { + assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled)) + } + + // // update few users + updateIds := []string{listUsers.Users[0].ID, listUsers.Users[1].ID} + err = db.Provider.UpdateUsers(ctx, map[string]interface{}{ + "is_multi_factor_auth_enabled": false, + }, updateIds) + assert.NoError(t, err) + + listUsers, err = db.Provider.ListUsers(ctx, model.Pagination{ + Limit: 20, + Offset: 0, + }) + for _, u := range listUsers.Users { + if utils.StringSliceContains(updateIds, u.ID) { + assert.False(t, refs.BoolValue(u.IsMultiFactorAuthEnabled)) + } else { + assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled)) + } + } + }) +} diff --git a/server/test/verify_otp_test.go b/server/test/verify_otp_test.go index afb7e2e..9e074cd 100644 --- a/server/test/verify_otp_test.go +++ b/server/test/verify_otp_test.go @@ -44,9 +44,11 @@ func verifyOTPTest(t *testing.T, s TestSetup) { // Using access token update profile s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken)) ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) - _, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ + updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{ IsMultiFactorAuthEnabled: refs.NewBoolRef(true), }) + assert.NoError(t, err) + assert.NotEmpty(t, updateProfileRes.Message) // Login should not return error but access token should be empty as otp should have been sent loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{ diff --git a/server/utils/webhook.go b/server/utils/webhook.go index 041a542..acacfbf 100644 --- a/server/utils/webhook.go +++ b/server/utils/webhook.go @@ -11,6 +11,7 @@ import ( "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/refs" log "github.com/sirupsen/logrus" ) @@ -52,6 +53,22 @@ func RegisterEvent(ctx context.Context, eventName string, authRecipe string, use return err } + // dont trigger webhook call in case of test + envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv) + if err != nil { + return err + } + if envKey == constants.TestEnv { + db.Provider.AddWebhookLog(ctx, models.WebhookLog{ + HttpStatus: 200, + Request: string(requestBody), + Response: string(`{"message": "test"}`), + WebhookID: webhook.ID, + }) + + return nil + } + requestBytesBuffer := bytes.NewBuffer(requestBody) req, err := http.NewRequest("POST", refs.StringValue(webhook.Endpoint), requestBytesBuffer) if err != nil { From a8900133170208978da645ac87ec914402a931ea Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 2 Aug 2022 18:26:05 +0530 Subject: [PATCH 19/20] Update generate_otp.go --- server/utils/generate_otp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils/generate_otp.go b/server/utils/generate_otp.go index eb26e97..61416b1 100644 --- a/server/utils/generate_otp.go +++ b/server/utils/generate_otp.go @@ -5,6 +5,7 @@ import ( "time" ) +// GenerateOTP to generate random 6 digit otp func GenerateOTP() string { code := "" codeLength := 6 From 465a92de224ee715dafdb682ea72ac7e4b4526af Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 3 Aug 2022 23:20:23 +0530 Subject: [PATCH 20/20] feat: add managing mfa --- server/constants/env.go | 4 + server/env/env.go | 18 ++++ server/env/persist_env.go | 2 +- server/go.mod | 1 - server/go.sum | 4 - server/graph/generated/generated.go | 107 ++++++++++++++++++++ server/graph/model/models_gen.go | 3 + server/graph/schema.graphqls | 3 + server/memorystore/memory_store.go | 1 + server/memorystore/providers/redis/store.go | 2 +- server/resolvers/env.go | 1 + server/resolvers/invite_members.go | 10 ++ server/resolvers/login.go | 7 +- server/resolvers/meta.go | 7 ++ server/resolvers/resend_otp.go | 6 ++ server/resolvers/reset_password.go | 11 ++ server/resolvers/signup.go | 11 ++ server/resolvers/update_env.go | 7 ++ server/resolvers/update_profile.go | 11 ++ 19 files changed, 208 insertions(+), 8 deletions(-) diff --git a/server/constants/env.go b/server/constants/env.go index 2b26688..bd3afaf 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -120,7 +120,11 @@ const ( // EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD" // EnvKeyEnforceMultiFactorAuthentication is key for env variable ENFORCE_MULTI_FACTOR_AUTHENTICATION + // If enforced and changed later on, existing user will have MFA but new user will not have MFA EnvKeyEnforceMultiFactorAuthentication = "ENFORCE_MULTI_FACTOR_AUTHENTICATION" + // EnvKeyDisableMultiFactorAuthentication is key for env variable DISABLE_MULTI_FACTOR_AUTHENTICATION + // this variable is used to completely disable multi factor authentication. It will have no effect on profile preference + EnvKeyDisableMultiFactorAuthentication = "DISABLE_MULTI_FACTOR_AUTHENTICATION" // Slice variables // EnvKeyRoles key for env variable ROLES diff --git a/server/env/env.go b/server/env/env.go index e8fe863..f8df401 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -85,6 +85,7 @@ func InitAllEnv() error { osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv) osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword) osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication) + osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication) // os slice vars osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins) @@ -504,6 +505,19 @@ func InitAllEnv() error { } } + if _, ok := envData[constants.EnvKeyDisableMultiFactorAuthentication]; !ok { + envData[constants.EnvKeyDisableMultiFactorAuthentication] = osDisableMultiFactorAuthentication == "true" + } + if osDisableMultiFactorAuthentication != "" { + boolValue, err := strconv.ParseBool(osDisableMultiFactorAuthentication) + if err != nil { + return err + } + if boolValue != envData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) { + envData[constants.EnvKeyDisableMultiFactorAuthentication] = boolValue + } + } + // no need to add nil check as its already done above if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" { envData[constants.EnvKeyDisableEmailVerification] = true @@ -519,6 +533,10 @@ func InitAllEnv() error { return errors.New("to enable multi factor authentication, please enable email service") } + if !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) { + envData[constants.EnvKeyDisableMultiFactorAuthentication] = true + } + if envData[constants.EnvKeyDisableEmailVerification].(bool) { envData[constants.EnvKeyDisableMagicLinkLogin] = true } diff --git a/server/env/persist_env.go b/server/env/persist_env.go index e4ec275..10eb778 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -201,7 +201,7 @@ func PersistEnv() error { envValue := strings.TrimSpace(os.Getenv(key)) if envValue != "" { switch key { - case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication: + case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication: if envValueBool, err := strconv.ParseBool(envValue); err == nil { if value.(bool) != envValueBool { storeData[key] = envValueBool diff --git a/server/go.mod b/server/go.mod index 98dcc94..8b5cea4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/99designs/gqlgen v0.14.0 github.com/arangodb/go-driver v1.2.1 - github.com/coreos/etcd v3.3.27+incompatible github.com/coreos/go-oidc/v3 v3.1.0 github.com/gin-gonic/gin v1.7.2 github.com/go-playground/validator/v10 v10.8.0 // indirect diff --git a/server/go.sum b/server/go.sum index 7a1bbcb..c51da71 100644 --- a/server/go.sum +++ b/server/go.sum @@ -62,8 +62,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/etcd v3.3.27+incompatible h1:QIudLb9KeBsE5zyYxd1mjzRSkzLg9Wf9QlRwFgd6oTA= -github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.3/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= @@ -112,8 +110,6 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= -github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE= github.com/gocql/gocql v1.2.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 0d247f3..ab728ca 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -88,6 +88,7 @@ type ComplexityRoot struct { DisableEmailVerification func(childComplexity int) int DisableLoginPage func(childComplexity int) int DisableMagicLinkLogin func(childComplexity int) int + DisableMultiFactorAuthentication func(childComplexity int) int DisableRedisForEnv func(childComplexity int) int DisableSignUp func(childComplexity int) int DisableStrongPassword func(childComplexity int) int @@ -139,6 +140,7 @@ type ComplexityRoot struct { IsGoogleLoginEnabled func(childComplexity int) int IsLinkedinLoginEnabled func(childComplexity int) int IsMagicLinkLoginEnabled func(childComplexity int) int + IsMultiFactorAuthEnabled func(childComplexity int) int IsSignUpEnabled func(childComplexity int) int IsStrongPasswordEnabled func(childComplexity int) int Version func(childComplexity int) int @@ -592,6 +594,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true + case "Env.DISABLE_MULTI_FACTOR_AUTHENTICATION": + if e.complexity.Env.DisableMultiFactorAuthentication == nil { + break + } + + return e.complexity.Env.DisableMultiFactorAuthentication(childComplexity), true + case "Env.DISABLE_REDIS_FOR_ENV": if e.complexity.Env.DisableRedisForEnv == nil { break @@ -886,6 +895,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsMagicLinkLoginEnabled(childComplexity), true + case "Meta.is_multi_factor_auth_enabled": + if e.complexity.Meta.IsMultiFactorAuthEnabled == nil { + break + } + + return e.complexity.Meta.IsMultiFactorAuthEnabled(childComplexity), true + case "Meta.is_sign_up_enabled": if e.complexity.Meta.IsSignUpEnabled == nil { break @@ -1866,6 +1882,7 @@ type Meta { is_magic_link_login_enabled: Boolean! is_sign_up_enabled: Boolean! is_strong_password_enabled: Boolean! + is_multi_factor_auth_enabled: Boolean! } type User { @@ -1965,6 +1982,7 @@ type Env { DISABLE_SIGN_UP: Boolean! DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_STRONG_PASSWORD: Boolean! + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean! ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! ROLES: [String!] PROTECTED_ROLES: [String!] @@ -2066,6 +2084,7 @@ input UpdateEnvInput { DISABLE_SIGN_UP: Boolean DISABLE_REDIS_FOR_ENV: Boolean DISABLE_STRONG_PASSWORD: Boolean + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] @@ -4425,6 +4444,41 @@ func (ec *executionContext) _Env_DISABLE_STRONG_PASSWORD(ctx context.Context, fi return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Env_DISABLE_MULTI_FACTOR_AUTHENTICATION(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + 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.DisableMultiFactorAuthentication, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5558,6 +5612,41 @@ func (ec *executionContext) _Meta_is_strong_password_enabled(ctx context.Context return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Meta_is_multi_factor_auth_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Meta", + 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 { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -11379,6 +11468,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "DISABLE_MULTI_FACTOR_AUTHENTICATION": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_MULTI_FACTOR_AUTHENTICATION")) + it.DisableMultiFactorAuthentication, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } case "ENFORCE_MULTI_FACTOR_AUTHENTICATION": var err error @@ -12152,6 +12249,11 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "DISABLE_MULTI_FACTOR_AUTHENTICATION": + out.Values[i] = ec._Env_DISABLE_MULTI_FACTOR_AUTHENTICATION(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "ENFORCE_MULTI_FACTOR_AUTHENTICATION": out.Values[i] = ec._Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -12331,6 +12433,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "is_multi_factor_auth_enabled": + out.Values[i] = ec._Meta_is_multi_factor_auth_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } 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 3c5f883..553d6c5 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -86,6 +86,7 @@ type Env struct { DisableSignUp bool `json:"DISABLE_SIGN_UP"` DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"` + DisableMultiFactorAuthentication bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"` EnforceMultiFactorAuthentication bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"` Roles []string `json:"ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"` @@ -164,6 +165,7 @@ type Meta struct { IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsSignUpEnabled bool `json:"is_sign_up_enabled"` IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"` + IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"` } type OAuthRevokeInput struct { @@ -273,6 +275,7 @@ type UpdateEnvInput struct { DisableSignUp *bool `json:"DISABLE_SIGN_UP"` DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"` DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"` + DisableMultiFactorAuthentication *bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"` EnforceMultiFactorAuthentication *bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"` Roles []string `json:"ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index ddab91c..17fc1ec 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -25,6 +25,7 @@ type Meta { is_magic_link_login_enabled: Boolean! is_sign_up_enabled: Boolean! is_strong_password_enabled: Boolean! + is_multi_factor_auth_enabled: Boolean! } type User { @@ -124,6 +125,7 @@ type Env { DISABLE_SIGN_UP: Boolean! DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_STRONG_PASSWORD: Boolean! + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean! ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean! ROLES: [String!] PROTECTED_ROLES: [String!] @@ -225,6 +227,7 @@ input UpdateEnvInput { DISABLE_SIGN_UP: Boolean DISABLE_REDIS_FOR_ENV: Boolean DISABLE_STRONG_PASSWORD: Boolean + DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index dc7a195..a44856e 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -33,6 +33,7 @@ func InitMemStore() error { constants.EnvKeyDisableStrongPassword: false, constants.EnvKeyIsEmailServiceEnabled: false, constants.EnvKeyEnforceMultiFactorAuthentication: false, + constants.EnvKeyDisableMultiFactorAuthentication: false, } requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go index 4fb1206..f57e1ca 100644 --- a/server/memorystore/providers/redis/store.go +++ b/server/memorystore/providers/redis/store.go @@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) { return nil, err } for key, value := range data { - if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication { + if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication { boolValue, err := strconv.ParseBool(value) if err != nil { return res, err diff --git a/server/resolvers/env.go b/server/resolvers/env.go index e97fac4..cd78f98 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -171,6 +171,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool) res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool) res.EnforceMultiFactorAuthentication = store[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) + res.DisableMultiFactorAuthentication = store[constants.EnvKeyDisableMultiFactorAuthentication].(bool) return res, nil } diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 914e7bb..e05406e 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -16,6 +16,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" @@ -136,6 +137,15 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) user.SignupMethods = constants.AuthRecipeMethodBasicAuth verificationRequest.Identifier = constants.VerificationTypeForgotPassword + isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication) + if err != nil { + log.Debug("MFA service not enabled: ", err) + isMFAEnforced = false + } + + if isMFAEnforced { + user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true) + } verifyEmailURL = appURL + "/setup-password" } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 6325a6a..8587330 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -104,8 +104,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes log.Debug("Email service not enabled: ", err) } + isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication) + if err != nil || !isEmailServiceEnabled { + log.Debug("MFA service not enabled: ", err) + } + // If email service is not enabled continue the process in any way - if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled { + if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled { otp := utils.GenerateOTP() otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ Email: user.Email, diff --git a/server/resolvers/meta.go b/server/resolvers/meta.go index 2acbcb4..f53437c 100644 --- a/server/resolvers/meta.go +++ b/server/resolvers/meta.go @@ -107,6 +107,12 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { isSignUpDisabled = true } + isMultiFactorAuthenticationEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication) + if err != nil { + log.Debug("Failed to get Disable Multi Factor Authentication from environment variable", err) + isSignUpDisabled = true + } + metaInfo := model.Meta{ Version: constants.VERSION, ClientID: clientID, @@ -120,6 +126,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, IsSignUpEnabled: !isSignUpDisabled, IsStrongPasswordEnabled: !isStrongPasswordDisabled, + IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled, } return &metaInfo, nil } diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go index edd7445..4ae53c0 100644 --- a/server/resolvers/resend_otp.go +++ b/server/resolvers/resend_otp.go @@ -52,6 +52,12 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod return nil, errors.New("email service not enabled") } + isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication) + if err != nil || isMFADisabled { + log.Debug("MFA service not enabled: ", err) + return nil, errors.New("multi factor authentication is disabled for this instance") + } + // get otp by email otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email) if err != nil { diff --git a/server/resolvers/reset_password.go b/server/resolvers/reset_password.go index abdfc79..84976bb 100644 --- a/server/resolvers/reset_password.go +++ b/server/resolvers/reset_password.go @@ -14,6 +14,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" @@ -84,6 +85,16 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput) signupMethod := user.SignupMethods if !strings.Contains(signupMethod, constants.AuthRecipeMethodBasicAuth) { signupMethod = signupMethod + "," + constants.AuthRecipeMethodBasicAuth + + isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication) + if err != nil { + log.Debug("MFA service not enabled: ", err) + isMFAEnforced = false + } + + if isMFAEnforced { + user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true) + } } user.SignupMethods = signupMethod diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 71ffb93..b7a548c 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -17,6 +17,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" @@ -161,6 +162,16 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled } + isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication) + if err != nil { + log.Debug("MFA service not enabled: ", err) + isMFAEnforced = false + } + + if isMFAEnforced { + user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true) + } + user.SignupMethods = constants.AuthRecipeMethodBasicAuth isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) if err != nil { diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index deecc20..caeea6e 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -235,6 +235,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model // in case SMTP is off but env is set to true if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" { updatedData[constants.EnvKeyIsEmailServiceEnabled] = false + updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { updatedData[constants.EnvKeyDisableEmailVerification] = true } @@ -248,6 +249,12 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model updatedData[constants.EnvKeyIsEmailServiceEnabled] = true } + if !currentData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && updatedData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) { + go db.Provider.UpdateUsers(ctx, map[string]interface{}{ + "is_multi_factor_auth_enabled": true, + }, nil) + } + // check the roles change if len(params.Roles) > 0 { if len(params.DefaultRoles) > 0 { diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index ebc4718..87762d9 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -104,6 +104,17 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) } } + isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication) + if err != nil { + log.Debug("MFA service not enabled: ", err) + isMFAEnforced = false + } + + if isMFAEnforced && !refs.BoolValue(params.IsMultiFactorAuthEnabled) { + log.Debug("Cannot disable mfa service as it is enforced:") + return nil, errors.New("cannot disable multi factor authentication as it is enforced by organization") + } + user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled }