go-modular-auth

command module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 16 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 modern modular authentication architectures, 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 interceptors to dynamically enrich request parameters (Set/Get) before database persistence.
  • πŸ“’ Native Pipeline & Reactive Lifecycle Hooks: Zero external dependencies! Synchronous interceptor pipeline (Pipeline()) for in-flight parameter mutation/validation and typed reactive hooks (Hooks()) for asynchronous notifications, metrics, and audit logging.
  • πŸ” 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 synchronous parameter interception, typed lifecycle hooks, 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/domain/entity"
	"github.com/BladiCreator/go-modular-auth/plugin"
	"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 via synchronous pipeline to attach dynamic metadata
	app.Pipeline().AddInterceptor(func(ctx context.Context, stage plugin.PipelineStage, payload any) error {
		if stage == plugin.StageBeforeSignUp {
			if params, ok := payload.(*dto.CreateUserParams); ok {
				params.Set("role", "admin")
				params.Set("org_id", "org_123")
			}
		}
		return nil
	})

	// 3. Subscribe to post-action notifications via typed Hooks or pub/sub
	app.Hooks().OnUserSignedUp(func(c context.Context, user *entity.User) {
		log.Printf("πŸ“§ [HOOK] Sending welcome email to: %s", user.Email)
	})

	app.Hooks().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,
	}
}

πŸ“’ Interceptor Pipeline and Reactive Hooks Patterns

go-modular-auth replaces arbitrary event buses with a dual-system architecture:

  1. Synchronous Interceptor Pipeline (app.Pipeline()): For in-flight parameter mutation, stage guards, and synchronous validation.
  2. Reactive Hooks & Pub/Sub (app.Hooks()): For asynchronous side-effects, audit logging, analytics, and notification dispatching.

1. Synchronous Registration Interception (Pipeline().AddInterceptor)

app.Pipeline().AddInterceptor(func(ctx context.Context, stage plugin.PipelineStage, payload any) error {
    if stage == plugin.StageBeforeSignUp {
        if params, ok := payload.(*dto.CreateUserParams); ok {
            // Enrich or mutate registration params before database persistence
            params.Set("organization_id", "org_987")
            params.Set("role", "member")
        }
    }
    return nil
})

2. Typed Lifecycle Hooks (Hooks().On*)

app.Hooks().OnUserSignedUp(func(ctx context.Context, user *entity.User) {
    // Fire-and-forget or async notification
    go func() {
        mailer.SendWelcomeEmail(user.Email, user.Name)
    }()
})

3. Topic-Based Audit Logging (Hooks().Subscribe)

app.Hooks().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.
repository
Package repository defines centralized persistent data storage contracts for the core domain entities.
Package repository defines centralized persistent data storage contracts for the core domain entities.
Package httpauth provides non-invasive, standard net/http transport utilities for authentication workflows in go-modular-auth.
Package httpauth provides non-invasive, standard net/http transport utilities for authentication workflows in go-modular-auth.
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).
customsession
Package customsession provides dynamic session payload transformation and dynamic additional fields management.
Package customsession provides dynamic session payload transformation and dynamic additional fields management.
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 shared Hooks dispatcher.
Package organization defines event names and typed event payloads published by the Organization plugin on the shared Hooks dispatcher.
ott
phonenumber
Package phonenumber provides SMS OTP (One-Time Password) and phone number-based authentication for go-modular-auth, supporting passwordless sign-in, phone number verification and updates, phone + password login, SMS password resets, and attempt budgeting.
Package phonenumber provides SMS OTP (One-Time Password) and phone number-based authentication for go-modular-auth, supporting passwordless sign-in, phone number verification and updates, phone + password login, SMS password resets, and attempt budgeting.
twofactor
Package twofactor defines event names and typed event payloads published by the TwoFactor plugin on the shared Hooks dispatcher.
Package twofactor defines event names and typed event payloads published by the TwoFactor plugin on the shared Hooks dispatcher.

Jump to

Keyboard shortcuts

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