Auth & Guards

Nimbus provides a full authentication and authorization system with four guards (Session, Token, Stateless, BasicAuth), a Gate for fine-grained authorization, personal access tokens, and middleware. Use nimbus make:auth to scaffold everything.

Quick Start

nimbus make:auth

Scaffolds User model, AuthController, login/register views, and migration. Add the migration to your registry, wire session middleware and auth routes, run nimbus db:migrate.

User Interface

All guards work with any type implementing auth.User:

type User struct {
    database.Model
    Name         string
    Email        string
    PasswordHash string `json:"-"`
}

// Implements auth.User
func (u *User) GetID() string { return fmt.Sprintf("%d", u.ID) }

// UserLoader for guards
func UserByID(db *gorm.DB) auth.UserLoaderFunc {
    return func(ctx context.Context, id string) (auth.User, error) {
        var user User
        err := db.First(&user, id).Error
        return &user, err
    }
}

Guards

Session Guard

Cookie-based authentication using the session store. Requires session.Middleware.

// With DB loader (recommended — loads user from DB each request)
guard := auth.NewSessionGuardWithLoader(models.UserByID(database.DB))

// In-memory (development only — sessions lost on restart)
guard := auth.NewSessionGuard()

// Wire routes
authCtrl := &controllers.AuthController{DB: database.DB, Guard: guard}
app.Router.Get("/login", authCtrl.LoginForm)
app.Router.Post("/login", authCtrl.Login)
app.Router.Post("/logout", authCtrl.Logout)

// Protected group
dashboard := app.Router.Group("/dashboard", auth.RequireAuth(guard, "/login"))

Stateless Guard (JWT/PASETO)

Token-based authentication that doesn't require database lookups for verification. Supports JWT and PASETO drivers.

// Create a stateless guard with JWT driver
driver := auth.NewJWTDriver(config.Auth.Stateless.Secret)
guard := auth.NewStatelessGuard(driver, models.UserByID(database.DB))

// Use in routes
api := app.Router.Group("/api", auth.RequireStatelessToken(guard))

Guard API

MethodDescription
User(ctx)Returns the authenticated user, or nil
Login(ctx, user)Logs the user in (regenerates session, stores user_id)
Logout(ctx)Logs the user out (clears user_id from session)

Middleware

MiddlewareDescription
RequireAuth(guard, redirectTo)Requires authenticated user. Redirects to redirectTo for web, or returns 401 JSON if empty
// Web — redirect to login
app.Router.Group("/dashboard", auth.RequireAuth(guard, "/login"))

// API — return 401 JSON
app.Router.Group("/api", auth.RequireAuth(guard, ""))

Context Helpers

// Get user in a handler (set by RequireAuth middleware)
user := auth.UserFromContext(c.Request.Context())
if user != nil {
    fmt.Println("User ID:", user.GetID())
}

// Manually set user on context
ctx := auth.WithUser(c.Request.Context(), user)

Gate (Authorization)

The Gate is the central authorization system. Define abilities, register policies, and check permissions:

Defining Abilities

gate := auth.DefaultGate()

gate.Define("edit-post", func(ctx context.Context, user auth.User, resource any) bool {
    post := resource.(*models.Post)
    return user.GetID() == fmt.Sprintf("%d", post.UserID)
})

gate.Define("delete-post", func(ctx context.Context, user auth.User, resource any) bool {
    post := resource.(*models.Post)
    return user.GetID() == fmt.Sprintf("%d", post.UserID)
})

// Or use the shortcut
auth.DefineAbility("admin-only", func(ctx context.Context, user auth.User, resource any) bool {
    return user.(*models.User).Role == "admin"
})

Checking Abilities

// In a controller
func UpdatePost(c *http.Context) error {
    post := getPost(c)

    // Using default gate with context user
    if auth.Cannot(c.Request.Context(), "edit-post", post) {
        return c.JSON(403, map[string]string{"error": "forbidden"})
    }

    // Or using gate directly
    gate := auth.DefaultGate()
    if gate.Denies(c.Request.Context(), user, "edit-post", post) {
        return errors.New("forbidden")
    }

    // Authorize — returns error if denied
    if err := auth.AuthorizeAction(c.Request.Context(), "edit-post", post); err != nil {
        return c.JSON(403, map[string]string{"error": err.Error()})
    }

    // ...update post
}

Gate API

MethodDescription
Define(ability, fn)Register a named ability check
RegisterPolicy(name, policy)Register a resource policy
Before(fn)Global hook before every check (return *bool to override)
After(fn)Global hook after every check (for logging/auditing)
Allows(ctx, user, ability, resource)Returns true if authorized
Denies(ctx, user, ability, resource)Returns true if NOT authorized
Authorize(ctx, user, ability, resource)Returns error if denied
Any(ctx, user, abilities, resource)True if user can do ANY of the abilities
None(ctx, user, abilities, resource)True if user can do NONE of the abilities
ForUser(user)Returns a UserGate scoped to one user

Before / After Hooks

gate := auth.DefaultGate()

// Super-admin bypass — return AllowAll() to short-circuit
gate.Before(func(ctx context.Context, user auth.User, ability string) *bool {
    if user.(*models.User).Role == "super-admin" {
        return auth.AllowAll()
    }
    return nil // continue to normal check
})

// Audit logging
gate.After(func(ctx context.Context, user auth.User, ability string, result bool) {
    logger.Info("auth check", "user", user.GetID(), "ability", ability, "allowed", result)
})

UserGate (Scoped)

ug := gate.ForUser(user)
if ug.Can(ctx, "edit-post", post) {
    // authorized
}
if ug.Cannot(ctx, "delete-post", post) {
    // denied
}
err := ug.Authorize(ctx, "publish-post", post)

Convenience Functions

These use the default gate and pull the user from context:

// Check against default gate (uses auth.UserFromContext)
auth.Can(ctx, "edit-post", post)
auth.Cannot(ctx, "edit-post", post)
auth.AuthorizeAction(ctx, "edit-post", post)

// Register on default gate
auth.DefineAbility("edit-post", fn)

Policies

Policies group authorization logic for a resource type. Implement the Policy interface:

type PostPolicy struct{}

func (p *PostPolicy) Allow(ctx context.Context, user auth.User, action string, resource any) bool {
    post := resource.(*models.Post)
    switch action {
    case "view":
        return true // anyone can view
    case "update", "delete":
        return user.GetID() == fmt.Sprintf("%d", post.UserID)
    case "publish":
        return user.(*models.User).Role == "editor" || user.(*models.User).Role == "admin"
    }
    return false
}

// Register
gate.RegisterPolicy("post", &PostPolicy{})