Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Security

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-service

Options 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

On this page

  • Security
  • Identity resolution
  • Declarative authorization
  • Combining rules
  • Options reference
  • Custom guards
  • Per-route authorization
  • Module-level security
  • SecurityOptions
  • Claim formats
  • Roles
  • Scopes
  • Client ID
  • HTTP responses
  • Related guides