fix: user verification

This commit is contained in:
Lakhan Samani
2024-03-28 13:57:47 +05:30
parent 27b51ad369
commit 67f866a787
21 changed files with 1824 additions and 1590 deletions

View File

@@ -11,8 +11,8 @@ const (
type OTP struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email" dynamo:"email" index:"email,hash"`
PhoneNumber string `gorm:"index:unique_index_phone_number,unique" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number"`
Email string `gorm:"index" json:"email" bson:"email" cql:"email" dynamo:"email" index:"email,hash"`
PhoneNumber string `gorm:"index" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number"`
Otp string `json:"otp" bson:"otp" cql:"otp" dynamo:"otp"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at" dynamo:"expires_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"`

View File

@@ -15,7 +15,7 @@ type User struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty" dynamo:"key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id" dynamo:"id,hash"`
Email *string `gorm:"unique" json:"email" bson:"email" cql:"email" dynamo:"email" index:"email,hash"`
Email *string `gorm:"index" json:"email" bson:"email" cql:"email" dynamo:"email" index:"email,hash"`
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at" dynamo:"email_verified_at"`
Password *string `json:"password" bson:"password" cql:"password" dynamo:"password"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods" dynamo:"signup_methods"`

View File

@@ -36,6 +36,10 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()

View File

@@ -35,6 +35,10 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()

View File

@@ -4,12 +4,14 @@ import (
"context"
"fmt"
"log"
"strings"
"time"
"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/authorizerdev/authorizer/server/refs"
"github.com/couchbase/gocb/v2"
"github.com/google/uuid"
)
@@ -28,6 +30,16 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
user.Roles = defaultRoles
}
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
insertOpt := gocb.InsertOptions{

View File

@@ -31,9 +31,13 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
user.Roles = defaultRoles
}
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()

View File

@@ -2,12 +2,15 @@ package mongodb
import (
"context"
"fmt"
"strings"
"time"
"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/authorizerdev/authorizer/server/refs"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
@@ -27,6 +30,15 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
}
user.Roles = defaultRoles
}
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.Key = user.ID

View File

@@ -2,12 +2,15 @@ package provider_template
import (
"context"
"fmt"
"strings"
"time"
"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/authorizerdev/authorizer/server/refs"
"github.com/google/uuid"
)
@@ -23,6 +26,15 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
}
user.Roles = defaultRoles
}
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
return user, nil

View File

@@ -7,33 +7,56 @@ import (
"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()
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
if otpParam.ID == "" {
otpParam.ID = uuid.New().String()
}
// check if email or phone number is present
if otp.Email == "" && otp.PhoneNumber == "" {
if otpParam.Email == "" && otpParam.PhoneNumber == "" {
return nil, errors.New("email or phone_number is required")
}
uniqueField := models.FieldNameEmail
if otp.Email == "" && otp.PhoneNumber != "" {
if otpParam.Email == "" && otpParam.PhoneNumber != "" {
uniqueField = models.FieldNamePhoneNumber
}
otp.Key = otp.ID
otp.CreatedAt = time.Now().Unix()
otp.UpdatedAt = time.Now().Unix()
res := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: uniqueField}},
DoUpdates: clause.AssignmentColumns([]string{"otp", "expires_at", "updated_at"}),
}).Create(&otp)
if res.Error != nil {
return nil, res.Error
var otp *models.OTP
if uniqueField == models.FieldNameEmail {
otp, _ = p.GetOTPByEmail(ctx, otpParam.Email)
} else {
otp, _ = p.GetOTPByPhoneNumber(ctx, otpParam.PhoneNumber)
}
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
PhoneNumber: otpParam.PhoneNumber,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
if shouldCreate {
result := p.db.Create(&otp)
if result.Error != nil {
return nil, result.Error
}
} else {
result := p.db.Save(&otp)
if result.Error != nil {
return nil, result.Error
}
}
return otp, nil
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/authorizerdev/authorizer/server/refs"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// AddUser to save user information in database
@@ -31,19 +30,19 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
}
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil {
if u, _ := p.GetUserByPhoneNumber(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given phone number already exists")
}
} else if user.Email != nil && strings.TrimSpace(refs.StringValue(user.Email)) != "" {
if u, _ := p.GetUserByEmail(ctx, refs.StringValue(user.Email)); u != nil && u.ID != user.ID {
return user, fmt.Errorf("user with given email already exists")
}
}
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.Key = user.ID
result := p.db.Clauses(
clause.OnConflict{
UpdateAll: true,
Columns: []clause.Column{{Name: "email"}},
}).Create(&user)
result := p.db.Create(&user)
if result.Error != nil {
return user, result.Error

View File

@@ -2837,6 +2837,7 @@ input UpdateUserInput {
gender: String
birthdate: String
phone_number: String
phone_number_verified: Boolean
picture: String
roles: [String]
is_multi_factor_auth_enabled: Boolean
@@ -18985,7 +18986,7 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o
asMap[k] = v
}
fieldsInOrder := [...]string{"id", "email", "email_verified", "given_name", "family_name", "middle_name", "nickname", "gender", "birthdate", "phone_number", "picture", "roles", "is_multi_factor_auth_enabled", "app_data"}
fieldsInOrder := [...]string{"id", "email", "email_verified", "given_name", "family_name", "middle_name", "nickname", "gender", "birthdate", "phone_number", "phone_number_verified", "picture", "roles", "is_multi_factor_auth_enabled", "app_data"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -19082,6 +19083,15 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o
return it, err
}
it.PhoneNumber = data
case "phone_number_verified":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("phone_number_verified"))
data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
it.PhoneNumberVerified = data
case "picture":
var err error

View File

@@ -440,6 +440,7 @@ type UpdateUserInput struct {
Gender *string `json:"gender,omitempty"`
Birthdate *string `json:"birthdate,omitempty"`
PhoneNumber *string `json:"phone_number,omitempty"`
PhoneNumberVerified *bool `json:"phone_number_verified,omitempty"`
Picture *string `json:"picture,omitempty"`
Roles []*string `json:"roles,omitempty"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled,omitempty"`

View File

@@ -452,6 +452,7 @@ input UpdateUserInput {
gender: String
birthdate: String
phone_number: String
phone_number_verified: Boolean
picture: String
roles: [String]
is_multi_factor_auth_enabled: Boolean

View File

@@ -83,6 +83,39 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
log.Debug("User access is revoked")
return res, fmt.Errorf(`user access has been revoked`)
}
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
}
isSMSServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsSMSServiceEnabled)
if err != nil || !isSMSServiceEnabled {
log.Debug("SMS service not enabled: ", err)
}
// If multi factor authentication is enabled and we need to generate OTP for mail / sms based MFA
generateOTP := func(expiresAt int64) (*models.OTP, error) {
otp := utils.GenerateOTP()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: refs.StringValue(user.Email),
PhoneNumber: refs.StringValue(user.PhoneNumber),
Otp: otp,
ExpiresAt: expiresAt,
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
return otpData, nil
}
setOTPMFaSession := func(expiresAt int64) error {
mfaSession := uuid.NewString()
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt)
if err != nil {
log.Debug("Failed to add mfasession: ", err)
return err
}
cookie.SetMfaSession(gc, mfaSession)
return nil
}
if isEmailLogin {
if !strings.Contains(user.SignupMethods, constants.AuthRecipeMethodBasicAuth) {
log.Debug("User signup method is not basic auth")
@@ -90,8 +123,38 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
}
if user.EmailVerifiedAt == nil {
log.Debug("User email is not verified")
return res, fmt.Errorf(`email not verified`)
// Check if email service is enabled
// Send email verification via otp
if !isEmailServiceEnabled {
log.Debug("User email is not verified and email service is not enabled")
return res, fmt.Errorf(`email not verified`)
} else {
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err := generateOTP(expiresAt)
if err != nil {
log.Debug("Failed to generate otp: ", err)
return nil, err
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
go func() {
// exec it as go routine so that we can reduce the api latency
if err := mailService.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"otp": otpData.Otp,
}); err != nil {
log.Debug("Failed to send otp email: ", err)
}
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
}()
return &model.AuthResponse{
Message: "Please check email inbox for the OTP",
ShouldShowEmailOtpScreen: refs.NewBoolRef(isMobileLogin),
}, nil
}
}
} else {
if !strings.Contains(user.SignupMethods, constants.AuthRecipeMethodMobileBasicAuth) {
@@ -100,8 +163,34 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
}
if user.PhoneNumberVerifiedAt == nil {
log.Debug("User phone number is not verified")
return res, fmt.Errorf(`phone number is not verified`)
if !isSMSServiceEnabled {
log.Debug("User phone number is not verified")
return res, fmt.Errorf(`phone number is not verified and sms service is not enabled`)
} else {
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err := generateOTP(expiresAt)
if err != nil {
log.Debug("Failed to generate otp: ", err)
return nil, err
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
go func() {
smsBody := strings.Builder{}
smsBody.WriteString("Your verification code is: ")
smsBody.WriteString(otpData.Otp)
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
if err := smsproviders.SendSMS(phoneNumber, smsBody.String()); err != nil {
log.Debug("Failed to send sms: ", err)
}
}()
return &model.AuthResponse{
Message: "Please check text message for the OTP",
ShouldShowMobileOtpScreen: refs.NewBoolRef(isMobileLogin),
}, nil
}
}
}
err = bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(params.Password))
@@ -129,14 +218,6 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
if params.Scope != nil && len(scope) > 0 {
scope = params.Scope
}
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
}
isSMSServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsSMSServiceEnabled)
if err != nil || !isSMSServiceEnabled {
log.Debug("SMS service not enabled: ", err)
}
isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil || !isMFADisabled {
@@ -157,44 +238,20 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
if err != nil || !isSMSOTPDisabled {
log.Debug("sms OTP service not enabled: ", err)
}
setOTPMFaSession := func(expiresAt int64) error {
mfaSession := uuid.NewString()
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt)
if err != nil {
log.Debug("Failed to add mfasession: ", err)
return err
}
cookie.SetMfaSession(gc, mfaSession)
return nil
}
// If multi factor authentication is enabled and we need to generate OTP for mail / sms based MFA
generateOTP := func(expiresAt int64) (*models.OTP, error) {
otp := utils.GenerateOTP()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: refs.StringValue(user.Email),
PhoneNumber: refs.StringValue(user.PhoneNumber),
Otp: otp,
ExpiresAt: expiresAt,
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
return otpData, nil
}
// If multi factor authentication is enabled and is email based login and email otp is enabled
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isMailOTPDisabled && isEmailServiceEnabled && isEmailLogin {
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err := generateOTP(expiresAt)
if err != nil {
log.Debug("Failed to generate otp: ", err)
return nil, err
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
go func() {
if err != nil {
log.Debug("Failed to generate otp: ", err)
return
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return
}
// exec it as go routine so that we can reduce the api latency
if err := mailService.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
@@ -214,15 +271,15 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isSMSOTPDisabled && isSMSServiceEnabled && isMobileLogin {
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err := generateOTP(expiresAt)
if err != nil {
log.Debug("Failed to generate otp: ", err)
return nil, err
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
go func() {
if err != nil {
log.Debug("Failed to generate otp: ", err)
return
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return
}
smsBody := strings.Builder{}
smsBody.WriteString("Your verification code is: ")
smsBody.WriteString(otpData.Otp)

View File

@@ -7,12 +7,14 @@ import (
"strings"
"time"
"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/db"
"github.com/authorizerdev/authorizer/server/db/models"
emailHelper "github.com/authorizerdev/authorizer/server/email"
mailService "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"
@@ -32,44 +34,42 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod
log.Debug("Email or phone number is required")
return nil, errors.New("email or phone number is required")
}
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
log.Debug("Failed to get GinContext: ", err)
return nil, err
}
var user *models.User
var err error
var isEmailServiceEnabled, isSMSServiceEnabled bool
if email != "" {
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
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")
}
user, err = db.Provider.GetUserByEmail(ctx, email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
return nil, fmt.Errorf(`user with this email/phone not found`)
}
} else {
isSMSServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
isSMSServiceEnabled, err = memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isSMSServiceEnabled {
log.Debug("Email service not enabled: ", err)
return nil, errors.New("email service not enabled")
}
user, err = db.Provider.GetUserByPhoneNumber(ctx, phoneNumber)
if err != nil {
log.Debug("Failed to get user by phone: ", err)
return nil, fmt.Errorf(`user with this email/phone not found`)
}
}
if err != nil {
log.Debug("Failed to get user by email: ", err)
return nil, fmt.Errorf(`user with this email/phone not found`)
}
if user.RevokedTimestamp != nil {
log.Debug("User access is revoked")
return nil, fmt.Errorf(`user access has been revoked`)
}
if email != "" && user.EmailVerifiedAt == nil {
log.Debug("User email is not verified")
return nil, fmt.Errorf(`email not verified`)
}
if phoneNumber != "" && user.PhoneNumberVerifiedAt == nil {
log.Debug("User phone number is not verified")
return nil, fmt.Errorf(`phone number not verified`)
}
if !refs.BoolValue(user.IsMultiFactorAuthEnabled) {
if !refs.BoolValue(user.IsMultiFactorAuthEnabled) && user.EmailVerifiedAt != nil && user.PhoneNumberVerifiedAt != nil {
log.Debug("User multi factor authentication is not enabled")
return nil, fmt.Errorf(`multi factor authentication not enabled`)
}
@@ -97,30 +97,63 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod
Message: "Failed to get for given email",
}, errors.New("failed to get otp for given email")
}
otp := utils.GenerateOTP()
if _, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: refs.StringValue(user.Email),
Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
}); err != nil {
log.Debug("Error upserting otp: ", err)
// If multi factor authentication is enabled and we need to generate OTP for mail / sms based MFA
generateOTP := func(expiresAt int64) (*models.OTP, error) {
otp := utils.GenerateOTP()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: refs.StringValue(user.Email),
PhoneNumber: refs.StringValue(user.PhoneNumber),
Otp: otp,
ExpiresAt: expiresAt,
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
return otpData, nil
}
setOTPMFaSession := func(expiresAt int64) error {
mfaSession := uuid.NewString()
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt)
if err != nil {
log.Debug("Failed to add mfasession: ", err)
return err
}
cookie.SetMfaSession(gc, mfaSession)
return nil
}
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err = generateOTP(expiresAt)
if err != nil {
log.Debug("Failed to generate otp: ", err)
return nil, err
}
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
if email != "" {
// exec it as go routine so that we can reduce the api latency
go emailHelper.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"otp": otp,
})
go func() {
// exec it as go routine so that we can reduce the api latency
if err := mailService.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]interface{}{
"user": user.ToMap(),
"organization": utils.GetOrganization(),
"otp": otpData.Otp,
}); err != nil {
log.Debug("Failed to send otp email: ", err)
}
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
}()
} else {
smsBody := strings.Builder{}
smsBody.WriteString("Your verification code is: ")
smsBody.WriteString(otp)
// exec it as go routine so that we can reduce the api latency
go smsproviders.SendSMS(phoneNumber, smsBody.String())
go func() {
smsBody := strings.Builder{}
smsBody.WriteString("Your verification code is: ")
smsBody.WriteString(otpData.Otp)
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
if err := smsproviders.SendSMS(phoneNumber, smsBody.String()); err != nil {
log.Debug("Failed to send sms: ", err)
}
}()
}
log.Info("OTP has been resent")
return &model.Response{

View File

@@ -290,25 +290,26 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
} else if !disablePhoneVerification && isSMSServiceEnabled && isMobileSignup {
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
}
expiresAt := time.Now().Add(duration).Unix()
_, err = db.Provider.UpsertOTP(ctx, &models.OTP{
PhoneNumber: phoneNumber,
Otp: smsCode,
ExpiresAt: time.Now().Add(duration).Unix(),
ExpiresAt: expiresAt,
})
if err != nil {
log.Debug("error while upserting OTP: ", err.Error())
return nil, err
}
mfaSession := uuid.NewString()
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt)
if err != nil {
log.Debug("Failed to add mfasession: ", err)
return nil, err
}
cookie.SetMfaSession(gc, mfaSession)
go func() {
smsproviders.SendSMS(phoneNumber, smsBody.String())
utils.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)

View File

@@ -48,7 +48,18 @@ 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 && params.IsMultiFactorAuthEnabled == nil && params.AppData == 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 &&
params.AppData == nil {
log.Debug("No params to update")
return res, fmt.Errorf("please enter atleast one param to update")
}
@@ -142,6 +153,15 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
user.EmailVerifiedAt = nil
}
}
if params.PhoneNumberVerified != nil {
if *params.PhoneNumberVerified {
now := time.Now().Unix()
user.PhoneNumberVerifiedAt = &now
} else {
user.PhoneNumberVerifiedAt = nil
}
}
if params.Email != nil && refs.StringValue(user.Email) != refs.StringValue(params.Email) {
// check if valid email
@@ -197,6 +217,24 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
}
if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(params.PhoneNumber) {
phone := strings.TrimSpace(refs.StringValue(params.PhoneNumber))
if len(phone) < 10 || len(phone) > 15 {
log.Debug("Invalid phone number: ", *params.PhoneNumber)
return res, fmt.Errorf("invalid phone number")
}
// check if user with new phone number exists
_, err = db.Provider.GetUserByPhoneNumber(ctx, phone)
// err = nil means user exists
if err == nil {
log.Debug("User with phone number already exists: ", phone)
return res, fmt.Errorf("user with this phone number already exists")
}
go memorystore.Provider.DeleteAllUserSessions(user.ID)
user.PhoneNumber = &phone
user.PhoneNumberVerifiedAt = nil
}
rolesToSave := ""
if params.Roles != nil && len(params.Roles) > 0 {
currentRoles := strings.Split(user.Roles, ",")
@@ -237,7 +275,6 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
if rolesToSave != "" {
user.Roles = rolesToSave
}
user, err = db.Provider.UpdateUser(ctx, user)
if err != nil {
log.Debug("Failed to update user: ", err)