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:
- User visits
/auth/login→ redirected to GitHub - User authorizes → GitHub redirects to
/auth/callback - Callback exchanges code for tokens, calls
Resolve, creates session, sets cookie, redirects to/profile /profilereads session cookie, loads identity from store, returns user data- 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
- Authentication Guide - Detailed auth patterns
- Best Practices - Security recommendations
- Events Reference - Auth events