Security
go.putnami.dev/security provides declarative authentication and authorization middleware for HTTP endpoints.
Identity resolution
Before authorization, you need to populate ctx.User with the authenticated user's claims. Use IdentityResolver to extract the identity from the request:
import (
fhttp "go.putnami.dev/http"
"go.putnami.dev/security"
)
server := fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000})
server.Use(security.IdentityResolver(func(ctx *fhttp.Context) map[string]any {
token := ctx.Header("Authorization")
if token == "" {
return nil // not authenticated
}
claims, err := verifyJWT(strings.TrimPrefix(token, "Bearer "))
if err != nil {
return nil
}
return claims // e.g., {"sub": "user-123", "roles": ["admin"], "scope": "read write"}
}))The resolver sets ctx.User if authentication succeeds. Returning nil means the request is unauthenticated — downstream authorization middleware will return 401.
Declarative authorization
Use security.Options to define role, scope, and client-based access control:
import "go.putnami.dev/security"
// Require specific roles
server.GET("/admin", adminHandler)
server.Use(security.Middleware(security.Options{
Roles: []string{"admin"}, // ALL listed roles required
}))
// Require at least one role
security.Middleware(security.Options{
RolesAny: []string{"admin", "moderator"}, // at least ONE required
})
// Require scopes
security.Middleware(security.Options{
Scopes: []string{"read", "write"}, // ALL scopes required
})
// Require at least one scope
security.Middleware(security.Options{
ScopesAny: []string{"read", "write"}, // at least ONE required
})
// Restrict to specific client IDs
security.Middleware(security.Options{
Client: []string{"web-app", "mobile-app"},
})Combining rules
All rules in a single Options are AND'd together:
security.Middleware(security.Options{
Roles: []string{"admin"},
Scopes: []string{"write"},
Client: []string{"internal-service"},
})
// Must have admin role AND write scope AND be from internal-serviceOptions reference
| Field | Type | Logic | Description |
|---|---|---|---|
Roles |
[]string |
AND | All listed roles must be present |
RolesAny |
[]string |
OR | At least one listed role must be present |
Scopes |
[]string |
AND | All listed scopes must be present |
ScopesAny |
[]string |
OR | At least one listed scope must be present |
Client |
[]string |
OR | Client ID must match one of the listed values |
Custom guards
For authorization logic that goes beyond declarative rules, use a guard function:
// Only allow users to access their own resources
server.GET("/users/{userId}/profile", profileHandler)
server.Use(security.Middleware(security.Guard(
func(user map[string]any, ctx *fhttp.Context) bool {
return user["sub"] == ctx.Param("userId")
},
)))
// Or pass the function directly (auto-converted to Guard)
server.Use(security.Middleware(
func(user map[string]any, ctx *fhttp.Context) bool {
return user["sub"] == ctx.Param("userId")
},
))Guards receive the authenticated user claims and the request context. Return true to allow, false to deny with 403.
Per-route authorization
Apply authorization middleware to specific routes:
server := fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000})
// Global: identity resolution
server.Use(security.IdentityResolver(resolveUser))
// Public routes
server.GET("/health", healthHandler)
server.GET("/login", loginHandler)
// Protected routes use Chain
adminOnly := fhttp.Chain(
security.Middleware(security.Options{Roles: []string{"admin"}}),
)
server.GET("/admin/users", adminOnly(listUsersHandler))
server.DELETE("/admin/users/{id}", adminOnly(deleteUserHandler))Module-level security
Apply default security to all endpoints in a module:
import "go.putnami.dev/app"
admin := app.NewModule("admin")
admin.Path("/admin")
admin.Secure(&app.SecurityOptions{
Roles: []string{"admin"},
Scopes: []string{"admin:read"},
Client: []string{"internal-dashboard"},
})
a := app.New("my-service")
a.Module.Use(server)
a.Module.Use(admin)SecurityOptions
| Field | Type | Description |
|---|---|---|
Roles |
[]string |
All roles required |
RolesAny |
[]string |
At least one role required |
Scopes |
[]string |
All scopes required |
ScopesAny |
[]string |
At least one scope required |
Client |
[]string |
Allowed client IDs |
Claim formats
The security middleware handles multiple claim formats:
Roles
Extracted from claims["roles"]:
// Slice of strings
{"roles": []string{"admin", "user"}}
// Slice of any
{"roles": []any{"admin", "user"}}
// Space-separated string
{"roles": "admin user moderator"}Scopes
Extracted from claims["scope"] (OAuth2 convention):
// Slice of strings
{"scope": []string{"read", "write"}}
// Space-separated string (OAuth2 standard)
{"scope": "read write delete"}Client ID
Extracted from claims["client_id"]:
{"client_id": "web-app"}HTTP responses
| Status | When |
|---|---|
401 Unauthorized |
ctx.User is nil (no identity resolved) |
403 Forbidden |
Authorization check failed |
Related guides
- HTTP & Middleware — middleware registration
- Plugins & Lifecycle — module-level security