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
| Method | Description |
|---|---|
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
| Middleware | Description |
|---|---|
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
| Method | Description |
|---|---|
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{})