diff --git a/server/constants/env.go b/server/constants/env.go index f9d7561d2..d8382fbf8 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -176,4 +176,13 @@ const ( // EnvKeyDefaultAuthorizeResponseMode key for env variable DEFAULT_AUTHORIZE_RESPONSE_MODE // This env is used for setting default response mode in authorize handler EnvKeyDefaultAuthorizeResponseMode = "DEFAULT_AUTHORIZE_RESPONSE_MODE" + + // Phone verification setting + EnvKeyDisablePhoneVerification = "DISABLE_PHONE_VERIFICATION" + + // Twilio env variables + EnvKeyTwilioAPIKey = "TWILIO_API_KEY" + EnvKeyTwilioAPISecret = "TWILIO_API_SECRET" + EnvKeyTwilioAccountSID = "TWILIO_ACCOUNT_SID" + EnvKeyTwilioSenderFrom = "TWILIO_SENDER_FROM" ) diff --git a/server/db/models/model.go b/server/db/models/model.go index 87b78a4a0..a0d576380 100644 --- a/server/db/models/model.go +++ b/server/db/models/model.go @@ -10,6 +10,7 @@ type CollectionList struct { WebhookLog string EmailTemplate string OTP string + SMSVerificationRequest string } var ( @@ -25,5 +26,6 @@ var ( WebhookLog: Prefix + "webhook_logs", EmailTemplate: Prefix + "email_templates", OTP: Prefix + "otps", + SMSVerificationRequest: Prefix + "sms_verification_requests", } ) diff --git a/server/db/models/sms_verification_requests.go b/server/db/models/sms_verification_requests.go new file mode 100644 index 000000000..2a70d5ef4 --- /dev/null +++ b/server/db/models/sms_verification_requests.go @@ -0,0 +1,11 @@ +package models + +// SMS verification requests model for database +type SMSVerificationRequest struct { + ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"` + PhoneNumber string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number" index:"phone_number,hash"` + Code string `json:"code" bson:"code" cql:"code" dynamo:"code"` + CodeExpiresAt int64 `json:"code_expires_at" bson:"code_expires_at" cql:"code_expires_at" dynamo:"code_expires_at"` + CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"` + UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"` +} diff --git a/server/db/providers/arangodb/sms_verification_requests.go b/server/db/providers/arangodb/sms_verification_requests.go new file mode 100644 index 000000000..4dee5bd14 --- /dev/null +++ b/server/db/providers/arangodb/sms_verification_requests.go @@ -0,0 +1,23 @@ +package arangodb + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/db/models" + +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, sms_code *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + return sms_code, nil +} + +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var sms_verification_request models.SMSVerificationRequest + + return &sms_verification_request, nil +} + +func(p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + return nil +} diff --git a/server/db/providers/cassandradb/sms_verification_requests.go b/server/db/providers/cassandradb/sms_verification_requests.go new file mode 100644 index 000000000..3c67c1b19 --- /dev/null +++ b/server/db/providers/cassandradb/sms_verification_requests.go @@ -0,0 +1,23 @@ +package cassandradb + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/db/models" + +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, sms_code *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + return sms_code, nil +} + +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var sms_verification_request models.SMSVerificationRequest + + return &sms_verification_request, nil +} + +func(p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + return nil +} diff --git a/server/db/providers/couchbase/sms_verification_requests.go b/server/db/providers/couchbase/sms_verification_requests.go new file mode 100644 index 000000000..9201d73c6 --- /dev/null +++ b/server/db/providers/couchbase/sms_verification_requests.go @@ -0,0 +1,23 @@ +package couchbase + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/db/models" + +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, sms_code *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + return sms_code, nil +} + +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var sms_verification_request models.SMSVerificationRequest + + return &sms_verification_request, nil +} + +func(p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + return nil +} diff --git a/server/db/providers/dynamodb/sms_verification_requests.go b/server/db/providers/dynamodb/sms_verification_requests.go new file mode 100644 index 000000000..bd47bcedd --- /dev/null +++ b/server/db/providers/dynamodb/sms_verification_requests.go @@ -0,0 +1,23 @@ +package dynamodb + +import ( + "context" + + "github.com/authorizerdev/authorizer/server/db/models" + +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, sms_code *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + return sms_code, nil +} + +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var sms_verification_request models.SMSVerificationRequest + + return &sms_verification_request, nil +} + +func(p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + return nil +} diff --git a/server/db/providers/mongodb/provider.go b/server/db/providers/mongodb/provider.go index 7a969a920..fd4a0b2c0 100644 --- a/server/db/providers/mongodb/provider.go +++ b/server/db/providers/mongodb/provider.go @@ -119,6 +119,15 @@ func NewProvider() (*provider, error) { }, }, options.CreateIndexes()) + mongodb.CreateCollection(ctx, models.Collections.SMSVerificationRequest, options.CreateCollection()) + smsCollection := mongodb.Collection(models.Collections.SMSVerificationRequest, options.Collection()) + smsCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{ + { + Keys: bson.M{"phone_number": 1}, + Options: options.Index().SetUnique(true).SetSparse(true), + }, + }, options.CreateIndexes()) + return &provider{ db: mongodb, }, nil diff --git a/server/db/providers/mongodb/sms_verification_requests.go b/server/db/providers/mongodb/sms_verification_requests.go new file mode 100644 index 000000000..b2d3a1355 --- /dev/null +++ b/server/db/providers/mongodb/sms_verification_requests.go @@ -0,0 +1,69 @@ +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" +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + smsVerificationRequest, _ := p.GetCodeByPhone(ctx, smsRequest.PhoneNumber) + shouldCreate := false + + if smsVerificationRequest == nil { + id := uuid.NewString() + + smsVerificationRequest = &models.SMSVerificationRequest{ + ID: id, + CreatedAt: time.Now().Unix(), + Code: smsRequest.Code, + PhoneNumber: smsRequest.PhoneNumber, + CodeExpiresAt: smsRequest.CodeExpiresAt, + } + shouldCreate = true + } + + smsVerificationRequest.UpdatedAt = time.Now().Unix() + smsRequestCollection := p.db.Collection(models.Collections.SMSVerificationRequest, options.Collection()) + + var err error + if shouldCreate { + _, err = smsRequestCollection.InsertOne(ctx, smsVerificationRequest) + } else { + _, err = smsRequestCollection.UpdateOne(ctx, bson.M{"phone_number": bson.M{"$eq": smsRequest.PhoneNumber}}, bson.M{"$set": smsVerificationRequest}, options.MergeUpdateOptions()) + } + + if err != nil { + return nil, err + } + + return smsVerificationRequest, nil +} + +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var smsVerificationRequest models.SMSVerificationRequest + + smsRequestCollection := p.db.Collection(models.Collections.SMSVerificationRequest, options.Collection()) + err := smsRequestCollection.FindOne(ctx, bson.M{"phone_number": phoneNumber}).Decode(&smsVerificationRequest) + + if err != nil { + return nil, err + } + + return &smsVerificationRequest, nil +} + +func (p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + smsVerificationRequests := p.db.Collection(models.Collections.SMSVerificationRequest, options.Collection()) + _, err := smsVerificationRequests.DeleteOne(nil, bson.M{"_id": smsRequest.ID}, options.Delete()) + if err != nil { + return err + } + + return nil +} diff --git a/server/db/providers/providers.go b/server/db/providers/providers.go index f6a2aad4d..28fbf78fb 100644 --- a/server/db/providers/providers.go +++ b/server/db/providers/providers.go @@ -84,4 +84,11 @@ type Provider interface { GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) // DeleteOTP to delete otp DeleteOTP(ctx context.Context, otp *models.OTP) error + + // Upsert SMS code request + UpsertSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) + // Get sms code by phone number + GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) + // Delete sms + DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error } diff --git a/server/db/providers/sql/provider.go b/server/db/providers/sql/provider.go index 210195324..89ea31bba 100644 --- a/server/db/providers/sql/provider.go +++ b/server/db/providers/sql/provider.go @@ -77,7 +77,7 @@ func NewProvider() (*provider, error) { logrus.Debug("Failed to drop phone number constraint:", err) } - err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{}) + err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{}, &models.SMSVerificationRequest{}) if err != nil { return nil, err } diff --git a/server/db/providers/sql/sms_verification_requests.go b/server/db/providers/sql/sms_verification_requests.go new file mode 100644 index 000000000..5035c548e --- /dev/null +++ b/server/db/providers/sql/sms_verification_requests.go @@ -0,0 +1,51 @@ +package sql + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/google/uuid" + "gorm.io/gorm/clause" +) + +// SMS verification Request +func (p *provider) UpsertSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) (*models.SMSVerificationRequest, error) { + if smsRequest.ID == "" { + smsRequest.ID = uuid.New().String() + } + + smsRequest.CreatedAt = time.Now().Unix() + smsRequest.UpdatedAt = time.Now().Unix() + + res := p.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "phone_number"}}, + DoUpdates: clause.AssignmentColumns([]string{"code", "code_expires_at"}), + }).Create(smsRequest) + if res.Error != nil { + return nil, res.Error + } + + return smsRequest, nil +} + +// GetOTPByEmail to get otp for a given email address +func (p *provider) GetCodeByPhone(ctx context.Context, phoneNumber string) (*models.SMSVerificationRequest, error) { + var sms_verification_request models.SMSVerificationRequest + + result := p.db.Where("phone_number = ?", phoneNumber).First(&sms_verification_request) + if result.Error != nil { + return nil, result.Error + } + return &sms_verification_request, nil +} + +func(p *provider) DeleteSMSRequest(ctx context.Context, smsRequest *models.SMSVerificationRequest) error { + result := p.db.Delete(&models.SMSVerificationRequest{ + ID: smsRequest.ID, + }) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/server/env/env.go b/server/env/env.go index 0340c321c..f61b09328 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -111,6 +111,15 @@ func InitAllEnv() error { osDefaultRoles := os.Getenv(constants.EnvKeyDefaultRoles) osProtectedRoles := os.Getenv(constants.EnvKeyProtectedRoles) + // phone verification var + osDisablePhoneVerification := os.Getenv(constants.EnvKeyDisablePhoneVerification) + + // twilio vars + osTwilioApiKey := os.Getenv(constants.EnvKeyTwilioAPIKey) + osTwilioApiSecret := os.Getenv(constants.EnvKeyTwilioAPISecret) + osTwilioAccountSid := os.Getenv(constants.EnvKeyTwilioAccountSID) + osTwilioSenderFrom := os.Getenv(constants.EnvKeyTwilioSenderFrom) + ienv, ok := envData[constants.EnvKeyEnv] if !ok || ienv == "" { envData[constants.EnvKeyEnv] = osEnv @@ -136,6 +145,7 @@ func InitAllEnv() error { if val, ok := envData[constants.EnvAwsRegion]; !ok || val == "" { envData[constants.EnvAwsRegion] = osAwsRegion } + if osAwsRegion != "" && envData[constants.EnvAwsRegion] != osAwsRegion { envData[constants.EnvAwsRegion] = osAwsRegion } @@ -591,7 +601,7 @@ func InitAllEnv() error { if err != nil { return err } - if boolValue != envData[constants.EnvKeyDisableMagicLinkLogin].(bool) { + if boolValue != envData[constants.EnvKeyDisableMagicLinkLogin] { envData[constants.EnvKeyDisableMagicLinkLogin] = boolValue } } @@ -767,6 +777,37 @@ func InitAllEnv() error { envData[constants.EnvKeyDefaultAuthorizeResponseMode] = osAuthorizeResponseMode } + if osTwilioApiSecret != "" && envData[constants.EnvKeyTwilioAPISecret] != osTwilioApiSecret { + envData[constants.EnvKeyTwilioAPISecret] = osTwilioApiSecret + } + + if osTwilioApiKey != "" && envData[constants.EnvKeyTwilioAPIKey] != osTwilioApiKey { + envData[constants.EnvKeyTwilioAPIKey] = osTwilioApiKey + } + + if osTwilioAccountSid != "" && envData[constants.EnvKeyTwilioAccountSID] != osTwilioAccountSid { + envData[constants.EnvKeyTwilioAccountSID] = osTwilioAccountSid + } + + if osTwilioSenderFrom != "" && envData[constants.EnvKeyTwilioSenderFrom] != osTwilioSenderFrom { + envData[constants.EnvKeyTwilioSenderFrom] = osTwilioSenderFrom + } + + if _, ok := envData[constants.EnvKeyDisablePhoneVerification]; !ok { + envData[constants.EnvKeyDisablePhoneVerification] = osDisablePhoneVerification == "false" + } + + if osDisablePhoneVerification != "" { + boolValue, err := strconv.ParseBool(osDisablePhoneVerification) + + if err != nil { + return err + } + if boolValue != envData[constants.EnvKeyDisablePhoneVerification] { + envData[constants.EnvKeyDisablePhoneVerification] = boolValue + } + } + err = memorystore.Provider.UpdateEnvStore(envData) if err != nil { log.Debug("Error while updating env store: ", err) diff --git a/server/env/persist_env.go b/server/env/persist_env.go index a2243363a..8ba33f25d 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -200,7 +200,7 @@ func PersistEnv() error { envValue := strings.TrimSpace(os.Getenv(key)) if envValue != "" { switch key { - case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure: + case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure, constants.EnvKeyDisablePhoneVerification: if envValueBool, err := strconv.ParseBool(envValue); err == nil { if value.(bool) != envValueBool { storeData[key] = envValueBool diff --git a/server/go.mod b/server/go.mod index 9bc16207d..3408404b5 100644 --- a/server/go.mod +++ b/server/go.mod @@ -25,6 +25,7 @@ require ( github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.8.0 + github.com/twilio/twilio-go v1.7.2 github.com/vektah/gqlparser/v2 v2.5.1 go.mongodb.org/mongo-driver v1.8.1 golang.org/x/crypto v0.4.0 diff --git a/server/go.sum b/server/go.sum index 1f19088c4..224e06d82 100644 --- a/server/go.sum +++ b/server/go.sum @@ -54,6 +54,7 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdK github.com/aws/aws-sdk-go v1.42.47/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= github.com/aws/aws-sdk-go v1.44.109 h1:+Na5JPeS0kiEHoBp5Umcuuf+IDqXqD0lXnM920E31YI= github.com/aws/aws-sdk-go v1.44.109/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= @@ -151,6 +152,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -251,6 +254,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= +github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -271,6 +276,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= @@ -316,6 +322,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/twilio/twilio-go v1.7.2 h1:tX38DXbSuDWWIK+tKAE2AJSMR6d8i7lf9ksY8J29VLE= +github.com/twilio/twilio-go v1.7.2/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= @@ -338,6 +346,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.8.1 h1:OZE4Wni/SJlrcmSIBRYNzunX5TKxjrTS4jKSnA99oKU= @@ -392,6 +401,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -425,6 +435,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -483,7 +494,9 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -558,6 +571,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -651,6 +665,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 5c38f9c56..e8493068e 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -198,6 +198,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 + VerifyMobile func(childComplexity int, params model.VerifyMobileRequest) int VerifyOtp func(childComplexity int, params model.VerifyOTPRequest) int } @@ -228,6 +229,15 @@ type ComplexityRoot struct { Message func(childComplexity int) int } + SMSVerificationRequests struct { + Code func(childComplexity int) int + CodeExpiresAt func(childComplexity int) int + CreatedAt func(childComplexity int) int + ID func(childComplexity int) int + PhoneNumber func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + TestEndpointResponse struct { HTTPStatus func(childComplexity int) int Response func(childComplexity int) int @@ -329,6 +339,7 @@ type MutationResolver interface { 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) + VerifyMobile(ctx context.Context, params model.VerifyMobileRequest) (*model.AuthResponse, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -1421,6 +1432,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.VerifyEmail(childComplexity, args["params"].(model.VerifyEmailInput)), true + case "Mutation.verify_mobile": + if e.complexity.Mutation.VerifyMobile == nil { + break + } + + args, err := ec.field_Mutation_verify_mobile_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.VerifyMobile(childComplexity, args["params"].(model.VerifyMobileRequest)), true + case "Mutation.verify_otp": if e.complexity.Mutation.VerifyOtp == nil { break @@ -1604,6 +1627,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Response.Message(childComplexity), true + case "SMSVerificationRequests.code": + if e.complexity.SMSVerificationRequests.Code == nil { + break + } + + return e.complexity.SMSVerificationRequests.Code(childComplexity), true + + case "SMSVerificationRequests.code_expires_at": + if e.complexity.SMSVerificationRequests.CodeExpiresAt == nil { + break + } + + return e.complexity.SMSVerificationRequests.CodeExpiresAt(childComplexity), true + + case "SMSVerificationRequests.created_at": + if e.complexity.SMSVerificationRequests.CreatedAt == nil { + break + } + + return e.complexity.SMSVerificationRequests.CreatedAt(childComplexity), true + + case "SMSVerificationRequests.id": + if e.complexity.SMSVerificationRequests.ID == nil { + break + } + + return e.complexity.SMSVerificationRequests.ID(childComplexity), true + + case "SMSVerificationRequests.phone_number": + if e.complexity.SMSVerificationRequests.PhoneNumber == nil { + break + } + + return e.complexity.SMSVerificationRequests.PhoneNumber(childComplexity), true + + case "SMSVerificationRequests.updated_at": + if e.complexity.SMSVerificationRequests.UpdatedAt == nil { + break + } + + return e.complexity.SMSVerificationRequests.UpdatedAt(childComplexity), true + case "TestEndpointResponse.http_status": if e.complexity.TestEndpointResponse.HTTPStatus == nil { break @@ -2029,6 +2094,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputUpdateWebhookRequest, ec.unmarshalInputValidateJWTTokenInput, ec.unmarshalInputVerifyEmailInput, + ec.unmarshalInputVerifyMobileRequest, ec.unmarshalInputVerifyOTPRequest, ec.unmarshalInputWebhookRequest, ) @@ -2168,6 +2234,20 @@ type VerificationRequests { verification_requests: [VerificationRequest!]! } +type SMSVerificationRequests { + id: ID! + code: String! + code_expires_at: Int64! + phone_number: String! + created_at: Int64! + updated_at: Int64 +} + +input VerifyMobileRequest { + phone_number: String! + code: String! +} + type Error { message: String! reason: String! @@ -2649,6 +2729,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + verify_mobile(params: VerifyMobileRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -3113,6 +3194,21 @@ func (ec *executionContext) field_Mutation_verify_email_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_verify_mobile_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.VerifyMobileRequest + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNVerifyMobileRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyMobileRequest(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + 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{}{} @@ -8488,6 +8584,77 @@ func (ec *executionContext) fieldContext_Mutation_resend_otp(ctx context.Context return fc, nil } +func (ec *executionContext) _Mutation_verify_mobile(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_verify_mobile(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().VerifyMobile(rctx, fc.Args["params"].(model.VerifyMobileRequest)) + }) + 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) fieldContext_Mutation_verify_mobile(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "message": + return ec.fieldContext_AuthResponse_message(ctx, field) + case "should_show_otp_screen": + return ec.fieldContext_AuthResponse_should_show_otp_screen(ctx, field) + case "access_token": + return ec.fieldContext_AuthResponse_access_token(ctx, field) + case "id_token": + return ec.fieldContext_AuthResponse_id_token(ctx, field) + case "refresh_token": + return ec.fieldContext_AuthResponse_refresh_token(ctx, field) + case "expires_in": + return ec.fieldContext_AuthResponse_expires_in(ctx, field) + case "user": + return ec.fieldContext_AuthResponse_user(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_verify_mobile_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation__delete_user(ctx, field) if err != nil { @@ -10854,6 +11021,267 @@ func (ec *executionContext) fieldContext_Response_message(ctx context.Context, f return fc, nil } +func (ec *executionContext) _SMSVerificationRequests_id(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, 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.(string) + fc.Result = res + return ec.marshalNID2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SMSVerificationRequests_code(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_code(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Code, 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_code(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SMSVerificationRequests_code_expires_at(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_code_expires_at(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CodeExpiresAt, 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.(int64) + fc.Result = res + return ec.marshalNInt642int64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_code_expires_at(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int64 does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SMSVerificationRequests_phone_number(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_phone_number(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PhoneNumber, 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_phone_number(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SMSVerificationRequests_created_at(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_created_at(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CreatedAt, 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.(int64) + fc.Result = res + return ec.marshalNInt642int64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_created_at(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int64 does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _SMSVerificationRequests_updated_at(ctx context.Context, field graphql.CollectedField, obj *model.SMSVerificationRequests) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SMSVerificationRequests_updated_at(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.UpdatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int64) + fc.Result = res + return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SMSVerificationRequests_updated_at(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SMSVerificationRequests", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int64 does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _TestEndpointResponse_http_status(ctx context.Context, field graphql.CollectedField, obj *model.TestEndpointResponse) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TestEndpointResponse_http_status(ctx, field) if err != nil { @@ -17163,6 +17591,42 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputVerifyMobileRequest(ctx context.Context, obj interface{}) (model.VerifyMobileRequest, error) { + var it model.VerifyMobileRequest + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"phone_number", "code"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "phone_number": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("phone_number")) + it.PhoneNumber, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + case "code": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("code")) + it.Code, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, obj interface{}) (model.VerifyOTPRequest, error) { var it model.VerifyOTPRequest asMap := map[string]interface{}{} @@ -18069,6 +18533,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_resend_otp(ctx, field) }) + if out.Values[i] == graphql.Null { + invalids++ + } + case "verify_mobile": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_verify_mobile(ctx, field) + }) + if out.Values[i] == graphql.Null { invalids++ } @@ -18654,6 +19127,66 @@ func (ec *executionContext) _Response(ctx context.Context, sel ast.SelectionSet, return out } +var sMSVerificationRequestsImplementors = []string{"SMSVerificationRequests"} + +func (ec *executionContext) _SMSVerificationRequests(ctx context.Context, sel ast.SelectionSet, obj *model.SMSVerificationRequests) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, sMSVerificationRequestsImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SMSVerificationRequests") + case "id": + + out.Values[i] = ec._SMSVerificationRequests_id(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "code": + + out.Values[i] = ec._SMSVerificationRequests_code(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "code_expires_at": + + out.Values[i] = ec._SMSVerificationRequests_code_expires_at(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "phone_number": + + out.Values[i] = ec._SMSVerificationRequests_phone_number(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "created_at": + + out.Values[i] = ec._SMSVerificationRequests_created_at(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "updated_at": + + out.Values[i] = ec._SMSVerificationRequests_updated_at(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var testEndpointResponseImplementors = []string{"TestEndpointResponse"} func (ec *executionContext) _TestEndpointResponse(ctx context.Context, sel ast.SelectionSet, obj *model.TestEndpointResponse) graphql.Marshaler { @@ -20010,6 +20543,11 @@ func (ec *executionContext) unmarshalNVerifyEmailInput2githubᚗcomᚋauthorizer return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNVerifyMobileRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyMobileRequest(ctx context.Context, v interface{}) (model.VerifyMobileRequest, error) { + res, err := ec.unmarshalInputVerifyMobileRequest(ctx, v) + 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) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 82b57eac4..7a1e3763b 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -120,6 +120,7 @@ type Env struct { AdminCookieSecure bool `json:"ADMIN_COOKIE_SECURE"` DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE"` DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE"` + SmsCodeExpiryTime *string `json:"SMS_CODE_EXPIRY_TIME"` } type Error struct { @@ -265,6 +266,15 @@ type Response struct { Message string `json:"message"` } +type SMSVerificationRequests struct { + ID string `json:"id"` + Code string `json:"code"` + CodeExpiresAt int64 `json:"code_expires_at"` + PhoneNumber string `json:"phone_number"` + CreatedAt int64 `json:"created_at"` + UpdatedAt *int64 `json:"updated_at"` +} + type SessionQueryInput struct { Roles []string `json:"roles"` Scope []string `json:"scope"` @@ -468,6 +478,11 @@ type VerifyEmailInput struct { State *string `json:"state"` } +type VerifyMobileRequest struct { + PhoneNumber string `json:"phone_number"` + Code string `json:"code"` +} + type VerifyOTPRequest struct { Email string `json:"email"` Otp string `json:"otp"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 7691c9df2..4013b12d3 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -75,6 +75,20 @@ type VerificationRequests { verification_requests: [VerificationRequest!]! } +type SMSVerificationRequests { + id: ID! + code: String! + code_expires_at: Int64! + phone_number: String! + created_at: Int64! + updated_at: Int64 +} + +input VerifyMobileRequest { + phone_number: String! + code: String! +} + type Error { message: String! reason: String! @@ -556,6 +570,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + verify_mobile(params: VerifyMobileRequest!): AuthResponse! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 67ce47b9c..75dad85a9 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -81,6 +81,11 @@ func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTP return resolvers.ResendOTPResolver(ctx, params) } +// VerifyMobile is the resolver for the verify_mobile field. +func (r *mutationResolver) VerifyMobile(ctx context.Context, params model.VerifyMobileRequest) (*model.AuthResponse, error) { + return resolvers.VerifyMobileResolver(ctx, params) +} + // DeleteUser is the resolver for the _delete_user field. func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) diff --git a/server/resolvers/mobile_signup.go b/server/resolvers/mobile_signup.go index 94d5b03a9..9aee0a65a 100644 --- a/server/resolvers/mobile_signup.go +++ b/server/resolvers/mobile_signup.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" - + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/crypto" @@ -19,6 +19,7 @@ import ( "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" + "github.com/authorizerdev/authorizer/server/smsproviders" "github.com/authorizerdev/authorizer/server/validators" ) @@ -131,11 +132,9 @@ func MobileSignupResolver(ctx context.Context, params *model.MobileSignUpInput) } } - now := time.Now().Unix() user := models.User{ Email: emailInput, PhoneNumber: &mobile, - PhoneNumberVerifiedAt: &now, } user.Roles = strings.Join(inputRoles, ",") @@ -180,17 +179,49 @@ func MobileSignupResolver(ctx context.Context, params *model.MobileSignUpInput) log.Debug("MFA service not enabled: ", err) isMFAEnforced = false } - + if isMFAEnforced { user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true) } + disablePhoneVerification, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification) + if disablePhoneVerification { + now := time.Now().Unix() + user.PhoneNumberVerifiedAt = &now + } + user.SignupMethods = constants.AuthRecipeMethodMobileBasicAuth user, err = db.Provider.AddUser(ctx, user) + if err != nil { log.Debug("Failed to add user: ", err) return res, err } + + if !disablePhoneVerification { + duration, _ := time.ParseDuration("10m") + smsCode := utils.GenerateOTP() + + smsBody := strings.Builder{} + smsBody.WriteString("Your verification code is: ") + smsBody.WriteString(smsCode) + + // TODO: For those who enabled the webhook to call their sms vendor separately - sending the otp to their api + if err != nil { + log.Debug("error while upserting user: ", err.Error()) + return nil, err + } + + go func() { + db.Provider.UpsertSMSRequest(ctx, &models.SMSVerificationRequest{ + PhoneNumber: mobile, + Code: smsCode, + CodeExpiresAt: time.Now().Add(duration).Unix(), + }) + smsproviders.SendSMS(mobile, smsBody.String()) + }() + } + roles := strings.Split(user.Roles, ",") userToReturn := user.AsAPIUser() diff --git a/server/resolvers/verify_mobile.go b/server/resolvers/verify_mobile.go new file mode 100644 index 000000000..4e077d7a5 --- /dev/null +++ b/server/resolvers/verify_mobile.go @@ -0,0 +1,62 @@ +package resolvers + +import ( + "fmt" + "context" + "time" + + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/utils" + "github.com/authorizerdev/authorizer/server/db" + log "github.com/sirupsen/logrus" +) + +func VerifyMobileResolver(ctx context.Context, params model.VerifyMobileRequest) (*model.AuthResponse, error) { + var res *model.AuthResponse + + _, err := utils.GinContextFromContext(ctx) + if err != nil { + log.Debug("Failed to get GinContext: ", err) + return res, err + } + + smsVerificationRequest, err := db.Provider.GetCodeByPhone(ctx, params.PhoneNumber) + if err != nil { + log.Debug("Failed to get sms request by phone: ", err) + return res, err + } + + if smsVerificationRequest.Code != params.Code { + log.Debug("Failed to verify request: bad credentials") + return res, fmt.Errorf(`bad credentials`) + } + + expiresIn := smsVerificationRequest.CodeExpiresAt - time.Now().Unix() + if expiresIn < 0 { + log.Debug("Failed to verify sms request: Timeout") + return res, fmt.Errorf("time expired") + } + + res = &model.AuthResponse{ + Message: "successful", + } + + user, err := db.Provider.GetUserByPhoneNumber(ctx, params.PhoneNumber) + if user.PhoneNumberVerifiedAt == nil { + now := time.Now().Unix() + user.PhoneNumberVerifiedAt = &now + } + + _, err = db.Provider.UpdateUser(ctx, *user) + if err != nil { + log.Debug("Failed to update user: ", err) + return res, err + } + + err = db.Provider.DeleteSMSRequest(ctx, smsVerificationRequest) + if err != nil { + log.Debug("Failed to delete sms request: ", err.Error()) + } + + return res, err +} diff --git a/server/smsproviders/twilio.go b/server/smsproviders/twilio.go new file mode 100644 index 000000000..093e924be --- /dev/null +++ b/server/smsproviders/twilio.go @@ -0,0 +1,54 @@ +package smsproviders + +import ( + twilio "github.com/twilio/twilio-go" + api "github.com/twilio/twilio-go/rest/api/v2010" + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/memorystore" + log "github.com/sirupsen/logrus" +) + +// TODO: Should be restructured to interface when another provider is added +func SendSMS(sendTo, messageBody string) error { + + twilioAPISecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAPISecret) + if err != nil || twilioAPISecret == ""{ + log.Errorf("Failed to get api secret: ", err) + return err + } + + twilioAPIKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAPIKey) + if err != nil || twilioAPIKey == ""{ + log.Errorf("Failed to get api key: ", err) + return err + } + + twilioSenderFrom, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioSenderFrom) + if err != nil || twilioSenderFrom == "" { + log.Errorf("Failed to get sender: ", err) + return err + } + + // accountSID is not a must to send sms on twilio + twilioAccountSID, _ := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwilioAccountSID) + + client := twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: twilioAPIKey, + Password: twilioAPISecret, + AccountSid: twilioAccountSID, + }) + + message := &api.CreateMessageParams{} + message.SetBody(messageBody) + message.SetFrom(twilioSenderFrom) + message.SetTo(sendTo) + + _, err = client.Api.CreateMessage(message) + + if err != nil { + log.Debug("Failed to send sms: ", err) + return err + } + + return nil +} diff --git a/server/test/mobile_login_test.go b/server/test/mobile_login_test.go index 544a140cb..48b76904e 100644 --- a/server/test/mobile_login_test.go +++ b/server/test/mobile_login_test.go @@ -6,6 +6,7 @@ import ( "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/resolvers" "github.com/stretchr/testify/assert" @@ -45,6 +46,25 @@ func mobileLoginTests(t *testing.T, s TestSetup) { assert.Error(t, err) assert.Nil(t, res) + // should fail because phone is not verified + res, err = resolvers.MobileLoginResolver(ctx, model.MobileLoginInput{ + PhoneNumber: phoneNumber, + Password: s.TestInfo.Password, + }) + assert.NotNil(t, err, "should fail because phone is not verified") + assert.Nil(t, res) + + smsRequest, err := db.Provider.GetCodeByPhone(ctx, phoneNumber) + assert.NoError(t, err) + assert.NotEmpty(t, smsRequest.Code) + + verifySMSRequest, err := resolvers.VerifyMobileResolver(ctx, model.VerifyMobileRequest{ + PhoneNumber: phoneNumber, + Code: smsRequest.Code, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifySMSRequest.Message, "", "message should not be empty") + res, err = resolvers.MobileLoginResolver(ctx, model.MobileLoginInput{ PhoneNumber: phoneNumber, Password: s.TestInfo.Password, diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index 3763e4df5..4c83bf3e3 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -135,6 +135,7 @@ func TestResolvers(t *testing.T) { validateJwtTokenTest(t, s) verifyOTPTest(t, s) resendOTPTest(t, s) + verifyMobileTest(t, s) updateAllUsersTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done diff --git a/server/test/verify_mobile_test.go b/server/test/verify_mobile_test.go new file mode 100644 index 000000000..b4daa5e68 --- /dev/null +++ b/server/test/verify_mobile_test.go @@ -0,0 +1,79 @@ +package test + +import ( + "strings" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func verifyMobileTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should verify mobile`, func(t *testing.T) { + _, ctx := createContext(s) + email := "mobile_verification." + s.TestInfo.Email + phoneNumber := "2234567890" + signUpRes, err := resolvers.MobileSignupResolver(ctx, &model.MobileSignUpInput{ + Email: refs.NewStringRef(email), + PhoneNumber: phoneNumber, + Password: s.TestInfo.Password, + ConfirmPassword: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotNil(t, signUpRes) + assert.Equal(t, email, signUpRes.User.Email) + assert.Equal(t, phoneNumber, refs.StringValue(signUpRes.User.PhoneNumber)) + assert.True(t, strings.Contains(signUpRes.User.SignupMethods, constants.AuthRecipeMethodMobileBasicAuth)) + assert.Len(t, strings.Split(signUpRes.User.SignupMethods, ","), 1) + + res, err := resolvers.MobileLoginResolver(ctx, model.MobileLoginInput{ + PhoneNumber: phoneNumber, + Password: "random_test", + }) + assert.Error(t, err) + assert.Nil(t, res) + + // should fail because phone is not verified + res, err = resolvers.MobileLoginResolver(ctx, model.MobileLoginInput{ + PhoneNumber: phoneNumber, + Password: s.TestInfo.Password, + }) + assert.NotNil(t, err, "should fail because phone is not verified") + assert.Nil(t, res) + + // get code from db + smsRequest, err := db.Provider.GetCodeByPhone(ctx, phoneNumber) + assert.NoError(t, err) + assert.NotEmpty(t, smsRequest.Code) + + // throw an error if the code is not correct + verifySMSRequest, err := resolvers.VerifyMobileResolver(ctx, model.VerifyMobileRequest{ + PhoneNumber: phoneNumber, + Code: "rand_12@1", + }) + assert.NotNil(t, err, "should fail because of bad credentials") + assert.Nil(t, verifySMSRequest) + + verifySMSRequest, err = resolvers.VerifyMobileResolver(ctx, model.VerifyMobileRequest{ + PhoneNumber: phoneNumber, + Code: smsRequest.Code, + }) + assert.Nil(t, err) + assert.NotEqual(t, verifySMSRequest.Message, "", "message should not be empty") + + res, err = resolvers.MobileLoginResolver(ctx, model.MobileLoginInput{ + PhoneNumber: phoneNumber, + Password: s.TestInfo.Password, + }) + assert.NoError(t, err) + assert.NotEmpty(t, res.AccessToken) + assert.NotEmpty(t, res.IDToken) + + cleanData(email) + }) +}