zoobzio January 27, 2026 Edit this page

Authentication & Authorization Guide

Rocco provides built-in support for identity extraction, scope-based authorization, role-based access control, and usage limits.

Identity Interface

Implement the Identity interface to represent authenticated users:

type Identity interface {
    ID() string              // Unique user/service identifier
    TenantID() string        // Tenant/organization ID (multi-tenancy)
    Email() string           // Email address
    Scopes() []string        // All granted scopes
    Roles() []string         // All assigned roles
    HasScope(string) bool    // Check if identity has scope
    HasRole(string) bool     // Check if identity has role
    Stats() map[string]int   // Usage statistics for rate limiting
}

Example Implementation

type UserIdentity struct {
    id       string
    tenantID string
    email    string
    scopes   []string
    roles    []string
    stats    map[string]int
}

func (u *UserIdentity) ID() string            { return u.id }
func (u *UserIdentity) TenantID() string      { return u.tenantID }
func (u *UserIdentity) Email() string         { return u.email }
func (u *UserIdentity) Scopes() []string      { return u.scopes }
func (u *UserIdentity) Roles() []string       { return u.roles }
func (u *UserIdentity) Stats() map[string]int { return u.stats }

func (u *UserIdentity) HasScope(scope string) bool {
    for _, s := range u.scopes {
        if s == scope {
            return true
        }
    }
    return false
}

func (u *UserIdentity) HasRole(role string) bool {
    for _, r := range u.roles {
        if r == role {
            return true
        }
    }
    return false
}

Identity Extraction

Provide an identity extractor when creating the engine:

engine := rocco.NewEngine().WithAuthenticator(extractIdentity)

func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    // Get token from header
    token := r.Header.Get("Authorization")
    if token == "" {
        return nil, errors.New("missing authorization header")
    }

    // Validate token (JWT, session lookup, API key, etc.)
    claims, err := validateToken(strings.TrimPrefix(token, "Bearer "))
    if err != nil {
        return nil, err
    }

    // Return identity
    return &UserIdentity{
        id:       claims.Subject,
        tenantID: claims.TenantID,
        scopes:   claims.Scopes,
        roles:    claims.Roles,
        stats:    fetchStats(claims.Subject),
    }, nil
}

JWT Example

func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    auth := r.Header.Get("Authorization")
    if !strings.HasPrefix(auth, "Bearer ") {
        return nil, errors.New("invalid authorization header")
    }

    tokenString := strings.TrimPrefix(auth, "Bearer ")

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte(secretKey), nil
    })
    if err != nil {
        return nil, err
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        return nil, errors.New("invalid token")
    }

    return &UserIdentity{
        id:       claims["sub"].(string),
        tenantID: claims["tenant_id"].(string),
        scopes:   parseScopes(claims["scope"]),
        roles:    parseRoles(claims["roles"]),
    }, nil
}

API Key Example

func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    apiKey := r.Header.Get("X-API-Key")
    if apiKey == "" {
        return nil, errors.New("missing API key")
    }

    // Look up API key in database
    keyInfo, err := db.GetAPIKey(apiKey)
    if err != nil {
        return nil, errors.New("invalid API key")
    }

    return &ServiceIdentity{
        id:       keyInfo.ServiceID,
        tenantID: keyInfo.TenantID,
        scopes:   keyInfo.Scopes,
    }, nil
}

Auth0 Integration

For Auth0-based authentication, use the rocco/auth0 package:

import "github.com/zoobz-io/rocco/auth0"

extractor, err := auth0.NewExtractor(auth0.Config{
    Domain:   "your-tenant.auth0.com",
    Audience: "https://your-api.example.com",
})
if err != nil {
    log.Fatal(err)
}

engine := rocco.NewEngine().WithAuthenticator(extractor)

The auth0 package handles:

  • JWKS fetching and caching
  • RS256 signature validation
  • Issuer and audience validation
  • Token expiration checks
  • Claim extraction to rocco.Identity

For custom claim paths (Auth0 namespaced claims):

auth0.Config{
    Domain:      "your-tenant.auth0.com",
    Audience:    "https://your-api.example.com",
    RolesClaim:  "https://myapp.com/roles",   // Custom roles claim
    TenantClaim: "https://myapp.com/org_id",  // Multi-tenancy claim
}

See the Authentication Cookbook for complete Auth0 examples including user enrichment patterns.

Session-Based OAuth

The rocco/session package provides cookie-based session management with built-in OAuth support. The rocco/oauth package handles the protocol layer (authorization URLs, token exchange, refresh).

import (
    "github.com/zoobz-io/rocco/oauth"
    "github.com/zoobz-io/rocco/session"
)

store := session.NewMemoryStore() // Use Redis/database in production

cfg := session.Config{
    OAuth: oauth.GitHub(),
    Store: store,
    Cookie: session.CookieConfig{
        SignKey: []byte(os.Getenv("SESSION_KEY")),
    },
    Resolve: func(ctx context.Context, tokens *oauth.TokenResponse) (*session.Data, error) {
        // Call provider API to get user info, build session data.
        user, err := fetchUser(ctx, tokens.AccessToken)
        if err != nil {
            return nil, err
        }
        return &session.Data{
            UserID: user.ID,
            Email:  user.Email,
            Roles:  user.Roles,
        }, nil
    },
    RedirectURL: "/dashboard",
}
cfg.OAuth.ClientID = os.Getenv("GITHUB_CLIENT_ID")
cfg.OAuth.ClientSecret = os.Getenv("GITHUB_CLIENT_SECRET")
cfg.OAuth.RedirectURI = "https://myapp.com/auth/callback"
cfg.OAuth.Scopes = []string{"read:user", "read:org"}

login, _ := session.NewLoginHandler("/auth/login", cfg)
callback, _ := session.NewCallbackHandler("/auth/callback", cfg)
logout, _ := session.NewLogoutHandler("/auth/logout", cfg, "/")

engine := rocco.NewEngine()
engine.WithAuthenticator(session.Extractor(store, cfg.Cookie))
engine.WithHandlers(login, callback, logout)

The session package handles:

  • CSRF state generation and verification (single-use)
  • Token exchange via rocco/oauth
  • Session creation with signed cookies (HMAC-SHA256)
  • Identity extraction from session cookies
  • Logout with session deletion and cookie clearing

The Resolve function is where the application maps tokens to session data — call the provider's user-info API, look up roles in a database, etc.

The oauth package is provider-agnostic. Use oauth.GitHub() for GitHub, oauth.GitHubEnterprise(baseURL) for GHE, or configure custom endpoints for any OAuth 2.0 provider.

For token refresh:

newTokens, err := oauth.Refresh(ctx, cfg.OAuth, oldRefreshToken)
if err != nil {
    // Refresh failed - user must re-authenticate
}

See the Authentication Cookbook for complete examples.

Requiring Authentication

Mark handlers as requiring authentication:

handler := rocco.NewHandler[rocco.NoBody, User](
    "get-profile",
    "GET",
    "/profile",
    func(req *rocco.Request[rocco.NoBody]) (User, error) {
        // req.Identity is guaranteed to be non-nil
        return getUser(req.Identity.ID())
    },
).WithAuthentication()

Unauthenticated requests receive 401 Unauthorized.

Scope-Based Authorization

Require specific scopes:

// Single scope required
handler.WithScopes("users:read")

// Any of these scopes (OR logic)
handler.WithScopes("users:read", "users:admin")

// Multiple scope groups (AND logic)
handler.WithScopes("users:read").WithScopes("verified")
// Requires (users:read OR users:admin) AND verified

Scope Patterns

Common scope naming conventions:

// Resource-based
handler.WithScopes("users:read")
handler.WithScopes("users:write")
handler.WithScopes("orders:read", "orders:write")

// Action-based
handler.WithScopes("read")
handler.WithScopes("write")
handler.WithScopes("delete")

// Feature-based
handler.WithScopes("analytics")
handler.WithScopes("reports")
handler.WithScopes("admin")

Role-Based Access Control

Require specific roles:

// Single role required
handler.WithRoles("admin")

// Any of these roles (OR logic)
handler.WithRoles("admin", "moderator")

// Multiple role groups (AND logic)
handler.WithRoles("admin").WithRoles("verified")
// Requires (admin OR moderator) AND verified

Combining Scopes and Roles

handler := rocco.NewHandler[rocco.NoBody, AdminData](
    "admin-dashboard",
    "GET",
    "/admin/dashboard",
    getAdminDashboard,
).
    WithScopes("admin:read").   // Must have admin:read scope
    WithRoles("admin", "super") // Must have admin OR super role

Usage Limits

Rate limit based on identity statistics:

handler := rocco.NewHandler[Input, Output](
    "create-resource",
    "POST",
    "/resources",
    createResource,
).WithUsageLimit("api_calls_today", func(id rocco.Identity) int {
    // Return limit based on identity
    if id.HasRole("premium") {
        return 10000
    }
    return 100
})

The handler checks identity.Stats()["api_calls_today"] against the limit. If exceeded, returns 429 Too Many Requests.

Multiple Limits

handler.
    WithUsageLimit("api_calls_today", dailyLimit).
    WithUsageLimit("storage_mb", storageLimit).
    WithUsageLimit("concurrent_jobs", concurrencyLimit)

Accessing Identity in Handlers

func(req *rocco.Request[Input]) (Output, error) {
    // Get identity (nil if handler doesn't require auth)
    identity := req.Identity

    // Check auth status
    if identity == nil {
        // Handler allows anonymous access
    }

    // Access identity data
    userID := identity.ID()
    tenantID := identity.TenantID()

    // Check permissions programmatically
    if identity.HasScope("admin:write") {
        // Admin logic
    }

    // Multi-tenancy: filter by tenant
    users, _ := db.FindUsers(tenantID)

    return Output{...}, nil
}

NoIdentity Type

For handlers that don't require authentication, req.Identity is a NoIdentity:

type NoIdentity struct{}

func (NoIdentity) ID() string            { return "" }
func (NoIdentity) TenantID() string      { return "" }
func (NoIdentity) Email() string         { return "" }
func (NoIdentity) Scopes() []string      { return nil }
func (NoIdentity) Roles() []string       { return nil }
func (NoIdentity) HasScope(string) bool  { return false }
func (NoIdentity) HasRole(string) bool   { return false }
func (NoIdentity) Stats() map[string]int { return nil }

Error Responses

ScenarioStatusError
No/invalid identity401ErrUnauthorized
Missing required scope403ErrForbidden (message: "insufficient scope")
Missing required role403ErrForbidden (message: "insufficient role")
Usage limit exceeded429ErrTooManyRequests

Events

Authentication events are emitted via capitan:

EventDescription
AuthenticationFailedIdentity extraction failed
AuthenticationSucceededIdentity extracted successfully
AuthorizationScopeDeniedScope check failed
AuthorizationRoleDeniedRole check failed
AuthorizationSucceededAuthorization passed
RateLimitExceededUsage limit exceeded

Best Practices

1. Use HTTPS in Production

Rocco doesn't handle TLS. Use a reverse proxy (nginx, Caddy) or cloud load balancer.

2. Validate Tokens Properly

// Use constant-time comparison
// Set reasonable expiration times
// Validate issuer and audience

3. Implement Token Refresh

Don't rely on long-lived tokens. Implement refresh token flows.

4. Log Authentication Failures

capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {
    path, _ := rocco.PathKey.From(e)
    err, _ := rocco.ErrorKey.From(e)
    securityLog.Warn("auth failed", "path", path, "error", err)
})

5. Multi-Tenancy

Always filter data by tenant:

func(req *rocco.Request[Input]) (Output, error) {
    tenantID := req.Identity.TenantID()

    // ALWAYS filter by tenant
    data, _ := db.Query("SELECT * FROM items WHERE tenant_id = ?", tenantID)

    return Output{...}, nil
}

See Also