go-modular-auth

command module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 15 Imported by: 0

README ΒΆ

Go Modular Auth Logo

Go Modular Auth

A modular, reactive, extensible, and strongly typed authentication framework for Go (Golang).

Go Reference Go Report Card Go Version


🌟 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.

Documentation ΒΆ

The Go Gopher

There is no documentation for this package.

Directories ΒΆ

Path Synopsis
adapters
memory
Package memory provides an in-memory repository implementation suitable for development and testing.
Package memory provides an in-memory repository implementation suitable for development and testing.
Package auth provides the core engine for initializing, configuring, and managing modular authentication plugins.
Package auth provides the core engine for initializing, configuring, and managing modular authentication plugins.
Package config defines global configuration options and functional option helpers for the Auth engine.
Package config defines global configuration options and functional option helpers for the Auth engine.
Package domain defines core domain entities, data transfer objects (DTOs), and sentinel errors.
Package domain defines core domain entities, data transfer objects (DTOs), and sentinel errors.
dto
Package dto provides Data Transfer Objects (Params) for authentication operations.
Package dto provides Data Transfer Objects (Params) for authentication operations.
entity
Package entity contains domain data models representing application domain objects.
Package entity contains domain data models representing application domain objects.
internal
Package plugin defines the foundational contracts and shared execution context for authentication plugins.
Package plugin defines the foundational contracts and shared execution context for authentication plugins.
Package plugins provides convenient factory constructors for instantiating officially supported authentication plugins, such as EmailPassword (credential-based sign-in/sign-up) and TwoFactor (RFC 6238 TOTP, backup codes, challenge OTP).
Package plugins provides convenient factory constructors for instantiating officially supported authentication plugins, such as EmailPassword (credential-based sign-in/sign-up) and TwoFactor (RFC 6238 TOTP, backup codes, challenge OTP).
emailotp
Package emailotp provides email-based One-Time Password (OTP) authentication for go-modular-auth, supporting passwordless sign-in, email verification, password reset, and email change flows.
Package emailotp provides email-based One-Time Password (OTP) authentication for go-modular-auth, supporting passwordless sign-in, email verification, password reset, and email change flows.
jwt
organization
Package organization defines event names and typed event payloads published by the Organization plugin on the global EventBus.
Package organization defines event names and typed event payloads published by the Organization plugin on the global EventBus.
twofactor
Package twofactor defines event names and typed event payloads published by the TwoFactor plugin on the global EventBus.
Package twofactor defines event names and typed event payloads published by the TwoFactor plugin on the global EventBus.

Jump to

Keyboard shortcuts

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