Go Modular Auth
A modular, reactive, extensible, and strongly typed authentication framework for Go (Golang).
π Overview
Go Modular Auth is a decoupled authentication engine designed to provide maximum flexibility and developer ergonomics in Go applications. Inspired by modular architectures such as Better-Auth, it allows developers to compose authentication systems from independent plugins (emailpassword, twofactor, OAuth2, etc.) without locking the project into any specific web framework (compatible with Gin, Fiber, Echo, Chi, net/http, or gRPC).
π Key Features
- π§© 100% Modular Plugin-Based Architecture: Add or remove authentication capabilities based on project requirements.
- β‘ Strong Typing with Generics (Go 1.18+): Safe access to individual plugin APIs with full IDE autocomplete and no manual casting via
auth.Plugin[emailpassword.Plugin](app).
- π¦ Mutable Parameters Pattern (
Params.Extra): Allows plugins and EventBus interceptors to dynamically enrich request parameters (Set/Get) before database persistence.
- π’ Integrated Reactive EventBus: Subscribe to lifecycle hooks (
emailpassword.EventSignUpBefore, emailpassword.EventSignUpAfter, etc.) to mutate payloads in-flight, send asynchronous emails, or audit user access.
- π Production-Grade Security: Strong password hashing using
bcrypt, cryptographically secure token generation via crypto/rand, and 2FA TOTP (RFC 6238).
- ποΈ Decoupled Storage: Connect any database (PostgreSQL, MySQL, SQLite, MongoDB, Redis, GORM) through clean repository interfaces. Includes a built-in thread-safe in-memory store.
π¦ Installation
go get github.com/BladiCreator/go-modular-auth
π‘ Production Example
The following complete example demonstrates user registration with dynamic parameter interception, lifecycle audit event subscription, user sign-in, 2FA TOTP secret generation, and TOTP code verification:
package main
import (
"context"
"fmt"
"log"
"github.com/BladiCreator/go-modular-auth/adapters/memory"
"github.com/BladiCreator/go-modular-auth/auth"
"github.com/BladiCreator/go-modular-auth/config"
"github.com/BladiCreator/go-modular-auth/domain/dto"
"github.com/BladiCreator/go-modular-auth/plugins"
"github.com/BladiCreator/go-modular-auth/plugins/emailpassword"
"github.com/BladiCreator/go-modular-auth/plugins/twofactor"
)
func main() {
ctx := context.Background()
// Storage adapter (in-memory store for this example)
storage := memory.New()
// 1. Initialize engine with configuration and plugins
app, err := auth.New(
config.WithBcryptCost(12),
config.WithPlugins(
plugins.EmailPassword(storage, emailpassword.WithMinPasswordLength(8)),
plugins.TwoFactor(storage, twofactor.WithIssuer("Enterprise ERP")),
),
)
if err != nil {
log.Fatalf("Failed to initialize Auth: %v", err)
}
// 2. Intercept registration to attach dynamic metadata (e.g. Roles, Organizations)
app.Events().Subscribe(emailpassword.EventSignUpBefore, func(c context.Context, payload *emailpassword.SignUpEventPayload) {
payload.Params.Set("role", "admin")
payload.Params.Set("org_id", "org_123")
})
// 3. Subscribe to post-action EventBus notifications
app.Events().Subscribe(emailpassword.EventSignUpAfter, func(c context.Context, payload *emailpassword.SignUpEventPayload) {
role, _ := payload.Params.Get("role")
log.Printf("π§ [EVENT] Sending welcome email to: %s (Role: %v)", payload.User.Email, role)
})
app.Events().Subscribe(emailpassword.EventSignInAfter, func(c context.Context, payload *emailpassword.SignInEventPayload) {
log.Printf("π‘οΈ [AUDIT] Successful sign-in - User ID: %s | Email: %s", payload.User.ID, payload.User.Email)
})
// 4. Flow 1: User Registration
fmt.Println("--- 1. User Registration ---")
newUser, err := auth.Plugin[emailpassword.Plugin](app).SignUp(ctx, dto.SignUpParams{
Name: "Carlos Mendoza",
Email: "carlos@enterprise.com",
Password: "SuperSecurePassword123!",
})
if err != nil {
log.Fatalf("Sign up failed: %v", err)
}
fmt.Printf("β Registered User: %s (ID: %s)\n\n", newUser.Name, newUser.ID)
// 5. Flow 2: User Sign-In
fmt.Println("--- 2. User Sign-In ---")
signedInUser, err := auth.Plugin[emailpassword.Plugin](app).SignIn(ctx, dto.SignInParams{
Email: "carlos@enterprise.com",
Password: "SuperSecurePassword123!",
})
if err != nil {
log.Fatalf("Sign in failed: %v", err)
}
fmt.Printf("β Successfully authenticated as: %s (ID: %s)\n\n", signedInUser.Email, signedInUser.ID)
// 6. Flow 3: 2FA TOTP Configuration
fmt.Println("--- 3. 2FA TOTP Setup ---")
otpURI, err := auth.Plugin[twofactor.Plugin](app).GenerateTOTPSecret(ctx, newUser.ID)
if err != nil {
log.Fatalf("Failed to generate 2FA: %v", err)
}
fmt.Printf("β Authenticator App URI: %s\n\n", otpURI)
// 7. Flow 4: 2FA Code Verification
fmt.Println("--- 4. 2FA Code Verification ---")
valid, err := auth.Plugin[twofactor.Plugin](app).VerifyCode(ctx, newUser.ID, "123456")
if err != nil || !valid {
fmt.Println("β Invalid 2FA code")
} else {
fmt.Println("β 2FA code successfully verified")
}
}
ποΈ Custom Repository Implementation (GORM + PostgreSQL)
In production applications, user records and sessions are persisted to a relational or document database. Implement the repository interfaces required by each plugin to use your database of choice.
1. Database Table / Schema Definition with GORM
package store
import (
"context"
"errors"
"time"
"github.com/BladiCreator/go-modular-auth/domain"
"github.com/BladiCreator/go-modular-auth/domain/dto"
"github.com/BladiCreator/go-modular-auth/domain/entity"
"github.com/BladiCreator/go-modular-auth/plugins/emailpassword"
"github.com/BladiCreator/go-modular-auth/plugins/twofactor"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Compile-time interface checks
var (
_ emailpassword.Repository = (*GormAuthRepository)(nil)
_ twofactor.Repository = (*GormAuthRepository)(nil)
)
// ORM Models for GORM
type UserModel struct {
ID string `gorm:"primaryKey;type:uuid"`
Name string `gorm:"not null"`
Email string `gorm:"uniqueIndex;not null"`
PasswordHash string `gorm:"not null"`
EmailVerified bool `gorm:"default:false"`
TOTPSecret string `gorm:"default:''"`
CreatedAt time.Time
UpdatedAt time.Time
}
type TwoFactorModel struct {
ID string `gorm:"primaryKey;type:uuid"`
UserID string `gorm:"uniqueIndex;not null"`
Secret string `gorm:"not null"`
BackupCodes string `gorm:"type:text"`
Verified bool `gorm:"default:false"`
Failures int `gorm:"default:0"`
LockedUntil *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type OTPChallengeModel struct {
Key string `gorm:"primaryKey"`
UserID string `gorm:"index;not null"`
CodeHash string `gorm:"not null"`
Attempts int `gorm:"default:0"`
ExpiresAt time.Time `gorm:"index"`
}
// Main Repository Struct
type GormAuthRepository struct {
db *gorm.DB
}
func NewGormAuthRepository(db *gorm.DB) *GormAuthRepository {
// AutoMigrate creates tables automatically in PostgreSQL / MySQL / SQLite
_ = db.AutoMigrate(&UserModel{}, &SessionModel{}, &TwoFactorModel{}, &OTPChallengeModel{})
return &GormAuthRepository{db: db}
}
// --- emailpassword.Repository Methods ---
func (r *GormAuthRepository) CreateUser(ctx context.Context, params *dto.CreateUserParams) (*entity.User, error) {
model := UserModel{
ID: uuid.New().String(),
Name: params.Name,
Email: params.Email,
PasswordHash: params.PasswordHash,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := r.db.WithContext(ctx).Create(&model).Error; err != nil {
return nil, err
}
return toUserEntity(&model), nil
}
func (r *GormAuthRepository) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) {
var model UserModel
err := r.db.WithContext(ctx).Where("email = ?", email).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domain.ErrUserNotFound
}
return toUserEntity(&model), err
}
func (r *GormAuthRepository) GetUserByID(ctx context.Context, id string) (*entity.User, error) {
var model UserModel
err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domain.ErrUserNotFound
}
return toUserEntity(&model), err
}
func (r *GormAuthRepository) CreateSession(ctx context.Context, s *dto.CreateSessionParams) (*entity.Session, error) {
model := SessionModel{
ID: uuid.New().String(),
UserID: s.UserID,
Token: s.Token,
IPAddress: s.IPAddress,
UserAgent: s.UserAgent,
ExpiresAt: s.ExpiresAt,
CreatedAt: s.CreatedAt,
}
if err := r.db.WithContext(ctx).Create(&model).Error; err != nil {
return nil, err
}
return toSessionEntity(&model), nil
}
func (r *GormAuthRepository) GetSessionByToken(ctx context.Context, token string) (*entity.Session, error) {
var model SessionModel
err := r.db.WithContext(ctx).Where("token = ?", token).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domain.ErrSessionNotFound
}
return toSessionEntity(&model), err
}
func (r *GormAuthRepository) DeleteSession(ctx context.Context, token string) error {
return r.db.WithContext(ctx).Where("token = ?", token).Delete(&SessionModel{}).Error
}
// --- twofactor.Repository Methods ---
func (r *GormAuthRepository) FindByUserID(ctx context.Context, userID string) (*twofactor.TwoFactor, error) {
var model TwoFactorModel
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, twofactor.ErrTwoFactorNotEnabled
}
return &twofactor.TwoFactor{
ID: model.ID,
UserID: model.UserID,
Secret: model.Secret,
BackupCodes: model.BackupCodes,
Verified: model.Verified,
Failures: model.Failures,
LockedUntil: model.LockedUntil,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}, err
}
func (r *GormAuthRepository) Create(ctx context.Context, tf *twofactor.TwoFactor) error {
model := TwoFactorModel{
ID: uuid.New().String(),
UserID: tf.UserID,
Secret: tf.Secret,
BackupCodes: tf.BackupCodes,
Verified: tf.Verified,
Failures: tf.Failures,
LockedUntil: tf.LockedUntil,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return r.db.WithContext(ctx).Create(&model).Error
}
func (r *GormAuthRepository) Update(ctx context.Context, tf *twofactor.TwoFactor) error {
return r.db.WithContext(ctx).Model(&TwoFactorModel{}).Where("user_id = ?", tf.UserID).Updates(map[string]any{
"secret": tf.Secret,
"backup_codes": tf.BackupCodes,
"verified": tf.Verified,
"failures": tf.Failures,
"locked_until": tf.LockedUntil,
"updated_at": time.Now(),
}).Error
}
func (r *GormAuthRepository) DeleteByUserID(ctx context.Context, userID string) error {
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&TwoFactorModel{}).Error
}
func (r *GormAuthRepository) SaveOTPChallenge(ctx context.Context, challenge *twofactor.OTPChallenge) error {
model := OTPChallengeModel{
Key: challenge.Key,
UserID: challenge.UserID,
CodeHash: challenge.CodeHash,
Attempts: challenge.Attempts,
ExpiresAt: challenge.ExpiresAt,
}
return r.db.WithContext(ctx).Save(&model).Error
}
func (r *GormAuthRepository) GetOTPChallenge(ctx context.Context, key string) (*twofactor.OTPChallenge, error) {
var model OTPChallengeModel
err := r.db.WithContext(ctx).Where("key = ?", key).First(&model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, twofactor.ErrOTPExpired
}
return &twofactor.OTPChallenge{
Key: model.Key,
UserID: model.UserID,
CodeHash: model.CodeHash,
Attempts: model.Attempts,
ExpiresAt: model.ExpiresAt,
}, err
}
func (r *GormAuthRepository) DeleteOTPChallenge(ctx context.Context, key string) error {
return r.db.WithContext(ctx).Where("key = ?", key).Delete(&OTPChallengeModel{}).Error
}
// Entity mapping helpers
func toUserEntity(m *UserModel) *entity.User {
return &entity.User{
ID: m.ID,
Name: m.Name,
Email: m.Email,
PasswordHash: m.PasswordHash,
EmailVerified: m.EmailVerified,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func toSessionEntity(m *SessionModel) *entity.Session {
return &entity.Session{
ID: m.ID,
UserID: m.UserID,
Token: m.Token,
ExpiresAt: m.ExpiresAt,
CreatedAt: m.CreatedAt,
IPAddress: m.IPAddress,
UserAgent: m.UserAgent,
}
}
π’ Advanced EventBus and Mutable Parameters Patterns
The decoupled EventBus allows intercepting and modifying in-flight parameters during registration or reacting asynchronously after actions complete.
1. Registration Parameter Interception (Params.Set)
app.Events().Subscribe(emailpassword.EventSignUpBefore, func(ctx context.Context, payload *emailpassword.SignUpEventPayload) {
// Attach dynamic plugin metadata (e.g. initial Organization ID or Role)
payload.Params.Set("organization_id", "org_987")
payload.Params.Set("role", "member")
})
2. Asynchronous Welcome Emails
app.Events().Subscribe(emailpassword.EventSignUpAfter, func(ctx context.Context, payload *emailpassword.SignUpEventPayload) {
// Execute inside a goroutine to avoid blocking the HTTP response
go func(user *entity.User) {
mailer.SendWelcomeEmail(user.Email, user.Name)
}(payload.User)
})
3. Security Audit Logging on Sign-In
app.Events().Subscribe(emailpassword.EventSignInAfter, func(ctx context.Context, payload *emailpassword.SignInEventPayload) {
securityLogger.Info("Successful sign-in", "userID", payload.User.ID, "email", payload.User.Email)
})
π Complete Plugin Reference
π§ Plugin emailpassword
Handles credential-based registration, authentication, password management, and password resets.
- Constructor:
plugins.EmailPassword(repo, opts...)
- Configuration Options:
emailpassword.WithMinPasswordLength(minLen int) (default: 8)
emailpassword.WithRequireEmailVerification(require bool) (default: false)
emailpassword.WithResetTokenExpiry(duration time.Duration) (default: 1 hour)
- Published Events:
emailpassword.EventSignUpBefore β (ctx context.Context, payload *emailpassword.SignUpEventPayload) (contains Params *dto.CreateUserParams)
emailpassword.EventSignUpAfter β (ctx context.Context, payload *emailpassword.SignUpEventPayload) (contains Params and User *entity.User)
emailpassword.EventSignInBefore β (ctx context.Context, payload *emailpassword.SignInEventPayload) (contains User *entity.User)
emailpassword.EventSignInAfter β (ctx context.Context, payload *emailpassword.SignInEventPayload) (contains User *entity.User)
emailpassword.EventPasswordChangeBefore / After β (ctx context.Context, payload *emailpassword.PasswordChangeEventPayload)
emailpassword.EventPasswordResetRequested β (ctx context.Context, payload *emailpassword.PasswordResetRequestedEventPayload)
emailpassword.EventPasswordResetCompleted β (ctx context.Context, payload *emailpassword.PasswordResetCompletedEventPayload)
π Plugin twofactor
Handles Two-Factor Authentication via Time-based One-Time Passwords (TOTP RFC 6238).
- Constructor:
plugins.TwoFactor(repo, opts...)
- Configuration Options:
twofactor.WithIssuer(issuer string) (default: "Auth")
- Published Events:
twofactor.EventTOTPGenerated β (ctx context.Context, payload *twofactor.TOTPGeneratedEventPayload)
π License
This project is licensed under the MIT License. See the LICENSE file for details.