Skip to content
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
}