twofactor

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT, MIT Imports: 23 Imported by: 0

README

GoModularAuth Two-Factor (MFA) Plugin

gomodularauth-twofactor provides enterprise-grade, strongly typed Multi-Factor Authentication (MFA) capabilities—including Time-based One-Time Passwords (TOTP / RFC 6238) and single-use Backup Recovery Codes—for GoModularAuth, without coupling the Core authentication model to any specific second factor.

Installation

go get github.com/BladiCreator/go-modular-auth/plugins/twofactor

Quick Start


Detailed guide: doc/plugins/twofactor.md.

Documentation

Overview

Package twofactor implements RFC 6238 TOTP two-factor authentication and recovery codes.

Index

Constants

View Source
const (
	// EventTwoFactorEnabled is emitted when a user successfully confirms enrollment of their first MFA factor.
	EventTwoFactorEnabled auth.EventName = "twofactor.enabled"

	// EventTwoFactorDisabled is emitted when MFA is disabled for a user.
	EventTwoFactorDisabled auth.EventName = "twofactor.disabled"

	// EventTwoFactorVerified is emitted when a user successfully verifies a secondary factor (TOTP or recovery code).
	EventTwoFactorVerified auth.EventName = "twofactor.verified"
)

Event names emitted by the twofactor plugin.

View Source
const (
	ModelTwoFactors    = "twofactor_factors"
	ModelRecoveryCodes = "twofactor_recovery_codes"
	ModelMFAChallenges = "twofactor_challenges"
)
View Source
const PluginID = "twofactor"

PluginID identifies this plugin in the GoModularAuth runtime.

Variables

View Source
var (
	// ErrInvalidInput is returned when required parameters (userID, factorID, code, etc.) are empty or malformed.
	ErrInvalidInput = errors.New("twofactor: invalid input")

	// ErrFactorNotFound is returned when the requested MFA factor does not exist.
	ErrFactorNotFound = errors.New("twofactor: factor not found")

	// ErrFactorAlreadyExists is returned when attempting to enroll an already existing factor.
	ErrFactorAlreadyExists = errors.New("twofactor: factor already exists")

	// ErrFactorAlreadyConfirmed is returned when attempting to confirm an already confirmed factor.
	ErrFactorAlreadyConfirmed = errors.New("twofactor: factor already confirmed")

	// ErrFactorNotConfirmed is returned when attempting to authenticate using an unconfirmed factor.
	ErrFactorNotConfirmed = errors.New("twofactor: factor is not confirmed")

	// ErrInvalidCode is returned when an OTP or recovery code fails verification.
	ErrInvalidCode = errors.New("twofactor: invalid authentication code")

	// ErrReplayDetected is returned when an OTP code has already been consumed for the current or a previous time-step.
	ErrReplayDetected = errors.New("twofactor: replay detected; code already used")

	// ErrChallengeNotFound is returned when an MFA challenge token does not exist.
	ErrChallengeNotFound = errors.New("twofactor: challenge not found")

	// ErrChallengeExpired is returned when an MFA challenge token has passed its expiration time.
	ErrChallengeExpired = errors.New("twofactor: challenge expired")

	// ErrChallengeAlreadyUsed is returned when attempting to verify an already consumed challenge.
	ErrChallengeAlreadyUsed = errors.New("twofactor: challenge already used")

	// ErrNoActiveRecoveryCodes is returned when no active recovery codes remain for the user.
	ErrNoActiveRecoveryCodes = errors.New("twofactor: no active recovery codes available")

	// ErrRecoveryCodeNotFound is returned when the supplied recovery code does not match any active record.
	ErrRecoveryCodeNotFound = errors.New("twofactor: recovery code not found")

	// ErrAssuranceLevelInsufficient is returned when an operation requires AAL2 but the context only holds AAL1.
	ErrAssuranceLevelInsufficient = errors.New("twofactor: assurance level insufficient; AAL2 required")

	// ErrTwoFactorNotEnabled is returned when an operation requires 2FA to be enabled for the user.
	ErrTwoFactorNotEnabled = errors.New("twofactor: two-factor authentication is not enabled")

	// ErrCryptoOperationFailed is returned when secret encryption or decryption fails.
	ErrCryptoOperationFailed = errors.New("twofactor: cryptographic operation failed")
)

Functions

func BuildProvisioningURI added in v1.3.0

func BuildProvisioningURI(issuer, accountName, secret string) string

BuildProvisioningURI constructs a standard otpauth://totp URI for authenticator applications.

func CalculateTOTP added in v1.3.0

func CalculateTOTP(secret string, step int64) (string, error)

CalculateTOTP generates a 6-digit TOTP code for a base32-encoded secret at a specific time step (RFC 6238 / RFC 4226).

func GenerateRandomID added in v1.3.0

func GenerateRandomID(bytesLen int) (string, error)

GenerateRandomID generates a cryptographically secure hex ID.

func GenerateRandomKey added in v1.3.0

func GenerateRandomKey() ([]byte, error)

GenerateRandomKey generates a cryptographically secure 32-byte key for AES-256.

func GenerateRecoveryCode added in v1.3.0

func GenerateRecoveryCode() (string, error)

GenerateRecoveryCode generates a formatted backup recovery code (e.g. "XXXX-XXXX-XXXX").

func GenerateTOTPSecret added in v1.3.0

func GenerateTOTPSecret() (string, error)

GenerateTOTPSecret creates a 20-byte cryptographically secure secret, base32 encoded without padding.

func HashRecoveryCode added in v1.3.0

func HashRecoveryCode(code string) string

HashRecoveryCode computes the SHA-256 hash of a normalized recovery code.

func NormalizeRecoveryCode added in v1.3.0

func NormalizeRecoveryCode(code string) string

NormalizeRecoveryCode cleans up user input by uppercasing and stripping hyphens and whitespaces.

func RegisterSchema added in v1.3.0

func RegisterSchema(s *schema.Schema) error

RegisterSchema registers all two-factor authentication models into the compiled schema.

func RequireAAL2 added in v1.3.0

func RequireAAL2(ctx context.Context) error

RequireAAL2 checks whether the context has achieved AssuranceLevel 2. Returns ErrAssuranceLevelInsufficient if not.

func ValidateTOTP added in v1.3.0

func ValidateTOTP(secret string, code string, now time.Time, stepDuration time.Duration, skewSteps int) (int64, bool)

ValidateTOTP validates a 6-digit code against a secret within the allowed clock skew window. Returns the matched time step if valid, or -1 if invalid.

func WithAssuranceLevel added in v1.3.0

func WithAssuranceLevel(ctx context.Context, level AssuranceLevel) context.Context

WithAssuranceLevel returns a new context bearing the specified AssuranceLevel.

Types

type AESGCMProtector added in v1.3.0

type AESGCMProtector struct {
	// contains filtered or unexported fields
}

AESGCMProtector implements SecretProtector using AES-256-GCM.

func NewAESGCMProtector added in v1.3.0

func NewAESGCMProtector(key []byte) (*AESGCMProtector, error)

NewAESGCMProtector creates a new protector from a 32-byte master key.

func (*AESGCMProtector) Decrypt added in v1.3.0

func (p *AESGCMProtector) Decrypt(ciphertext string) ([]byte, error)

Decrypt decrypts a hex-encoded AES-GCM ciphertext.

func (*AESGCMProtector) Encrypt added in v1.3.0

func (p *AESGCMProtector) Encrypt(plaintext []byte) (string, error)

Encrypt encrypts plaintext using AES-GCM with a random 12-byte nonce.

type AssuranceLevel added in v1.3.0

type AssuranceLevel string

AssuranceLevel classifies the level of confidence in the authenticated identity (NIST SP 800-63B).

const (
	// AAL1 represents single-factor authentication (e.g. password, magic link, email OTP alone).
	AAL1 AssuranceLevel = "aal1"

	// AAL2 represents multi-factor authentication (primary factor plus a confirmed secondary factor).
	AAL2 AssuranceLevel = "aal2"
)

func AssuranceLevelFrom added in v1.3.0

func AssuranceLevelFrom(ctx context.Context) (AssuranceLevel, bool)

AssuranceLevelFrom retrieves the AssuranceLevel from the context, defaulting to AAL1 if absent.

type Challenge added in v1.3.0

type Challenge struct {
	ID        string    `json:"id"`
	UserID    string    `json:"user_id"`
	ExpiresAt time.Time `json:"expires_at"`
	CreatedAt time.Time `json:"created_at"`
}

Challenge represents an ephemeral MFA challenge issued when a user completes primary authentication and must satisfy a second factor challenge to gain full session privileges.

type ChallengeRecord added in v0.7.0

type ChallengeRecord struct {
	ID        string     `json:"id"`
	UserID    string     `json:"user_id"`
	ExpiresAt time.Time  `json:"expires_at"`
	UsedAt    *time.Time `json:"used_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

ChallengeRecord is the persistent fallback model for ephemeral MFA challenges.

type Config

type Config struct {
	// Protector encrypts and decrypts factor secrets at rest.
	// If nil, defaults to an AES-256-GCM protector initialized with a secure random key.
	Protector SecretProtector

	// Issuer is the default organization or application name included in otpauth:// URIs.
	Issuer string

	// StepDuration is the time interval for TOTP tokens (RFC 6238 recommends 30 seconds).
	StepDuration time.Duration

	// SkewSteps is the number of time-steps allowed before and after current time to tolerate clock drift.
	// Default is 1 (covering +/- 30 seconds).
	SkewSteps int

	// ChallengeTTL is the lifetime for ephemeral MFA challenges.
	ChallengeTTL time.Duration

	// DefaultRecoveryCodesCount is the number of recovery codes generated by default.
	DefaultRecoveryCodesCount int

	// EnforceMFAOnSignIn controls whether the plugin verifies that users with active 2FA
	// are not granted final sessions without completing MFA verification.
	EnforceMFAOnSignIn bool
}

Config holds the configuration options for the twofactor plugin module.

type EnrollConfig added in v1.3.0

type EnrollConfig struct {
	Label       string
	Issuer      string
	AccountName string
}

EnrollConfig holds optional parameters for TOTP enrollment.

type EnrollOption added in v1.3.0

type EnrollOption func(*EnrollConfig)

EnrollOption configures an enrollment request.

func WithEnrollAccountName added in v1.3.0

func WithEnrollAccountName(accountName string) EnrollOption

WithEnrollAccountName sets the account name in the otpauth URI (e.g. user's email).

func WithEnrollIssuer added in v1.3.0

func WithEnrollIssuer(issuer string) EnrollOption

WithEnrollIssuer sets a custom issuer name for the otpauth URI.

func WithEnrollLabel added in v1.3.0

func WithEnrollLabel(label string) EnrollOption

WithEnrollLabel sets a human-friendly label for the factor (e.g. "Work iPhone").

type Factor added in v1.3.0

type Factor struct {
	ID          string     `json:"id"`
	UserID      string     `json:"user_id"`
	Type        FactorType `json:"type"`
	Label       string     `json:"label,omitempty"`
	Confirmed   bool       `json:"confirmed"`
	ConfirmedAt *time.Time `json:"confirmed_at,omitempty"`
	LastUsedAt  *time.Time `json:"last_used_at,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

Factor represents a registered second factor. It deliberately omits any plaintext or encrypted secrets so normal APIs cannot leak sensitive material.

type FactorRecord added in v1.3.0

type FactorRecord struct {
	ID               string     `json:"id"`
	UserID           string     `json:"user_id"`
	Type             string     `json:"type"`
	Label            string     `json:"label,omitempty"`
	EncryptedSecret  string     `json:"encrypted_secret"`
	Confirmed        bool       `json:"confirmed"`
	ConfirmedAt      *time.Time `json:"confirmed_at,omitempty"`
	LastUsedAt       *time.Time `json:"last_used_at,omitempty"`
	LastUsedTimeStep int64      `json:"last_used_time_step"`
	CreatedAt        time.Time  `json:"created_at"`
	UpdatedAt        time.Time  `json:"updated_at"`
}

FactorRecord is the persistent model for an MFA factor.

type FactorType added in v1.3.0

type FactorType string

FactorType represents an authentication second factor kind.

const (
	// FactorTypeTOTP represents Time-based One-Time Password (RFC 6238).
	FactorTypeTOTP FactorType = "totp"

	// FactorTypeRecoveryCodes represents single-use backup recovery codes.
	FactorTypeRecoveryCodes FactorType = "recovery_codes"
)

type Module added in v1.3.0

type Module struct {
	// contains filtered or unexported fields
}

Module implements auth.PluginModule and auth.SchemaProvider for two-factor authentication.

func New

func New(opts ...Option) *Module

New creates an installable twofactor PluginModule with optional configurations.

func (*Module) ID added in v1.3.0

func (m *Module) ID() string

ID returns the module's unique identifier.

func (*Module) Init added in v1.3.0

func (m *Module) Init(h *auth.Host) error

Init configures infrastructure, registers hooks, and provides the typed Capability into GoModularAuth via plugin-sdk.

func (*Module) RegisterSchema added in v1.3.0

func (m *Module) RegisterSchema(s *schema.Schema) error

RegisterSchema contributes twofactor tables and user field extensions to the compiled schema.

type Option

type Option func(*Config)

Option configures the Config struct.

func WithChallengeTTL added in v1.3.0

func WithChallengeTTL(ttl time.Duration) Option

WithChallengeTTL sets the duration for ephemeral MFA challenges.

func WithDefaultRecoveryCodesCount added in v1.3.0

func WithDefaultRecoveryCodesCount(count int) Option

WithDefaultRecoveryCodesCount sets the default number of recovery codes generated.

func WithEncryptionKey added in v1.3.0

func WithEncryptionKey(key []byte) Option

WithEncryptionKey sets the 32-byte master key for the default AES-GCM protector.

func WithEnforceMFAOnSignIn added in v1.3.0

func WithEnforceMFAOnSignIn(enforce bool) Option

WithEnforceMFAOnSignIn configures whether to require MFA challenges upon primary sign in.

func WithIssuer

func WithIssuer(issuer string) Option

WithIssuer sets the default issuer name for TOTP provisioning URIs.

func WithSecretProtector added in v1.3.0

func WithSecretProtector(p SecretProtector) Option

WithSecretProtector sets a custom SecretProtector implementation.

func WithSkewSteps added in v1.3.0

func WithSkewSteps(steps int) Option

WithSkewSteps sets the allowed clock skew steps (e.g. 1 allows +/- 1 step).

type RecoveryCodeRecord added in v1.3.0

type RecoveryCodeRecord struct {
	ID        string     `json:"id"`
	UserID    string     `json:"user_id"`
	CodeHash  string     `json:"code_hash"`
	UsedAt    *time.Time `json:"used_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

RecoveryCodeRecord is the persistent model for a hashed backup recovery code.

type SecretProtector added in v1.3.0

type SecretProtector interface {
	Encrypt(plaintext []byte) (string, error)
	Decrypt(ciphertext string) ([]byte, error)
}

SecretProtector encrypts and decrypts sensitive factor secrets at rest.

type Status added in v1.3.0

type Status struct {
	Enabled            bool       `json:"enabled"`
	Factors            []Factor   `json:"factors"`
	RecoveryCodesCount int        `json:"recovery_codes_count"`
	PrimaryFactorType  FactorType `json:"primary_factor_type,omitempty"`
}

Status summarizes the two-factor authentication configuration for a user.

type TOTPEnrollment added in v1.3.0

type TOTPEnrollment struct {
	FactorID        string `json:"factor_id"`
	Secret          string `json:"secret"`           // Plaintext base32 secret; returned only during enrollment
	ProvisioningURI string `json:"provisioning_uri"` // Standard otpauth:// URI for QR code generation
	Issuer          string `json:"issuer"`
	AccountName     string `json:"account_name"`
}

TOTPEnrollment contains the setup data returned ONCE during TOTP enrollment initiation. Applications present this secret or URI to the user (e.g., via QR code).

type TwoFactor added in v0.3.0

type TwoFactor interface {
	// GetStatus returns the complete 2FA status and configured factors for a user.
	GetStatus(ctx context.Context, userID string) (*Status, error)

	// IsEnabled reports whether the user has at least one confirmed MFA factor.
	IsEnabled(ctx context.Context, userID string) (bool, error)

	// ListFactors returns all registered factors (confirmed or pending) for a user.
	ListFactors(ctx context.Context, userID string) ([]Factor, error)

	// EnrollTOTP initiates TOTP enrollment. It creates a pending factor and returns the secret & provisioning URI.
	// The factor remains inactive until confirmed via ConfirmTOTP.
	EnrollTOTP(ctx context.Context, userID string, opts ...EnrollOption) (*TOTPEnrollment, error)

	// ConfirmTOTP confirms a pending TOTP factor with a valid code from the authenticator app.
	// Upon success, the factor is activated and EventTwoFactorEnabled is emitted.
	ConfirmTOTP(ctx context.Context, userID, factorID, code string) error

	// VerifyTOTP verifies a TOTP code for a user against their confirmed TOTP factors.
	// It applies clock-skew tolerance and prevents replay attacks. Emits EventTwoFactorVerified.
	VerifyTOTP(ctx context.Context, userID, code string) (*VerifyResult, error)

	// VerifyRecoveryCode verifies and consumes a single-use backup recovery code.
	// Emits EventTwoFactorVerified upon successful consumption.
	VerifyRecoveryCode(ctx context.Context, userID, code string) (*VerifyResult, error)

	// CreateChallenge creates a short-lived MFA challenge for two-step authentication flows.
	CreateChallenge(ctx context.Context, userID string) (*Challenge, error)

	// VerifyChallenge verifies a challenge token with either a TOTP code or a recovery code.
	VerifyChallenge(ctx context.Context, challengeID, code string) (*VerifyResult, error)

	// GenerateRecoveryCodes generates a fresh set of recovery codes, hashing them before storing.
	// Returns the plaintext codes ONCE.
	GenerateRecoveryCodes(ctx context.Context, userID string, count int) ([]string, error)

	// RotateRecoveryCodes revokes all existing recovery codes for a user and generates a fresh set.
	RotateRecoveryCodes(ctx context.Context, userID string) ([]string, error)

	// GetRemainingRecoveryCodesCount returns the number of unused active recovery codes.
	GetRemainingRecoveryCodesCount(ctx context.Context, userID string) (int, error)

	// Disable disables MFA for a user by removing all factors and recovery codes.
	// Emits EventTwoFactorDisabled.
	Disable(ctx context.Context, userID string) error

	// DisableFactor removes a specific factor. If no confirmed factors remain, MFA is effectively disabled.
	DisableFactor(ctx context.Context, userID, factorID string) error
}

TwoFactor is the strongly typed capability provided by the twofactor plugin.

type TwoFactorDisabledPayload added in v1.3.0

type TwoFactorDisabledPayload struct {
	UserID     string    `json:"user_id"`
	DisabledAt time.Time `json:"disabled_at"`
	Reason     string    `json:"reason,omitempty"`
}

TwoFactorDisabledPayload represents observational data when 2FA is disabled.

type TwoFactorEnabledPayload added in v1.3.0

type TwoFactorEnabledPayload struct {
	UserID     string     `json:"user_id"`
	FactorID   string     `json:"factor_id"`
	FactorType FactorType `json:"factor_type"`
	EnabledAt  time.Time  `json:"enabled_at"`
}

TwoFactorEnabledPayload represents observational data when 2FA is enabled.

type TwoFactorVerifiedPayload added in v1.3.0

type TwoFactorVerifiedPayload struct {
	UserID         string         `json:"user_id"`
	FactorID       string         `json:"factor_id,omitempty"`
	FactorType     FactorType     `json:"factor_type"`
	AssuranceLevel AssuranceLevel `json:"assurance_level"`
	VerifiedAt     time.Time      `json:"verified_at"`
}

TwoFactorVerifiedPayload represents observational data when a factor is verified.

type VerifyResult added in v0.7.0

type VerifyResult struct {
	UserID         string         `json:"user_id"`
	FactorID       string         `json:"factor_id,omitempty"`
	FactorType     FactorType     `json:"factor_type"`
	AssuranceLevel AssuranceLevel `json:"assurance_level"`
	VerifiedAt     time.Time      `json:"verified_at"`
}

VerifyResult details the successful verification of an MFA factor.

Directories

Path Synopsis
Package plugins exposes the capability aliases for GoModularAuth plugins.
Package plugins exposes the capability aliases for GoModularAuth plugins.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL