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
| Scenario | Status | Error |
|---|---|---|
| No/invalid identity | 401 | ErrUnauthorized |
| Missing required scope | 403 | ErrForbidden (message: "insufficient scope") |
| Missing required role | 403 | ErrForbidden (message: "insufficient role") |
| Usage limit exceeded | 429 | ErrTooManyRequests |
Events
Authentication events are emitted via capitan:
| Event | Description |
|---|---|
AuthenticationFailed | Identity extraction failed |
AuthenticationSucceeded | Identity extracted successfully |
AuthorizationScopeDenied | Scope check failed |
AuthorizationRoleDenied | Role check failed |
AuthorizationSucceeded | Authorization passed |
RateLimitExceeded | Usage 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
- Best Practices - Security recommendations
- Authentication Cookbook - Implementation patterns
- Events Reference - Auth event details