diff --git a/server/authenticators/providers/providers.go b/server/authenticators/providers/providers.go index 7f43ef5c7..9c11d49c6 100644 --- a/server/authenticators/providers/providers.go +++ b/server/authenticators/providers/providers.go @@ -1,6 +1,8 @@ package providers -import "context" +import ( + "context" +) // AuthenticatorConfig defines authenticator config type AuthenticatorConfig struct { @@ -22,4 +24,6 @@ type Provider interface { Validate(ctx context.Context, passcode string, userID string) (bool, error) // ValidateRecoveryCode totp: allows user to validate using recovery code incase if they lost their device ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) + // UpdateTotpInfo: to update secret and recovery codes into db and returns base64 of QR code image + UpdateTotpInfo(ctx context.Context, id string) (*AuthenticatorConfig, error) } diff --git a/server/authenticators/providers/totp/totp.go b/server/authenticators/providers/totp/totp.go index b02fe293e..2d2ce99d8 100644 --- a/server/authenticators/providers/totp/totp.go +++ b/server/authenticators/providers/totp/totp.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/pquerna/otp/totp" + log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/authenticators/providers" @@ -18,6 +19,7 @@ import ( "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/refs" + "github.com/authorizerdev/authorizer/server/utils" ) // Generate generates a Time-Based One-Time Password (TOTP) for a user and returns the base64-encoded QR code for frontend display. @@ -149,3 +151,60 @@ func (p *provider) ValidateRecoveryCode(ctx context.Context, recoveryCode, userI } return true, nil } + +// UpdateTotpInfo generates a Time-Based One-Time Password (TOTP) for a user, +// updates the user's authenticator details, and returns the base64-encoded QR code for frontend display. +func (p *provider) UpdateTotpInfo(ctx context.Context, id string) (*providers.AuthenticatorConfig, error) { + // Buffer to store the base64-encoded QR code image + var buf bytes.Buffer + + // Retrieve user details from the database + user, err := db.Provider.GetUserByID(ctx, id) + if err != nil { + 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, err + } + + // Generate image for the TOTP key and encode it to base64 for frontend display + img, err := key.Image(200, 200) + if err != nil { + return nil, err + } + + // Encode the QR code image to base64 + png.Encode(&buf, img) + encodedText := crypto.EncryptB64(buf.String()) + + // Update the authenticator record with the new TOTP secret + secret := key.Secret() + + // Retrieve an authenticator details for the user + authenticator, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator) + if err != nil { + log.Debug("Failed to get authenticator details by user id, creating new record: ", err) + return nil, err + } + + // Update the authenticator record with the new TOTP secret + authenticator.Secret = secret + + // Update the authenticator record in the database + _, err = db.Provider.UpdateAuthenticator(ctx, authenticator) + if err != nil { + return nil, err + } + + // Return the response with base64-encoded QR code, TOTP secret, and recovery codes + return &providers.AuthenticatorConfig{ + ScannerImage: encodedText, + Secret: secret, + RecoveryCodes: utils.ParseReferenceStringArray(authenticator.RecoveryCodes), + }, nil +} diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index 16a10c707..66f3b09e2 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -80,6 +80,85 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod log.Debug("Failed to verify otp request: Incorrect value") return res, fmt.Errorf(`invalid otp`) } + + // Redirect to TOTP scanner image screen when the user validates through a recovery code + { + // Update totp info into db + { + // Get TOTP details for the user + totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator) + if err != nil { + return nil, err + } + + // Clear TOTP secret from the TOTP model + totpModel.Secret = "" + + // Reset recovery code and TOTP secret in the database + _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) + if err != nil { + return nil, err + } + } + + // Redirect to TOTP scanner image screen by resetting TOTP secret and updating a recovery codes + { + // Function to set OTP MFA session + setOTPMFaSession := func(expiresAt int64) error { + // Generate a new MFA session ID + mfaSession := uuid.NewString() + + // Store the MFA session in the memory store + err = memorystore.Provider.SetMfaSession(user.ID, mfaSession, expiresAt) + if err != nil { + log.Debug("Failed to add mfasession: ", err) + return err + } + + // Set the MFA session ID in a cookie + cookie.SetMfaSession(gc, mfaSession) + return nil + } + + // Calculate the expiration time for the TOTP information + expiresAt := time.Now().Add(3 * time.Minute).Unix() + + // Set the OTP MFA session + if err := setOTPMFaSession(expiresAt); err != nil { + log.Debug("Failed to set mfa session: ", err) + return nil, err + } + + // Retrieve TOTP details again after updating the session + authenticator, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, user.ID, constants.EnvKeyTOTPAuthenticator) + + // Check for an error or an empty TOTP secret in the authenticator details + if err != nil || authenticator.Secret == "" { + // If there's an error or the TOTP secret is empty, initiate TOTP information update + authConfig, err := authenticators.Provider.UpdateTotpInfo(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)) + } + + // Response for the case when the user validate through TOTP recovery codes + res = &model.AuthResponse{ + Message: `Proceed to totp verification screen`, + ShouldShowTotpScreen: refs.NewBoolRef(true), + AuthenticatorScannerImage: &authConfig.ScannerImage, + AuthenticatorSecret: &authConfig.Secret, + AuthenticatorRecoveryCodes: recoveryCodes, + } + + return res, nil + } + } + } } } else { var otp *models.OTP diff --git a/server/utils/parser.go b/server/utils/parser.go index 1b037c097..a7dbd5043 100644 --- a/server/utils/parser.go +++ b/server/utils/parser.go @@ -2,6 +2,7 @@ package utils import ( "errors" + "strings" "time" ) @@ -19,3 +20,48 @@ func ParseDurationInSeconds(s string) (time.Duration, error) { return d, nil } + +// Helper function to parse string array values +func ParseStringArray(value string) []*string { + if value == "" { + return nil + } + splitValues := strings.Split(value, "|") + + var result []*string + for _, s := range splitValues { + temp := s + result = append(result, &temp) + } + + return result +} + +// Helper function to parse reference string array values +func ParseReferenceStringArray(value *string) []string { + if value == nil { + return nil + } + + // Dereference the pointer to get the string value + strValue := *value + + // Remove JSON brackets + strValue = strings.Trim(strValue, "{}") + + splitValues := strings.Split(strValue, ",") + + var result []string + for _, s := range splitValues { + // Split each key-value pair by colon ':' + parts := strings.SplitN(s, ":", 2) + if len(parts) > 0 { + unquoted := strings.Trim(strings.TrimSpace(parts[0]), `"`) + + // Extract and append only the key (UUID) + result = append(result, unquoted) + } + } + + return result +}