feat: add totp login API (#416)

* fix:
* removed hasReversedValue in playground

* feat:
* added totp methods in db's providers
* adding totp in login method

* feat:
* added toggle in dashboard
* fixing issue with env set

* feat:
* integrated totp

* feat:
* encrypted userid
* added totp_verified column in user table
* started test for totp

* feat:
* test cases totp

* test-cases:
* completed test cases
* tested for all dbs

* fixes:
* return variable to snake case
* import refactoring

* feat:
* created seperate folder for authenticator with totp subfolder
* refactored code
* created new table for authenticators
* added recovery code for totp

* feat:
* adding functions to different db providers

* feat:
* added authenticators method for all db

* feat:
* added logic for updating mfa in user_profile update

* fix:
* merge conflict

* fix:
* resolved mongodb, dynamodb and arangodb test case bug
* added new condition for checking first time totp user or not

* feat:
* changes in all respective db with authenticator

* fix:
* PR suggested changes

* fix(cassandra): list users

* Update verify otp

* fix totp login api

---------

Co-authored-by: lemonScaletech <anand.panigrahi@scaletech.xyz>
This commit is contained in:
Lakhan Samani
2023-11-16 18:30:54 +05:30
committed by GitHub
parent d8cd965004
commit fe4c693324
57 changed files with 4321 additions and 1111 deletions

View File

@@ -203,6 +203,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res.AdminCookieSecure = store[constants.EnvKeyAdminCookieSecure].(bool)
res.AppCookieSecure = store[constants.EnvKeyAppCookieSecure].(bool)
res.DisablePlayground = store[constants.EnvKeyDisablePlayGround].(bool)
res.DisableMailOtpLogin = store[constants.EnvKeyDisableMailOTPLogin].(bool)
res.DisableTotpLogin = store[constants.EnvKeyDisableTOTPLogin].(bool)
return res, nil
}

View File

@@ -7,14 +7,15 @@ import (
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/authenticators"
"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"
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"
@@ -22,6 +23,8 @@ import (
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
mailService "github.com/authorizerdev/authorizer/server/email"
)
// LoginResolver is a resolver for login mutation
@@ -141,52 +144,134 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
log.Debug("MFA service not enabled: ", err)
}
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled {
isTOTPLoginDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
if err != nil || !isTOTPLoginDisabled {
log.Debug("totp service not enabled: ", err)
}
isMailOTPDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
if err != nil || !isMailOTPDisabled {
log.Debug("mail OTP service not enabled: ", err)
}
isSMSOTPDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
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()
expires := time.Now().Add(1 * time.Minute).Unix()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: refs.StringValue(user.Email),
Otp: otp,
ExpiresAt: expires,
ExpiresAt: expiresAt,
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
mfaSession := uuid.NewString()
err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expires)
if err != nil {
log.Debug("Failed to add mfasession: ", err)
return otpData, nil
}
// If mfa enabled and also totp enabled
// first priority is given to totp
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isTOTPLoginDisabled {
expiresAt := time.Now().Add(3 * time.Minute).Unix()
if err := setOTPMFaSession(expiresAt); err != nil {
log.Debug("Failed to set mfa session: ", err)
return nil, err
}
cookie.SetMfaSession(gc, mfaSession)
if isEmailServiceEnabled && isEmailLogin {
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 if isSMSServiceEnabled && isMobileLogin {
authenticator, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator)
// Check if it's the first time user or if their TOTP is not verified
if err != nil || ((authenticator == nil) || (authenticator != nil && authenticator.VerifiedAt == nil)) {
// Generate a base64 URL and initiate the registration for TOTP
authConfig, err := authenticators.Provider.Generate(ctx, user.ID)
if err != nil {
log.Debug("error while generating base64 url: ", err)
return nil, err
}
recoveryCodes := []*string{}
for _, code := range authConfig.RecoveryCodes {
recoveryCodes = append(recoveryCodes, refs.NewStringRef(code))
}
// when user is first time registering for totp
res = &model.AuthResponse{
Message: `Proceed to totp verification screen`,
ShouldShowTotpScreen: refs.NewBoolRef(true),
AuthenticatorScannerImage: refs.NewStringRef(authConfig.ScannerImage),
AuthenticatorSecret: refs.NewStringRef(authConfig.Secret),
AuthenticatorRecoveryCodes: recoveryCodes,
}
return res, nil
} else {
//when user is already register for totp
res = &model.AuthResponse{
Message: `Proceed to totp screen`,
ShouldShowTotpScreen: refs.NewBoolRef(true),
}
return res, 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)
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(),
"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
}
// If multi factor authentication is enabled and is sms based login and sms otp is enabled
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMFADisabled && !isSMSOTPDisabled && isSMSServiceEnabled && isMobileLogin {
expiresAt := time.Now().Add(1 * time.Minute).Unix()
otpData, err := generateOTP(expiresAt)
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)
go func() {
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user)
if err := smsproviders.SendSMS(phoneNumber, smsBody.String()); err != nil {
log.Debug("Failed to send sms: ", err)
}
}()
}
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 the OTP in",
ShouldShowEmailOtpScreen: refs.NewBoolRef(isEmailLogin),
Message: "Please check text message for the OTP",
ShouldShowMobileOtpScreen: refs.NewBoolRef(isMobileLogin),
}, nil
}

View File

@@ -253,13 +253,15 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// in case SMTP is off but env is set to true
if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true
}
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) {
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
}
if !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) {
updatedData[constants.EnvKeyDisableMagicLinkLogin] = true
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
}
}
@@ -274,6 +276,21 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
}
}
if updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
updatedData[constants.EnvKeyDisableTOTPLogin] = true
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
} else {
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && !updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
errors.New("can't enable both mfa methods at same time")
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
} else if updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
errors.New("can't disable both mfa methods at same time")
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
}
}
if !currentData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && updatedData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
go db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": true,

View File

@@ -8,6 +8,8 @@ import (
"strings"
"time"
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
@@ -23,7 +25,6 @@ import (
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators"
"golang.org/x/crypto/bcrypt"
)
// UpdateProfileResolver is resolver for update profile mutation
@@ -101,13 +102,28 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
appDataString = string(appDataBytes)
user.AppData = &appDataString
}
// Check if the user is trying to enable or disable multi-factor authentication (MFA)
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
}
// Check if totp, email or sms is enabled
isMailOTPEnvServiceDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
if err != nil {
log.Debug("Error getting mail otp disabled: ", err)
isMailOTPEnvServiceDisabled = false
}
isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
if err != nil {
log.Debug("Error getting totp disabled: ", err)
isTOTPEnvServiceDisabled = false
}
isSMSOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
if err != nil {
log.Debug("Error getting sms otp disabled: ", err)
isSMSOTPEnvServiceDisabled = false
}
// Initialize a flag to check if enabling Mail OTP is required
if isMailOTPEnvServiceDisabled && isTOTPEnvServiceDisabled && isSMSOTPEnvServiceDisabled {
log.Debug("Cannot enable mfa service as all mfa services are disabled")
return nil, errors.New("cannot enable multi factor authentication as all mfa services are disabled")
}
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)

View File

@@ -110,10 +110,26 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
// Check if totp, email or sms is enabled
isMailOTPEnvServiceDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
if err != nil {
log.Debug("Error getting mail otp disabled: ", err)
isMailOTPEnvServiceDisabled = false
}
isTOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
if err != nil {
log.Debug("Error getting totp disabled: ", err)
isTOTPEnvServiceDisabled = false
}
isSMSOTPEnvServiceDisabled, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification)
if err != nil {
log.Debug("Error getting sms otp disabled: ", err)
isSMSOTPEnvServiceDisabled = false
}
// Initialize a flag to check if enabling Mail OTP is required
if isMailOTPEnvServiceDisabled && isTOTPEnvServiceDisabled && isSMSOTPEnvServiceDisabled {
log.Debug("Cannot enable mfa service as all mfa services are disabled")
return nil, errors.New("cannot enable multi factor authentication as all mfa services are disabled")
}
}
}

View File

@@ -6,6 +6,10 @@ import (
"strings"
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/authenticators"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db"
@@ -15,8 +19,6 @@ import (
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
// VerifyOtpResolver resolver for verify otp mutation
@@ -38,30 +40,11 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
log.Debug("Email or phone number is required")
return res, fmt.Errorf(`email or phone_number is required`)
}
currentField := models.FieldNameEmail
if refs.StringValue(params.Email) == "" {
currentField = models.FieldNamePhoneNumber
}
var otp *models.OTP
if currentField == models.FieldNameEmail {
otp, err = db.Provider.GetOTPByEmail(ctx, refs.StringValue(params.Email))
} else {
otp, err = db.Provider.GetOTPByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber))
}
if otp == nil && err != nil {
log.Debugf("Failed to get otp request for %s: %s", currentField, err.Error())
return res, fmt.Errorf(`invalid %s: %s`, currentField, err.Error())
}
if params.Otp != otp.Otp {
log.Debug("Failed to verify otp request: Incorrect value")
return res, fmt.Errorf(`invalid otp`)
}
expiresIn := otp.ExpiresAt - time.Now().Unix()
if expiresIn < 0 {
log.Debug("Failed to verify otp request: Timeout")
return res, fmt.Errorf("otp expired")
}
// Get user by email or phone number
var user *models.User
if currentField == models.FieldNameEmail {
user, err = db.Provider.GetUserByEmail(ctx, refs.StringValue(params.Email))
@@ -72,6 +55,35 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
log.Debug("Failed to get user by email or phone number: ", err)
return res, err
}
// Verify OTP based on TOPT or OTP
if refs.BoolValue(params.Totp) {
status, err := authenticators.Provider.Validate(ctx, params.Otp, user.ID)
if err != nil || !status {
log.Debug("Failed to validate totp: ", err)
return nil, fmt.Errorf("error while validating passcode")
}
} else {
var otp *models.OTP
if currentField == models.FieldNameEmail {
otp, err = db.Provider.GetOTPByEmail(ctx, refs.StringValue(params.Email))
} else {
otp, err = db.Provider.GetOTPByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber))
}
if otp == nil && err != nil {
log.Debugf("Failed to get otp request for %s: %s", currentField, err.Error())
return res, fmt.Errorf(`invalid %s: %s`, currentField, err.Error())
}
if params.Otp != otp.Otp {
log.Debug("Failed to verify otp request: Incorrect value")
return res, fmt.Errorf(`invalid otp`)
}
expiresIn := otp.ExpiresAt - time.Now().Unix()
if expiresIn < 0 {
log.Debug("Failed to verify otp request: Timeout")
return res, fmt.Errorf("otp expired")
}
db.Provider.DeleteOTP(gc, otp)
}
if _, err := memorystore.Provider.GetMfaSession(user.ID, mfaSession); err != nil {
log.Debug("Failed to get mfa session: ", err)
@@ -121,7 +133,6 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
}
go func() {
db.Provider.DeleteOTP(gc, otp)
if isSignUp {
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)
// User is also logged in with signup