diff --git a/server/authenticators/providers/providers.go b/server/authenticators/providers/providers.go index 9280397..60c0f79 100644 --- a/server/authenticators/providers/providers.go +++ b/server/authenticators/providers/providers.go @@ -8,8 +8,10 @@ type AuthenticatorConfig struct { ScannerImage string // Secrets is the secret key Secret string - // RecoveryCode is the secret key + // RecoveryCode is the list of recovery codes RecoveryCodes []string + // RecoveryCodeMap is the map of recovery codes + RecoveryCodeMap map[string]bool } // Provider defines authenticators provider diff --git a/server/authenticators/providers/totp/totp.go b/server/authenticators/providers/totp/totp.go index 3a48af7..1a28f87 100644 --- a/server/authenticators/providers/totp/totp.go +++ b/server/authenticators/providers/totp/totp.go @@ -4,12 +4,12 @@ import ( "bytes" "context" "encoding/json" - "fmt" "image/png" "time" "github.com/google/uuid" "github.com/pquerna/otp/totp" + log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/authenticators/providers" "github.com/authorizerdev/authorizer/server/constants" @@ -22,30 +22,26 @@ import ( // Generate generates a Time-Based One-Time Password (TOTP) for a user and returns the base64-encoded QR code for frontend display. func (p *provider) Generate(ctx context.Context, id string) (*providers.AuthenticatorConfig, error) { var buf bytes.Buffer - //get user details user, err := db.Provider.GetUserByID(ctx, id) if err != nil { - return nil, fmt.Errorf("error while getting user details") + return nil, err } - // generate totp, Authenticators hash is valid for 30 seconds key, err := totp.Generate(totp.GenerateOpts{ Issuer: "authorizer", AccountName: refs.StringValue(user.Email), }) if err != nil { - return nil, fmt.Errorf("error while genrating totp") + return nil, err } - //generating image for key and encoding to base64 for displaying in frontend img, err := key.Image(200, 200) if err != nil { - return nil, fmt.Errorf("error while creating qr image for totp") + return nil, err } png.Encode(&buf, img) encodedText := crypto.EncryptB64(buf.String()) - secret := key.Secret() recoveryCodes := []string{} for i := 0; i < 10; i++ { @@ -59,24 +55,40 @@ func (p *provider) Generate(ctx context.Context, id string) (*providers.Authenti // Converting recoveryCodesMap to string jsonData, err := json.Marshal(recoverCodesMap) if err != nil { - return nil, fmt.Errorf("error while converting recoveryCodes to string") + return nil, err } recoveryCodesString := string(jsonData) - totpModel := &models.Authenticator{ Secret: secret, RecoveryCodes: refs.NewStringRef(recoveryCodesString), UserID: user.ID, Method: constants.EnvKeyTOTPAuthenticator, } - _, err = db.Provider.AddAuthenticator(ctx, totpModel) + authenticator, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator) if err != nil { - return nil, fmt.Errorf("error while inserting into totp table") + log.Debug("Failed to get authenticator details by user id, creating new record: ", err) + // continue + } + if authenticator == nil { + // if authenticator is nil then create new authenticator + _, err = db.Provider.AddAuthenticator(ctx, totpModel) + if err != nil { + return nil, err + } + } else { + authenticator.Secret = secret + authenticator.RecoveryCodes = refs.NewStringRef(recoveryCodesString) + // if authenticator is not nil then update authenticator + _, err = db.Provider.UpdateAuthenticator(ctx, authenticator) + if err != nil { + return nil, err + } } return &providers.AuthenticatorConfig{ - ScannerImage: encodedText, - Secret: secret, - RecoveryCodes: recoveryCodes, + ScannerImage: encodedText, + Secret: secret, + RecoveryCodes: recoveryCodes, + RecoveryCodeMap: recoverCodesMap, }, nil } @@ -85,22 +97,18 @@ func (p *provider) Validate(ctx context.Context, passcode string, userID string) // get totp details totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, userID, constants.EnvKeyTOTPAuthenticator) if err != nil { - return false, fmt.Errorf("error while getting totp details from authenticators") + return false, err } - + // validate totp status := totp.Validate(passcode, totpModel.Secret) // checks if user not signed in for totp and totp code is correct then VerifiedAt will be stored in db - if totpModel.VerifiedAt == nil { - if status { - timeNow := time.Now().Unix() - totpModel.VerifiedAt = &timeNow - _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) - if err != nil { - return false, fmt.Errorf("error while updaing authenticator table for totp") - } - return status, nil + if totpModel.VerifiedAt == nil && status { + timeNow := time.Now().Unix() + totpModel.VerifiedAt = &timeNow + _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) + if err != nil { + return false, err } - return status, nil } return status, nil } diff --git a/server/db/providers/sql/authenticator.go b/server/db/providers/sql/authenticator.go index d83b537..b8cf797 100644 --- a/server/db/providers/sql/authenticator.go +++ b/server/db/providers/sql/authenticator.go @@ -35,13 +35,10 @@ func (p *provider) AddAuthenticator(ctx context.Context, authenticators *models. func (p *provider) UpdateAuthenticator(ctx context.Context, authenticators *models.Authenticator) (*models.Authenticator, error) { authenticators.UpdatedAt = time.Now().Unix() - result := p.db.Save(&authenticators) - if result.Error != nil { return authenticators, result.Error } - return authenticators, nil } diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 47b3cc6..708e3b4 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -244,8 +244,8 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return nil, err } 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)) { + if err != nil || authenticator == nil || authenticator.VerifiedAt == nil { + // generate totp // Generate a base64 URL and initiate the registration for TOTP authConfig, err := authenticators.Provider.Generate(ctx, user.ID) if err != nil { diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 00e340a..a0eeb13 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -58,10 +58,14 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod // 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 { + if err != nil { log.Debug("Failed to validate totp: ", err) return nil, fmt.Errorf("error while validating passcode") } + if !status { + log.Debug("Failed to verify otp request: Incorrect value") + return res, fmt.Errorf(`invalid otp`) + } } else { var otp *models.OTP if currentField == models.FieldNameEmail {