Merge remote-tracking branch 'hub/main' into discours

This commit is contained in:
2024-04-15 09:31:44 +03:00
60 changed files with 2099 additions and 1733 deletions

View File

@@ -205,6 +205,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
// bool vars
res.DisableEmailVerification = store[constants.EnvKeyDisableEmailVerification].(bool)
res.DisableBasicAuthentication = store[constants.EnvKeyDisableBasicAuthentication].(bool)
res.DisableMobileBasicAuthentication = store[constants.EnvKeyDisableMobileBasicAuthentication].(bool)
res.DisableMagicLinkLogin = store[constants.EnvKeyDisableMagicLinkLogin].(bool)
res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool)
res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool)

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 = crypto.VerifyPassword(*user.Password, 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)