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/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/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 3b5a767..06fe1fd 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/constants/env.go b/server/constants/env.go index 7b0b06f..bd3afaf 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 @@ -117,6 +119,12 @@ 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 + // 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/db/models/model.go b/server/db/models/model.go index 553f3f5..87b78a4 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 + "webhooks", WebhookLog: Prefix + "webhook_logs", EmailTemplate: Prefix + "email_templates", + 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/models/user.go b/server/db/models/user.go index 3731f3b..bc5bc04 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -14,51 +14,53 @@ 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 { 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, - 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: user.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/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 new file mode 100644 index 0000000..29f265a --- /dev/null +++ b/server/db/providers/arangodb/otp.go @@ -0,0 +1,92 @@ +package arangodb + +import ( + "context" + "fmt" + "time" + + "github.com/arangodb/go-driver" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" +) + +// 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 { + 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(ctx, models.Collections.OTP) + + var meta driver.DocumentMeta + var err error + if shouldCreate { + 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() + 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 + 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 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 new file mode 100644 index 0000000..bfe481d --- /dev/null +++ b/server/db/providers/cassandradb/otp.go @@ -0,0 +1,67 @@ +package cassandradb + +import ( + "context" + "fmt" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/gocql/gocql" + "github.com/google/uuid" +) + +// 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 = &models.OTP{ + ID: uuid.NewString(), + Otp: otpParam.Otp, + Email: otpParam.Email, + ExpiresAt: otpParam.ExpiresAt, + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + } + } else { + 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 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 + } + + 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 + 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 08b2a26..0d0b193 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 { @@ -159,6 +161,13 @@ 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 { + 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 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) @@ -221,6 +230,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/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 new file mode 100644 index 0000000..d6ff2df --- /dev/null +++ b/server/db/providers/mongodb/otp.go @@ -0,0 +1,70 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// 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 { + 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()) + + 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 + } + + 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 + + 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/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/otp.go b/server/db/providers/provider_template/otp.go new file mode 100644 index 0000000..d8685e7 --- /dev/null +++ b/server/db/providers/provider_template/otp.go @@ -0,0 +1,22 @@ +package provider_template + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/db/models" +) + +// 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 +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/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 6f8c074..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) @@ -72,4 +75,11 @@ type Provider interface { GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) // DeleteEmailTemplate to delete EmailTemplate DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) 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 + 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..9aabcab --- /dev/null +++ b/server/db/providers/sql/otp.go @@ -0,0 +1,53 @@ +package sql + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" + "gorm.io/gorm/clause" +) + +// 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() + + 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 + } + + 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..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 @@ -60,7 +61,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 } 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/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/env.go b/server/env/env.go index 8d6c3c4..f8df401 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -84,6 +84,8 @@ func InitAllEnv() error { osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp) 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) @@ -490,10 +492,49 @@ 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 + } + } + + 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 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.EnvKeyEnforceMultiFactorAuthentication].(bool) && !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) { + 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) { diff --git a/server/env/persist_env.go b/server/env/persist_env.go index e9b849d..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: + 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 @@ -221,6 +221,8 @@ 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 hasChanged = true diff --git a/server/go.mod b/server/go.mod index 13e3e52..8b5cea4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,7 +9,7 @@ require ( 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..c51da71 100644 --- a/server/go.sum +++ b/server/go.sum @@ -110,8 +110,8 @@ 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= 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 7acb942..9c5130f 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 { @@ -67,54 +68,56 @@ 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 + DisableMultiFactorAuthentication 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 { @@ -138,6 +141,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 @@ -159,6 +163,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 @@ -171,6 +176,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 { @@ -205,24 +211,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 { @@ -293,6 +300,8 @@ 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) + 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) @@ -376,6 +385,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 @@ -586,6 +602,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 @@ -607,6 +630,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 @@ -873,6 +903,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 @@ -1064,6 +1101,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 @@ -1208,6 +1257,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 @@ -1437,6 +1498,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 @@ -1822,6 +1890,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 { @@ -1844,6 +1913,7 @@ type User { created_at: Int64 updated_at: Int64 revoked_timestamp: Int64 + is_multi_factor_auth_enabled: Boolean } type Users { @@ -1875,6 +1945,7 @@ type Error { type AuthResponse { message: String! + should_show_otp_screen: Boolean access_token: String id_token: String refresh_token: String @@ -1919,6 +1990,8 @@ 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!] DEFAULT_ROLES: [String!] @@ -2020,6 +2093,8 @@ 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!] DEFAULT_ROLES: [String!] @@ -2061,6 +2136,7 @@ input SignUpInput { roles: [String!] scope: [String!] redirect_uri: String + is_multi_factor_auth_enabled: Boolean } input LoginInput { @@ -2092,6 +2168,7 @@ input UpdateProfileInput { birthdate: String phone_number: String picture: String + is_multi_factor_auth_enabled: Boolean } input UpdateUserInput { @@ -2107,6 +2184,7 @@ input UpdateUserInput { phone_number: String picture: String roles: [String] + is_multi_factor_auth_enabled: Boolean } input ForgotPasswordInput { @@ -2217,6 +2295,15 @@ input DeleteEmailTemplateRequest { id: ID! } +input VerifyOTPRequest { + email: String! + otp: String! +} + +input ResendOTPRequest { + email: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -2228,6 +2315,8 @@ type Mutation { forgot_password(params: ForgotPasswordInput!): Response! 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! @@ -2556,6 +2645,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{}{} @@ -2646,6 +2750,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{}{} @@ -2854,6 +2973,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 { @@ -4339,6 +4490,76 @@ 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 { + 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 { @@ -5437,6 +5658,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 { @@ -5850,6 +6106,90 @@ 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_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 { @@ -7934,6 +8274,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 { @@ -10597,6 +10969,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{}{} @@ -10819,6 +11214,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 + } } } @@ -11127,6 +11530,22 @@ 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 + + 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 @@ -11366,6 +11785,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 + } } } @@ -11477,6 +11904,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 + } } } @@ -11600,6 +12035,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{}{} @@ -11647,6 +12113,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": @@ -11848,6 +12316,16 @@ 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 { + invalids++ + } case "ROLES": out.Values[i] = ec._Env_ROLES(ctx, field, obj) case "PROTECTED_ROLES": @@ -12022,6 +12500,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)) } @@ -12098,6 +12581,16 @@ 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 "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 { @@ -12549,6 +13042,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)) } @@ -13325,6 +13820,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) @@ -13618,6 +14118,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 6d7f319..69f2ab1 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -24,12 +24,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 { @@ -55,54 +56,56 @@ 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"` + DisableMultiFactorAuthentication bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"` + 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 { @@ -164,6 +167,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 { @@ -186,6 +190,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"` @@ -207,20 +215,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 { @@ -246,75 +255,79 @@ 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"` + DisableMultiFactorAuthentication *bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"` + 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 { - 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 { @@ -326,24 +339,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 { @@ -382,6 +396,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 e163379..156be33 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 { @@ -47,6 +48,7 @@ type User { created_at: Int64 updated_at: Int64 revoked_timestamp: Int64 + is_multi_factor_auth_enabled: Boolean } type Users { @@ -78,6 +80,7 @@ type Error { type AuthResponse { message: String! + should_show_otp_screen: Boolean access_token: String id_token: String refresh_token: String @@ -122,6 +125,8 @@ 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!] DEFAULT_ROLES: [String!] @@ -223,6 +228,8 @@ 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!] DEFAULT_ROLES: [String!] @@ -264,6 +271,7 @@ input SignUpInput { roles: [String!] scope: [String!] redirect_uri: String + is_multi_factor_auth_enabled: Boolean } input LoginInput { @@ -295,6 +303,7 @@ input UpdateProfileInput { birthdate: String phone_number: String picture: String + is_multi_factor_auth_enabled: Boolean } input UpdateUserInput { @@ -310,6 +319,7 @@ input UpdateUserInput { phone_number: String picture: String roles: [String] + is_multi_factor_auth_enabled: Boolean } input ForgotPasswordInput { @@ -420,6 +430,15 @@ input DeleteEmailTemplateRequest { id: ID! } +input VerifyOTPRequest { + email: String! + otp: String! +} + +input ResendOTPRequest { + email: String! +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -431,6 +450,8 @@ type Mutation { forgot_password(params: ForgotPasswordInput!): Response! 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 2cf0acf..8d5b55b 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -51,6 +51,14 @@ 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) 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/memorystore/memory_store.go b/server/memorystore/memory_store.go index ad6a418..a44856e 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -25,12 +25,15 @@ 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.EnvKeyDisableBasicAuthentication: false, + constants.EnvKeyDisableMagicLinkLogin: false, + constants.EnvKeyDisableEmailVerification: false, + constants.EnvKeyDisableLoginPage: false, + constants.EnvKeyDisableSignUp: false, + constants.EnvKeyDisableStrongPassword: false, + constants.EnvKeyIsEmailServiceEnabled: false, + constants.EnvKeyEnforceMultiFactorAuthentication: false, + constants.EnvKeyDisableMultiFactorAuthentication: 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 36b4b0c..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 { + 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 3abda07..cd78f98 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -170,6 +170,8 @@ 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) + res.DisableMultiFactorAuthentication = store[constants.EnvKeyDisableMultiFactorAuthentication].(bool) return res, nil } diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 2316456..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" @@ -35,13 +36,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") } @@ -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 e55f984..8587330 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -13,8 +13,10 @@ 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" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/validators" @@ -97,6 +99,42 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes scope = params.Scope } + isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled) + if err != nil || !isEmailServiceEnabled { + 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 && !isMFADisabled { + otp := utils.GenerateOTP() + otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{ + Email: user.Email, + 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), + }, nil + } + authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) if err != nil { log.Debug("Failed to create auth token", err) 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 new file mode 100644 index 0000000..4ae53c0 --- /dev/null +++ b/server/resolvers/resend_otp.go @@ -0,0 +1,96 @@ +package resolvers + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + 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" +) + +// ResendOTPResolver is a resolver for resend otp mutation +func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) { + 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 nil, fmt.Errorf(`user with this email not found`) + } + + if user.RevokedTimestamp != nil { + log.Debug("User access is revoked") + return nil, fmt.Errorf(`user access has been revoked`) + } + + if user.EmailVerifiedAt == nil { + log.Debug("User email is not verified") + return nil, fmt.Errorf(`email not verified`) + } + + if !refs.BoolValue(user.IsMultiFactorAuthEnabled) { + log.Debug("User multi factor authentication is not enabled") + 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") + } + + 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 { + 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: otp, + ExpiresAt: time.Now().Add(1 * time.Minute).Unix(), + }) + if err != nil { + log.Debug("Error generating new otp: ", err) + return nil, err + } + + 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/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 dbd3652..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" @@ -157,6 +158,20 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR user.Picture = params.Picture } + if params.IsMultiFactorAuthEnabled != nil { + 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 508d47e..caeea6e 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -234,6 +234,8 @@ 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 + updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { updatedData[constants.EnvKeyDisableEmailVerification] = true } @@ -243,6 +245,16 @@ 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 + } + + 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 { @@ -265,8 +277,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() @@ -320,6 +330,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 18058f2..87762d9 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" @@ -46,7 +47,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") } @@ -94,6 +95,29 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) user.Picture = params.Picture } + if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(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") + } + } + + 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 + } + 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..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" @@ -15,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" @@ -45,7 +47,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") } @@ -56,38 +58,49 @@ 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 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 { if *params.EmailVerified { now := time.Now().Unix() diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go new file mode 100644 index 0000000..b792adb --- /dev/null +++ b/server/resolvers/verify_otp.go @@ -0,0 +1,104 @@ +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) { + 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()) + } + + 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 expiresIn < 0 { + log.Debug("Failed to verify otp request: Timeout") + return res, fmt.Errorf("otp expired") + } + + 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 when we introduce OTP for social media login + 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() { + db.Provider.DeleteOTP(gc, otp) + 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 + } + + 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/resend_otp_test.go b/server/test/resend_otp_test.go new file mode 100644 index 0000000..2ba256c --- /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 resend otp`, func(t *testing.T) { + req, ctx := createContext(s) + email := "resend_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 1a49632..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) } @@ -114,7 +114,10 @@ func TestResolvers(t *testing.T) { metaTests(t, s) inviteUserTest(t, s) 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/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/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 new file mode 100644 index 0000000..9e074cd --- /dev/null +++ b/server/test/verify_otp_test.go @@ -0,0 +1,75 @@ +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 verifyOTPTest(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) + 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{ + 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) + }) +} diff --git a/server/utils/generate_otp.go b/server/utils/generate_otp.go new file mode 100644 index 0000000..61416b1 --- /dev/null +++ b/server/utils/generate_otp.go @@ -0,0 +1,25 @@ +package utils + +import ( + "math/rand" + "time" +) + +// GenerateOTP to generate random 6 digit otp +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))) +} 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) { 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 {