chiauth

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: MIT Imports: 16 Imported by: 0

README

chiauth

Go Reference Go Report Card

A complete, mountable authentication and authorization package for Go applications using the Chi router.

Inspired by Djoser for Django REST Framework — drop it into any Chi app and get a full auth system with one function call.

go get github.com/kimenyu/chiauth

What you get out of the box

Feature Detail
Registration Email + password, bcrypt hashing
Email verification OTP token, sent via email or printed to stdout in dev
Login Returns JWT access token + refresh token
Token refresh Rotation — old token revoked on each use
Logout Single session or all sessions
Password management Change, forgot, reset via token
Profile GET / PATCH / DELETE /me
RBAC Roles, permissions, middleware gates
Admin endpoints User management, role/permission assignment
Audit log Every auth event recorded with IP + user agent
Account lockout After N failed login attempts (configurable)
Swagger UI Mounted at /auth/docs
Migrations Embedded SQL, applied with one call

Quick Start

package main

import (
    "context"
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi/v5"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
    "github.com/kimenyu/chiauth"
    "github.com/kimenyu/chiauth/models"
)

func main() {
    db, err := sqlx.Connect("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }

    ca := chiauth.New(chiauth.Config{
        DB:        db,
        JWTSecret: os.Getenv("JWT_SECRET"),
        BaseURL:   "http://localhost:8080",
        AppName:   "My App",
    })

    // Apply database migrations (run once on startup)
    if err := ca.RunMigrations(); err != nil {
        log.Fatalf("migrations failed: %v", err)
    }

    // Seed your domain permissions (idempotent — safe every boot)
    ca.SeedPermissions(context.Background(), []models.Permission{
        {Resource: "invoice", Action: "create", Codename: "invoice:create", Description: "Create invoices"},
        {Resource: "invoice", Action: "read",   Codename: "invoice:read",   Description: "Read invoices"},
        {Resource: "invoice", Action: "delete", Codename: "invoice:delete", Description: "Delete invoices"},
        {Resource: "report",  Action: "read",   Codename: "report:read",    Description: "View reports"},
    })

    // Seed your domain roles
    ca.SeedRoles(context.Background(), []models.SeedRoleInput{
        {
            Slug:        "accountant",
            Name:        "Accountant",
            Permissions: []string{"invoice:create", "invoice:read", "invoice:delete", "report:read"},
        },
        {
            Slug:        "viewer",
            Name:        "Viewer",
            Permissions: []string{"invoice:read", "report:read"},
        },
    })

    r := chi.NewRouter()

    // Mount chiauth at /auth — all endpoints available under this prefix
    r.Mount("/auth", ca.Router())

    // Protect your own routes using chiauth middleware
    r.With(ca.AuthenticateMiddleware()).Get("/dashboard", dashboardHandler)
    r.With(ca.AuthenticateMiddleware(), ca.RequireRole("accountant")).Post("/invoices", createInvoiceHandler)
    r.With(ca.AuthenticateMiddleware(), ca.RequirePermission("invoice:delete")).Delete("/invoices/{id}", deleteInvoiceHandler)
    r.With(ca.AuthenticateMiddleware(), ca.RequireStaff()).Get("/admin/stats", adminStatsHandler)

    log.Println("listening on :8080")
    http.ListenAndServe(":8080", r)
}

Configuration

chiauth.Config{
    // Required
    DB:        db,           // *sqlx.DB
    JWTSecret: "...",        // min 32 chars recommended

    // Token TTLs (optional — defaults shown)
    JWTAccessTTL:  15 * time.Minute,
    JWTRefreshTTL: 7 * 24 * time.Hour,

    // Security (optional — defaults shown)
    PasswordMinLength:   8,
    MaxLoginAttempts:    5,
    RequireEmailVerify:  true,   // set false for API-only apps or dev
    RotateRefreshTokens: true,   // recommended — leave true
    AllowHardDelete:     false,  // true = permanent deletion

    // Email (optional — see Email Setup section)
    EmailSender:       mySender,           // email.Sender interface
    EmailTemplatesDir: "./my-emails",      // optional custom templates directory
    BaseURL:           "https://api.myapp.com",
    AppName:           "My App",
    SupportEmail:      "support@myapp.com",

    // Lifecycle hooks (optional)
    OnUserCreated:    func(u *models.User) { /* sync to CRM */ },
    OnLogin:          func(u *models.User, ip string) { /* analytics */ },
    OnPasswordReset:  func(u *models.User) { /* notify */ },
    OnAccountLocked:  func(u *models.User) { /* alert */ },
    OnAccountDeleted: func(u *models.User) { /* cleanup */ },
}

All Endpoints

Public (no auth required)
Method Path Description
POST /auth/register Register a new user
POST /auth/activate Verify email with token
POST /auth/activate/resend Resend verification email
POST /auth/login Login, get access + refresh tokens
POST /auth/token/refresh Refresh access token
POST /auth/password/forgot Request password reset email
POST /auth/password/reset/confirm Confirm password reset with token
GET /auth/docs/* Swagger UI
Authenticated (Bearer token required)
Method Path Description
POST /auth/logout Revoke current session
POST /auth/logout/all Revoke all sessions
GET /auth/me Get current user
PATCH /auth/me Update profile
DELETE /auth/me Delete account
POST /auth/password/change Change password
Admin (staff or superuser required)
Method Path Description
GET /auth/admin/users List users (paginated)
POST /auth/admin/users/{id}/roles Assign role to user
DELETE /auth/admin/users/{id}/roles/{roleId} Remove role from user
POST /auth/admin/users/{id}/permissions Grant direct permission
DELETE /auth/admin/users/{id}/permissions/{permissionId} Revoke direct permission
GET /auth/admin/roles List all roles
POST /auth/admin/roles Create a role
DELETE /auth/admin/roles/{id} Delete a role
GET /auth/admin/permissions List all permissions

Permissions and Roles

chiauth is domain-agnostic. It does not ship with predefined permissions. You define permissions that match your application's domain and seed them on startup.

The mental model: chiauth is a lock manufacturer. It makes locks, keys, and a key management system. It does not decide which doors you put locks on — that is your building, your rules.

How permissions are structured

Every permission has three fields:

models.Permission{
    Resource:    "invoice",         // the thing being acted on
    Action:      "delete",          // what is being done
    Codename:    "invoice:delete",  // Resource:Action — used in middleware
    Description: "Delete invoices", // human-readable
}
How roles bundle permissions
// Hospital app
ca.SeedPermissions(ctx, []models.Permission{
    {Resource: "patient",      Action: "read",   Codename: "patient:read"},
    {Resource: "prescription", Action: "create", Codename: "prescription:create"},
    {Resource: "lab_result",   Action: "read",   Codename: "lab_result:read"},
})

ca.SeedRoles(ctx, []models.SeedRoleInput{
    {
        Slug:        "doctor",
        Name:        "Doctor",
        Permissions: []string{"patient:read", "prescription:create", "lab_result:read"},
    },
    {
        Slug:        "receptionist",
        Name:        "Receptionist",
        Permissions: []string{"patient:read"},
    },
})
// E-commerce app
ca.SeedPermissions(ctx, []models.Permission{
    {Resource: "product", Action: "create", Codename: "product:create"},
    {Resource: "order",   Action: "refund", Codename: "order:refund"},
})

ca.SeedRoles(ctx, []models.SeedRoleInput{
    {Slug: "vendor",        Permissions: []string{"product:create"}},
    {Slug: "support_agent", Permissions: []string{"order:refund"}},
})

The middleware call is identical regardless of domain:

r.With(ca.AuthenticateMiddleware(), ca.RequirePermission("prescription:create")).Post("/prescriptions", handler)
r.With(ca.AuthenticateMiddleware(), ca.RequirePermission("order:refund")).Post("/orders/{id}/refund", handler)
Permission resolution order
  1. IsSuperuser = true → always granted, no checks run
  2. User has the permission directly (via POST /auth/admin/users/{id}/permissions)
  3. User holds a role that has the permission
Built-in system roles

These are seeded automatically and cannot be deleted:

Slug Description
superuser Bypasses all permission checks
staff Can access /auth/admin/* endpoints
user Default role assigned to every new user

Middleware Reference

ca := chiauth.New(cfg)

// Validates Bearer JWT, injects *models.User into context
ca.AuthenticateMiddleware()

// Role gates — chain after AuthenticateMiddleware
ca.RequireRole("admin")
ca.RequireAnyRole("admin", "moderator")

// Permission gate — chain after AuthenticateMiddleware
ca.RequirePermission("invoice:delete")

// Convenience gates
ca.RequireStaff()      // IsStaff or IsSuperuser
ca.RequireSuperuser()  // IsSuperuser only

// Read the injected user in your own handlers
user := middleware.UserFromContext(r.Context())

Email Setup

Development (no setup required)

If no EmailSender is configured, chiauth prints all tokens to stdout:

========== [chiauth] VERIFICATION EMAIL ==========
To:    user@example.com
Token: a1b2c3d4e5f6...
URL:   http://localhost:8080/auth/activate?token=a1b2c3d4...
==================================================

Copy the token and POST it to /auth/activate. No email provider needed.

You can also disable email verification entirely:

chiauth.Config{
    RequireEmailVerify: false, // accounts are active immediately
}
Production (SMTP)

SMTPSender handles template rendering but requires you to wire in your own SMTP transport using gopkg.in/gomail.v2. Add gomail to your app's go.mod, then uncomment the implementation in email/email.go:

// In your app's go.mod:
// require gopkg.in/gomail.v2 v2.0.0-...

import "github.com/kimenyu/chiauth/email"

sender, err := email.NewSMTPSender(email.SMTPConfig{
    Host:        "smtp.gmail.com",
    Port:        587,
    Username:    os.Getenv("SMTP_USER"),
    Password:    os.Getenv("SMTP_PASS"),
    FromAddress: "noreply@myapp.com",
    FromName:    "My App",
})
if err != nil {
    log.Fatal(err)
}

chiauth.Config{
    EmailSender: sender,
}

Note: The sendEmail method in SMTPSender has commented-out gomail code by design so the package compiles without requiring gomail as a dependency. Uncomment email/email.go's sendEmail body after adding gomail to your own go.mod.

Custom email provider (Resend, SendGrid, etc.)

Implement the email.Sender interface — this is the recommended approach for production:

type Sender interface {
    SendVerification(to string, data VerificationData) error
    SendPasswordReset(to string, data PasswordResetData) error
    SendLoginAlert(to string, data LoginAlertData) error
}

Example with Resend:

type ResendSender struct{ client *resend.Client }

func (s *ResendSender) SendVerification(to string, data email.VerificationData) error {
    _, err := s.client.Emails.Send(&resend.SendEmailRequest{
        From:    "noreply@myapp.com",
        To:      []string{to},
        Subject: "Verify your email",
        Html:    buildHTML(data),
    })
    return err
}

// implement SendPasswordReset and SendLoginAlert similarly
Custom email templates

Option 1 — Directory override (recommended):

my-templates/
├── verification.html
├── password_reset.html
└── login_alert.html
sender, _ := email.NewSMTPSender(email.SMTPConfig{
    TemplatesDir: "./my-templates",
    // ...
})

Option 2 — Inline string override:

chiauth.Config{
    EmailTemplates: &config.EmailTemplateOverrides{
        Verification: `<h1>Hi {{.FirstName}}, verify here: {{.VerificationURL}}</h1>`,
    },
}

Available template variables:

Variable Available in
{{.FirstName}} All templates
{{.AppName}} All templates
{{.SupportEmail}} All templates
{{.VerificationURL}} verification.html
{{.Token}} verification.html, password_reset.html
{{.ExpiresIn}} verification.html, password_reset.html
{{.ResetURL}} password_reset.html
{{.IPAddress}} password_reset.html, login_alert.html
{{.DeviceInfo}} login_alert.html
{{.Time}} login_alert.html

Postman Walkthrough

Full auth flow without a frontend:

1. Register
POST /auth/register
{
    "email": "joe@example.com",
    "password": "secret1234",
    "first_name": "Joe",
    "last_name": "Doe"
}

Check your terminal for the verification token.

2. Activate
POST /auth/activate
{
    "token": "<token from terminal>"
}

Or skip entirely by setting RequireEmailVerify: false.

3. Login
POST /auth/login
{
    "email": "joe@example.com",
    "password": "secret1234"
}

Response:

{
    "access_token": "eyJ...",
    "refresh_token": "abc123...",
    "token_type": "Bearer",
    "expires_at": "2026-05-10T16:00:00Z",
    "user": { "..." }
}
4. Use protected endpoints

Set header: Authorization: Bearer eyJ...

5. Refresh when expired
POST /auth/token/refresh
{
    "refresh_token": "abc123..."
}

Swagger UI

The Swagger UI is mounted at /auth/docs. Generate the spec with:

go install github.com/swaggo/swag/cmd/swag@latest
export PATH=$PATH:$(go env GOPATH)/bin
swag init -g docs/swagger.go -o docs

Then open http://localhost:8080/auth/docs in your browser.


Database

chiauth prefixes all its tables with chiauth_ to avoid conflicts with your existing schema:

chiauth_users
chiauth_roles
chiauth_permissions
chiauth_role_permissions
chiauth_user_roles
chiauth_user_permissions
chiauth_refresh_tokens
chiauth_otp_tokens
chiauth_audit_logs
chiauth_migrations

Apply migrations:

ca := chiauth.New(cfg)
if err := ca.RunMigrations(); err != nil {
    log.Fatal(err)
}

Lifecycle Hooks

React to auth events without patching the package:

chiauth.Config{
    OnUserCreated: func(u *models.User) {
        crm.CreateContact(u.Email, u.FirstName)
    },

    OnLogin: func(u *models.User, ip string) {
        analytics.Track("login", u.ID.String(), ip)
    },

    OnPasswordReset: func(u *models.User) {
        slack.Notify(fmt.Sprintf("Password reset: %s", u.Email))
    },

    OnAccountLocked: func(u *models.User) {
        sms.Send(u.PhoneNumber, "Your account has been locked. Contact support.")
    },
}

Security Notes

  • Passwords are hashed with bcrypt (cost 12)
  • Refresh tokens are stored as SHA-256 hashes — a leaked database does not expose active sessions
  • OTP tokens (email verification, password reset) are also stored as hashes
  • Refresh token rotation is on by default — reuse of a revoked token triggers full session invalidation
  • forgot password always returns 200 regardless of whether the email exists (prevents enumeration)
  • All tables are prefixed chiauth_ — no conflicts with your schema

Contributing

Contributions are welcome. Please open an issue before submitting a PR for large changes.

git clone https://github.com/kimenyu/chiauth
cd chiauth
go test ./models/... ./services/... -v

If you are adding a feature, add tests. If you are fixing a bug, add a test that reproduces it.


License

MIT — see LICENSE


Author

Built by Joseph Njorogenjorogekimenyu.online

Documentation

Overview

Package chiauth is a complete, mountable authentication and authorization package for Go applications using the Chi router.

Quick Start

r.Mount("/auth", chiauth.New(chiauth.Config{
    DB:        db,
    JWTSecret: os.Getenv("JWT_SECRET"),
}))

Protecting Routes

mw := chiauth.Middleware(cfg)

r.With(mw.Authenticate).Get("/dashboard", handler)
r.With(mw.Authenticate, mw.RequireRole("admin")).Get("/admin", handler)
r.With(mw.Authenticate, mw.RequirePermission("invoice:delete")).Delete("/invoices/{id}", handler)

Seeding Permissions and Roles

chiauth.SeedPermissions(db, []models.Permission{
    {Resource: "invoice", Action: "create", Codename: "invoice:create"},
    {Resource: "invoice", Action: "delete", Codename: "invoice:delete"},
})

chiauth.SeedRoles(db, []models.SeedRoleInput{
    {Slug: "accountant", Name: "Accountant", Permissions: []string{"invoice:create", "invoice:delete"}},
})

See https://github.com/kimenyu/chiauth for full documentation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChiAuth

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

ChiAuth is a fully wired chiauth instance. Mount its Router into your Chi app and use its Middleware methods to protect your own routes.

func New

func New(cfg Config) *ChiAuth

New wires all dependencies and returns a ChiAuth instance ready to mount.

r.Mount("/auth", chiauth.New(chiauth.Config{
    DB:        db,
    JWTSecret: "your-secret",
}).Router())

func (*ChiAuth) Authenticate

func (ca *ChiAuth) Authenticate() func(http.Handler) http.Handler

Authenticate returns Chi middleware that validates the Bearer JWT and injects the resolved *models.User into the request context. Use this on any route that requires authentication.

func (*ChiAuth) AuthenticateMiddleware

func (ca *ChiAuth) AuthenticateMiddleware() func(http.Handler) http.Handler

AuthenticateMiddleware returns the authenticate middleware bound to the live user store.

func (*ChiAuth) RequireAnyRole

func (ca *ChiAuth) RequireAnyRole(slugs ...string) func(http.Handler) http.Handler

RequireAnyRole returns middleware that passes if the user holds any of the given slugs.

func (*ChiAuth) RequirePermission

func (ca *ChiAuth) RequirePermission(codename string) func(http.Handler) http.Handler

RequirePermission returns middleware that gates on a permission codename. Chain after Authenticate.

r.With(ca.AuthenticateMiddleware(), ca.RequirePermission("invoice:delete")).Delete(...)

func (*ChiAuth) RequireRole

func (ca *ChiAuth) RequireRole(slug string) func(http.Handler) http.Handler

RequireRole returns middleware that gates on a role slug. Chain after Authenticate.

r.With(ca.AuthenticateMiddleware(), ca.RequireRole("admin")).Get(...)

func (*ChiAuth) RequireStaff

func (ca *ChiAuth) RequireStaff() func(http.Handler) http.Handler

RequireStaff returns middleware that allows only staff and superusers.

func (*ChiAuth) RequireSuperuser

func (ca *ChiAuth) RequireSuperuser() func(http.Handler) http.Handler

RequireSuperuser returns middleware that allows only superusers.

func (*ChiAuth) Router

func (ca *ChiAuth) Router() http.Handler

Router returns the fully mounted Chi router. Pass this to r.Mount("/auth", chiauth.New(cfg).Router()).

func (*ChiAuth) RunMigrations

func (ca *ChiAuth) RunMigrations() error

RunMigrations applies all chiauth SQL migrations to the database. Call this once during app startup, before mounting the router.

if err := ca.RunMigrations(); err != nil {
    log.Fatalf("chiauth migrations failed: %v", err)
}

func (*ChiAuth) SeedPermissions

func (ca *ChiAuth) SeedPermissions(ctx context.Context, permissions []models.Permission) error

SeedPermissions upserts a list of permissions into the database. Call this once on application startup. Idempotent.

ca.SeedPermissions(context.Background(), []models.Permission{
    {Resource: "invoice", Action: "create", Codename: "invoice:create", Description: "Create invoices"},
    {Resource: "invoice", Action: "delete", Codename: "invoice:delete", Description: "Delete invoices"},
})

func (*ChiAuth) SeedRoles

func (ca *ChiAuth) SeedRoles(ctx context.Context, inputs []models.SeedRoleInput) error

SeedRoles creates roles and assigns their permissions. Idempotent.

ca.SeedRoles(context.Background(), []models.SeedRoleInput{
    {
        Slug:        "accountant",
        Name:        "Accountant",
        Permissions: []string{"invoice:create", "invoice:delete"},
    },
})

type Config

type Config = config.Config

Config is re-exported from the config package for ergonomic top-level access.

Directories

Path Synopsis
Package config defines the configuration surface for chiauth.
Package config defines the configuration surface for chiauth.
Package docs contains the Swagger/OpenAPI specification for chiauth.
Package docs contains the Swagger/OpenAPI specification for chiauth.
Package email defines the EmailSender interface and built-in implementations.
Package email defines the EmailSender interface and built-in implementations.
Package errors defines all typed errors returned by chiauth services.
Package errors defines all typed errors returned by chiauth services.
This file is a complete example showing how to use chiauth in a real application.
This file is a complete example showing how to use chiauth in a real application.
Package handlers provides Chi HTTP handlers for all chiauth endpoints.
Package handlers provides Chi HTTP handlers for all chiauth endpoints.
internal
testutil
Package testutil provides in-memory mock implementations of all store and email interfaces.
Package testutil provides in-memory mock implementations of all store and email interfaces.
Package middleware provides Chi-compatible HTTP middleware for chiauth.
Package middleware provides Chi-compatible HTTP middleware for chiauth.
Package models defines all database models, DTOs, and domain types for chiauth.
Package models defines all database models, DTOs, and domain types for chiauth.
Package services contains all business logic for chiauth.
Package services contains all business logic for chiauth.
Package store defines the persistence interfaces for chiauth.
Package store defines the persistence interfaces for chiauth.
postgres
Package postgres provides the PostgreSQL implementation of all chiauth store interfaces.
Package postgres provides the PostgreSQL implementation of all chiauth store interfaces.

Jump to

Keyboard shortcuts

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