zoobzio January 27, 2026 Edit this page

Authentication Cookbook

Implementation patterns for common authentication scenarios.

Auth0 Authentication

The rocco/auth0 package provides drop-in Auth0 JWT authentication.

Basic Setup

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

func main() {
    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)

    engine.WithHandlers(
        rocco.NewHandler[rocco.NoBody, UserResponse](
            "get-profile", "GET", "/profile",
            func(req *rocco.Request[rocco.NoBody]) (UserResponse, error) {
                return UserResponse{
                    ID:    req.Identity.ID(),
                    Email: req.Identity.Email(),
                    Roles: req.Identity.Roles(),
                }, nil
            },
        ).WithAuthentication(),
    )

    engine.Start(rocco.HostAll, 8080)
}

Custom Claims (Namespaced)

Auth0 requires namespaced custom claims. Configure the claim paths:

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

User Enrichment

Enrich JWT identity with database user data:

auth0Validator, _ := auth0.NewValidator(cfg)

extractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    // Validate JWT
    identity, err := auth0Validator.Extractor()(ctx, r)
    if err != nil {
        return nil, err
    }

    // Look up or create user in database
    user, err := userRepo.FindOrCreateBySubject(ctx, identity.ID(), identity.Email())
    if err != nil {
        return nil, err
    }

    return user, nil  // User implements rocco.Identity
}

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

Your User type implements rocco.Identity:

type User struct {
    id            string
    email         string
    tenantID      string
    roles         []string
    subscription  string  // app-specific data
    requestsToday int     // for rate limiting
}

func (u *User) ID() string            { return u.id }
func (u *User) TenantID() string      { return u.tenantID }
func (u *User) Email() string         { return u.email }
func (u *User) Scopes() []string      { return nil }
func (u *User) Roles() []string       { return u.roles }
func (u *User) HasScope(s string) bool { return false }
func (u *User) HasRole(r string) bool {
    for _, role := range u.roles {
        if role == r {
            return true
        }
    }
    return false
}
func (u *User) Stats() map[string]int {
    return map[string]int{"requests_today": u.requestsToday}
}
func (u *User) Subscription() string { return u.subscription }

// Helper for type-safe access in handlers
func UserFrom(id rocco.Identity) *User {
    u, _ := id.(*User)
    return u
}

Usage in handlers:

func(req *rocco.Request[Input]) (Output, error) {
    user := UserFrom(req.Identity)
    if user.Subscription() == "free" {
        // apply free tier limits
    }
    // ...
}

Testing with Auth0

For testing, use rocco/testing.MockIdentity instead of real JWTs:

import (
    "net/http/httptest"
    "testing"

    "github.com/zoobz-io/rocco"
    roccotest "github.com/zoobz-io/rocco/testing"
)

func TestProtectedEndpoint(t *testing.T) {
    // Create mock identity extractor
    mockIdentity := roccotest.NewMockIdentity("user-123").
        WithEmail("test@example.com").
        WithRoles("admin")

    extractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {
        return mockIdentity, nil
    }

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

    req := httptest.NewRequest("GET", "/protected", nil)
    w := httptest.NewRecorder()
    engine.Router().ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", w.Code)
    }
}

For integration tests requiring actual JWT validation, create a mock JWKS server:

import (
    "crypto/rand"
    "crypto/rsa"
    "encoding/json"
    "net/http/httptest"

    "github.com/golang-jwt/jwt/v5"
    "github.com/zoobz-io/rocco/auth0"
)

func setupMockJWKS(t *testing.T) (*httptest.Server, *rsa.PrivateKey) {
    t.Helper()
    privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)

    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        jwks := map[string]any{
            "keys": []map[string]any{{
                "kty": "RSA",
                "kid": "test-key",
                "use": "sig",
                "n":   base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()),
                "e":   "AQAB",
            }},
        }
        json.NewEncoder(w).Encode(jwks)
    }))
    t.Cleanup(server.Close)

    return server, privateKey
}

func generateTestToken(t *testing.T, key *rsa.PrivateKey, subject string) string {
    t.Helper()
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
        "sub": subject,
        "iss": "https://test.auth0.com/",
        "aud": "https://api.test.com",
        "exp": time.Now().Add(time.Hour).Unix(),
    })
    token.Header["kid"] = "test-key"
    signed, _ := token.SignedString(key)
    return signed
}

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.

Complete OAuth Flow

import (
    "context"
    "os"

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

func main() {
    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 build session data
            user, err := fetchGitHubUser(ctx, tokens.AccessToken)
            if err != nil {
                return nil, err
            }
            return &session.Data{
                UserID: user.ID,
                Email:  user.Email,
                Roles:  user.Roles,
                Meta:   map[string]any{"access_token": tokens.AccessToken},
            }, nil
        },
        RedirectURL: "/profile",
    }
    cfg.OAuth.ClientID = os.Getenv("GITHUB_CLIENT_ID")
    cfg.OAuth.ClientSecret = os.Getenv("GITHUB_CLIENT_SECRET")
    cfg.OAuth.RedirectURI = "http://localhost:8080/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))

    // Public handlers
    engine.WithHandlers(login, callback, logout)

    // Protected handlers
    engine.WithHandlers(
        rocco.GET[rocco.NoBody, ProfileResponse]("/profile",
            func(req *rocco.Request[rocco.NoBody]) (ProfileResponse, error) {
                return ProfileResponse{
                    ID:    req.Identity.ID(),
                    Email: req.Identity.Email(),
                    Roles: req.Identity.Roles(),
                }, nil
            },
        ).WithAuthentication(),
    )

    engine.Start("", 8080)
}

type ProfileResponse struct {
    ID    string   `json:"id"`
    Email string   `json:"email"`
    Roles []string `json:"roles"`
}

The flow:

  1. User visits /auth/login → redirected to GitHub
  2. User authorizes → GitHub redirects to /auth/callback
  3. Callback exchanges code for tokens, calls Resolve, creates session, sets cookie, redirects to /profile
  4. /profile reads session cookie, loads identity from store, returns user data
  5. User visits /auth/logout → session deleted, cookie cleared, redirected to /

Custom OAuth Provider

The oauth package is provider-agnostic. Configure any OAuth 2.0 provider:

cfg.OAuth = oauth.Config{
    Name:         "my-provider",
    AuthURL:      "https://provider.com/authorize",
    TokenURL:     "https://provider.com/token",
    ClientID:     os.Getenv("CLIENT_ID"),
    ClientSecret: os.Getenv("CLIENT_SECRET"),
    RedirectURI:  "https://myapp.com/auth/callback",
    Scopes:       []string{"openid", "profile", "email"},
}

For GitHub Enterprise:

cfg.OAuth = oauth.GitHubEnterprise("https://github.mycompany.com")

Custom Session Store

Implement session.Store for your backend:

type RedisStore struct {
    client *redis.Client
    ttl    time.Duration
}

func (s *RedisStore) CreateState(ctx context.Context, state string) error {
    return s.client.Set(ctx, "state:"+state, "1", 10*time.Minute).Err()
}

func (s *RedisStore) VerifyState(ctx context.Context, state string) (bool, error) {
    result, err := s.client.Del(ctx, "state:"+state).Result()
    if err != nil {
        return false, err
    }
    return result > 0, nil
}

func (s *RedisStore) Create(ctx context.Context, id string, data session.Data) error {
    b, _ := json.Marshal(data)
    return s.client.Set(ctx, "session:"+id, b, s.ttl).Err()
}

func (s *RedisStore) Get(ctx context.Context, id string) (*session.Data, error) {
    b, err := s.client.Get(ctx, "session:"+id).Bytes()
    if err != nil {
        return nil, err
    }
    var data session.Data
    return &data, json.Unmarshal(b, &data)
}

func (s *RedisStore) Refresh(ctx context.Context, id string) error {
    return s.client.Expire(ctx, "session:"+id, s.ttl).Err()
}

func (s *RedisStore) Delete(ctx context.Context, id string) error {
    return s.client.Del(ctx, "session:"+id).Err()
}

Token Refresh

Use oauth.Refresh to exchange a refresh token for new tokens:

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

Testing with Sessions

For unit tests, use rocco/testing.MockIdentity:

func TestProtectedEndpoint(t *testing.T) {
    mockIdentity := roccotest.NewMockIdentity("user-123").
        WithEmail("dev@example.com").
        WithRoles("admin")

    extractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {
        return mockIdentity, nil
    }

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

For integration tests, use session.NewMemoryStore with a mock OAuth server:

func TestOAuthFlow(t *testing.T) {
    // Mock OAuth token endpoint
    mockProvider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(oauth.TokenResponse{
            AccessToken: "test-token",
            TokenType:   "Bearer",
        })
    }))
    defer mockProvider.Close()

    store := session.NewMemoryStore()
    cfg := session.Config{
        OAuth: oauth.Config{
            Name:         "test",
            AuthURL:      "https://provider.com/auth",
            TokenURL:     mockProvider.URL,
            ClientID:     "test-id",
            ClientSecret: "test-secret",
            RedirectURI:  "http://localhost/callback",
        },
        Store:  store,
        Cookie: session.CookieConfig{SignKey: []byte("test-key")},
        Resolve: func(ctx context.Context, tokens *oauth.TokenResponse) (*session.Data, error) {
            return &session.Data{UserID: "user-1", Email: "test@example.com"}, nil
        },
        RedirectURL: "/dashboard",
    }

    callback, _ := session.NewCallbackHandler("/callback", cfg)
    // Pre-create state, then test the callback handler...
}

JWT Authentication (Manual)

For non-Auth0 JWT providers or custom JWT validation.

Identity Implementation

type JWTIdentity struct {
    userID   string
    tenantID string
    email    string
    scopes   []string
    roles    []string
}

func (i *JWTIdentity) ID() string            { return i.userID }
func (i *JWTIdentity) TenantID() string      { return i.tenantID }
func (i *JWTIdentity) Email() string         { return i.email }
func (i *JWTIdentity) Scopes() []string      { return i.scopes }
func (i *JWTIdentity) Roles() []string       { return i.roles }
func (i *JWTIdentity) Stats() map[string]int { return nil }

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

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

Identity Extractor

import (
    "context"
    "errors"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
    "github.com/zoobz-io/rocco"
)

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

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

    // Validate Bearer format
    if !strings.HasPrefix(auth, "Bearer ") {
        return nil, errors.New("invalid authorization format")
    }

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

    // Parse and validate token
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        // Validate signing method
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, errors.New("invalid signing method")
        }
        return jwtSecret, nil
    })

    if err != nil {
        return nil, err
    }

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

    // Extract claims
    return &JWTIdentity{
        userID:   claims["sub"].(string),
        tenantID: getStringClaim(claims, "tenant_id"),
        email:    getStringClaim(claims, "email"),
        scopes:   getStringSliceClaim(claims, "scope"),
        roles:    getStringSliceClaim(claims, "roles"),
    }, nil
}

func getStringClaim(claims jwt.MapClaims, key string) string {
    if v, ok := claims[key].(string); ok {
        return v
    }
    return ""
}

func getStringSliceClaim(claims jwt.MapClaims, key string) []string {
    switch v := claims[key].(type) {
    case []interface{}:
        result := make([]string, len(v))
        for i, item := range v {
            result[i] = item.(string)
        }
        return result
    case string:
        return strings.Split(v, " ")
    }
    return nil
}

Login Handler

type LoginInput struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

type LoginOutput struct {
    Token     string `json:"token"`
    ExpiresAt int64  `json:"expires_at"`
}

var loginHandler = rocco.NewHandler[LoginInput, LoginOutput](
    "login",
    "POST",
    "/auth/login",
    func(req *rocco.Request[LoginInput]) (LoginOutput, error) {
        // Verify credentials
        user, err := db.GetUserByEmail(req.Body.Email)
        if err != nil {
            return LoginOutput{}, rocco.ErrUnauthorized.WithMessage("invalid credentials")
        }

        if !verifyPassword(req.Body.Password, user.PasswordHash) {
            return LoginOutput{}, rocco.ErrUnauthorized.WithMessage("invalid credentials")
        }

        // Generate token
        expiresAt := time.Now().Add(24 * time.Hour)
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
            "sub":       user.ID,
            "tenant_id": user.TenantID,
            "email":     user.Email,
            "scope":     strings.Join(user.Scopes, " "),
            "roles":     user.Roles,
            "exp":       expiresAt.Unix(),
            "iat":       time.Now().Unix(),
        })

        tokenString, err := token.SignedString(jwtSecret)
        if err != nil {
            return LoginOutput{}, rocco.ErrInternalServer.WithCause(err)
        }

        return LoginOutput{
            Token:     tokenString,
            ExpiresAt: expiresAt.Unix(),
        }, nil
    },
).
    WithSummary("User login").
    WithTags("auth").
    WithErrors(rocco.ErrUnauthorized)

Setup

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

// Public endpoints
engine.WithHandlers(loginHandler)

// Protected endpoints
engine.WithHandlers(
    rocco.NewHandler[rocco.NoBody, User](
        "get-profile",
        "GET",
        "/profile",
        getProfile,
    ).WithAuthentication(),
)

API Key Authentication

Identity Implementation

type APIKeyIdentity struct {
    keyID    string
    tenantID string
    name     string
    scopes   []string
}

func (i *APIKeyIdentity) ID() string            { return i.keyID }
func (i *APIKeyIdentity) TenantID() string      { return i.tenantID }
func (i *APIKeyIdentity) Email() string         { return "" }
func (i *APIKeyIdentity) Scopes() []string      { return i.scopes }
func (i *APIKeyIdentity) Roles() []string       { return nil }
func (i *APIKeyIdentity) Stats() map[string]int { return nil }

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

func (i *APIKeyIdentity) HasRole(string) bool { return false }

Identity Extractor

func extractAPIKeyIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    // Check header
    apiKey := r.Header.Get("X-API-Key")
    if apiKey == "" {
        // Fall back to query param (for webhooks)
        apiKey = r.URL.Query().Get("api_key")
    }

    if apiKey == "" {
        return nil, errors.New("missing API key")
    }

    // Validate key format
    if !strings.HasPrefix(apiKey, "sk_") {
        return nil, errors.New("invalid API key format")
    }

    // Look up key (use constant-time comparison in production)
    keyInfo, err := db.GetAPIKey(hashAPIKey(apiKey))
    if err != nil {
        return nil, errors.New("invalid API key")
    }

    // Check if key is active
    if !keyInfo.Active {
        return nil, errors.New("API key disabled")
    }

    // Check expiration
    if keyInfo.ExpiresAt != nil && keyInfo.ExpiresAt.Before(time.Now()) {
        return nil, errors.New("API key expired")
    }

    return &APIKeyIdentity{
        keyID:    keyInfo.ID,
        tenantID: keyInfo.TenantID,
        name:     keyInfo.Name,
        scopes:   keyInfo.Scopes,
    }, nil
}

API Key Management

type CreateAPIKeyInput struct {
    Name   string   `json:"name" validate:"required,min=1,max=100"`
    Scopes []string `json:"scopes" validate:"required,min=1"`
}

type APIKeyOutput struct {
    ID        string    `json:"id"`
    Key       string    `json:"key,omitempty"` // Only returned on creation
    Name      string    `json:"name"`
    Scopes    []string  `json:"scopes"`
    CreatedAt time.Time `json:"created_at"`
}

var createAPIKey = rocco.NewHandler[CreateAPIKeyInput, APIKeyOutput](
    "create-api-key",
    "POST",
    "/api-keys",
    func(req *rocco.Request[CreateAPIKeyInput]) (APIKeyOutput, error) {
        // Generate key
        key := "sk_" + generateSecureRandom(32)
        keyHash := hashAPIKey(key)

        keyInfo := &APIKey{
            ID:        generateID(),
            KeyHash:   keyHash,
            TenantID:  req.Identity.TenantID(),
            Name:      req.Body.Name,
            Scopes:    req.Body.Scopes,
            CreatedAt: time.Now(),
        }

        if err := db.CreateAPIKey(keyInfo); err != nil {
            return APIKeyOutput{}, rocco.ErrInternalServer.WithCause(err)
        }

        return APIKeyOutput{
            ID:        keyInfo.ID,
            Key:       key, // Only returned once!
            Name:      keyInfo.Name,
            Scopes:    keyInfo.Scopes,
            CreatedAt: keyInfo.CreatedAt,
        }, nil
    },
).
    WithAuthentication().
    WithScopes("api-keys:write").
    WithSuccessStatus(201)

Session Authentication

For session-based authentication, use the rocco/session package. It provides a session.Identity implementation, cookie management with HMAC-SHA256 signing, and a pluggable session.Store interface.

See the Session-Based OAuth section above for complete setup. The session.Extractor function handles cookie reading, signature verification, and session lookup automatically.

For custom session needs beyond OAuth (e.g., password-based login), use the store and cookie primitives directly:

store := session.NewMemoryStore()
cookie := session.CookieConfig{SignKey: []byte(os.Getenv("SESSION_KEY"))}

// In your login handler, create a session manually:
sessionID, _ := generateID()
store.Create(ctx, sessionID, session.Data{
    UserID: user.ID,
    Email:  user.Email,
    Roles:  user.Roles,
})

// Use the same extractor for identity extraction:
engine.WithAuthenticator(session.Extractor(store, cookie))

Multi-Auth Support

Support multiple authentication methods:

func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    // Try JWT first
    if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
        return extractJWTIdentity(ctx, r)
    }

    // Try API key
    if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
        return extractAPIKeyIdentity(ctx, r)
    }

    // Try session cookie
    if _, err := r.Cookie("sid"); err == nil {
        return session.Extractor(store, cookieCfg)(ctx, r)
    }

    return nil, errors.New("no authentication provided")
}

Usage Limits with Stats

type PlanBasedIdentity struct {
    userID   string
    tenantID string
    email    string
    plan     string // "free", "pro", "enterprise"
    scopes   []string
    roles    []string
    stats    map[string]int
}

func (i *PlanBasedIdentity) ID() string            { return i.userID }
func (i *PlanBasedIdentity) TenantID() string      { return i.tenantID }
func (i *PlanBasedIdentity) Email() string         { return i.email }
func (i *PlanBasedIdentity) Scopes() []string      { return i.scopes }
func (i *PlanBasedIdentity) Roles() []string       { return i.roles }
func (i *PlanBasedIdentity) HasScope(s string) bool {
    for _, scope := range i.scopes {
        if scope == s {
            return true
        }
    }
    return false
}
func (i *PlanBasedIdentity) HasRole(r string) bool {
    for _, role := range i.roles {
        if role == r {
            return true
        }
    }
    return false
}
func (i *PlanBasedIdentity) Stats() map[string]int { return i.stats }

// Handler with usage limits
var createResource = rocco.NewHandler[Input, Output](
    "create-resource",
    "POST",
    "/resources",
    createResourceHandler,
).
    WithAuthentication().
    WithUsageLimit("resources_created_today", func(id rocco.Identity) int {
        // Dynamic limits based on plan
        planID := id.(*PlanBasedIdentity).plan
        switch planID {
        case "enterprise":
            return 10000
        case "pro":
            return 1000
        default:
            return 100
        }
    })

Security Best Practices

1. Use HTTPS

Always use HTTPS in production. Rocco doesn't handle TLS - use a reverse proxy.

2. Short Token Lifetimes

// Access tokens: short lived
accessExpiry := time.Now().Add(15 * time.Minute)

// Refresh tokens: longer, but rotated
refreshExpiry := time.Now().Add(7 * 24 * time.Hour)

3. Log Security Events

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)
})

4. Rate Limit Auth Endpoints

loginHandler.WithMiddleware(rateLimiter(5, time.Minute))

5. Hash API Keys

func hashAPIKey(key string) string {
    hash := sha256.Sum256([]byte(key))
    return hex.EncodeToString(hash[:])
}

See Also