Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/totp reset after recovery code validation #445

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion server/authenticators/providers/providers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package providers

import "context"
import (
"context"
)

// AuthenticatorConfig defines authenticator config
type AuthenticatorConfig struct {
Expand All @@ -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)
}
59 changes: 59 additions & 0 deletions server/authenticators/providers/totp/totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
79 changes: 79 additions & 0 deletions server/resolvers/verify_otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions server/utils/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"errors"
"strings"
"time"
)

Expand All @@ -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
}