Files
ResolveSpec/pkg/security
..
2025-12-09 11:18:11 +02:00
2025-12-09 11:18:11 +02:00
2025-12-09 11:18:11 +02:00
2025-12-09 11:18:11 +02:00
2025-12-08 16:56:48 +02:00

ResolveSpec Security Provider

Type-safe, composable security system for ResolveSpec with support for authentication, column-level security (masking), and row-level security (filtering).

Features

  • Interface-Based - Type-safe providers instead of callbacks
  • Login/Logout Support - Built-in authentication lifecycle
  • Two-Factor Authentication (2FA) - Optional TOTP support for enhanced security
  • Composable - Mix and match different providers
  • No Global State - Each handler has its own security configuration
  • Testable - Easy to mock and test
  • Extensible - Implement custom providers for your needs
  • Stored Procedures - All database operations use PostgreSQL stored procedures for security and maintainability
  • OAuth2 Authorization Server - Built-in OAuth 2.1 + PKCE server (RFC 8414, 7591, 7009, 7662) with login form and external provider federation

Stored Procedure Architecture

All database-backed security providers use PostgreSQL stored procedures exclusively. No raw SQL queries are executed from Go code.

Benefits

  • Security: Database logic is centralized and protected
  • Maintainability: Update database logic without recompiling Go code
  • Performance: Stored procedures are pre-compiled and optimized
  • Testability: Test database logic independently
  • Consistency: Standardized resolvespec_* naming convention

Available Stored Procedures

Procedure Purpose Used By
resolvespec_login Session-based login DatabaseAuthenticator
resolvespec_logout Session invalidation DatabaseAuthenticator
resolvespec_session Session validation DatabaseAuthenticator
resolvespec_session_update Update session activity DatabaseAuthenticator
resolvespec_refresh_token Token refresh DatabaseAuthenticator
resolvespec_jwt_login JWT user validation JWTAuthenticator
resolvespec_jwt_logout JWT token blacklist JWTAuthenticator
resolvespec_column_security Load column rules DatabaseColumnSecurityProvider
resolvespec_row_security Load row templates DatabaseRowSecurityProvider
resolvespec_oauth_register_client Persist OAuth2 client (RFC 7591) OAuthServer / DatabaseAuthenticator
resolvespec_oauth_get_client Retrieve OAuth2 client by ID OAuthServer / DatabaseAuthenticator
resolvespec_oauth_save_code Persist authorization code OAuthServer / DatabaseAuthenticator
resolvespec_oauth_exchange_code Consume authorization code (single-use) OAuthServer / DatabaseAuthenticator
resolvespec_oauth_introspect Token introspection (RFC 7662) OAuthServer / DatabaseAuthenticator
resolvespec_oauth_revoke Token revocation (RFC 7009) OAuthServer / DatabaseAuthenticator

See database_schema.sql for complete stored procedure definitions and examples.

Quick Start

import (
    "github.com/bitechdev/ResolveSpec/pkg/security"
    "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
)

// 1. Create security providers
auth := security.NewJWTAuthenticator("your-secret-key", db)
colSec := security.NewDatabaseColumnSecurityProvider(db)
rowSec := security.NewDatabaseRowSecurityProvider(db)

// 2. Combine providers
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)

// 3. Create handler and register security hooks
handler := restheadspec.NewHandlerWithGORM(db)
securityList := security.NewSecurityList(provider)
restheadspec.RegisterSecurityHooks(handler, securityList)

// 4. Apply middleware
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler)
router.Use(security.NewAuthMiddleware(securityList))
router.Use(security.SetSecurityMiddleware(securityList))

Architecture

Spec-Agnostic Design

The security system is completely spec-agnostic - it doesn't depend on any specific spec implementation. Instead, each spec (restheadspec, funcspec, resolvespec) implements its own security integration by adapting to the SecurityContext interface.

┌─────────────────────────────────────┐
│     Security Package (Generic)      │
│  - SecurityContext interface        │
│  - Security providers                │
│  - Core security logic               │
└─────────────────────────────────────┘
           ▲          ▲          ▲
           │          │          │
    ┌──────┘          │          └──────┐
    │                 │                 │
┌───▼────┐      ┌────▼─────┐     ┌────▼──────┐
│RestHead│      │ FuncSpec │     │ResolveSpec│
│  Spec  │      │          │     │           │
│        │      │          │     │           │
│Adapts  │      │ Adapts   │     │  Adapts   │
│to      │      │ to       │     │  to       │
│Security│      │ Security │     │  Security │
│Context │      │ Context  │     │  Context  │
└────────┘      └──────────┘     └───────────┘

Benefits:

  • No circular dependencies
  • Each spec can customize security integration
  • Easy to add new specs
  • Security logic is reusable across all specs

Core Interfaces

The security system is built on three main interfaces:

1. Authenticator

Handles user authentication lifecycle:

type Authenticator interface {
    Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
    Logout(ctx context.Context, req LogoutRequest) error
    Authenticate(r *http.Request) (*UserContext, error)
}

2. ColumnSecurityProvider

Manages column-level security (masking/hiding):

type ColumnSecurityProvider interface {
    GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error)
}

3. RowSecurityProvider

Manages row-level security (WHERE clause filtering):

type RowSecurityProvider interface {
    GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
}

SecurityProvider

The main interface that combines all three:

type SecurityProvider interface {
    Authenticator
    ColumnSecurityProvider
    RowSecurityProvider
}

4. SecurityContext (Spec Integration Interface)

Each spec implements this interface to integrate with the security system:

type SecurityContext interface {
    GetContext() context.Context
    GetUserID() (int, bool)
    GetSchema() string
    GetEntity() string
    GetModel() interface{}
    GetQuery() interface{}
    SetQuery(interface{})
    GetResult() interface{}
    SetResult(interface{})
}

Implementation Examples:

  • restheadspec: Adapts restheadspec.HookContextSecurityContext
  • funcspec: Adapts funcspec.HookContextSecurityContext
  • resolvespec: Adapts resolvespec.HookContextSecurityContext

UserContext

Enhanced user context with complete user information:

type UserContext struct {
    UserID    int            // User's unique ID
    UserName  string         // Username
    UserLevel int            // User privilege level
    SessionID string         // Current session ID
    RemoteID  string         // Remote system ID
    Roles     []string       // User roles
    Email     string         // User email
    Claims    map[string]any // Additional authentication claims
    Meta      map[string]any // Additional metadata (can hold any JSON-serializable values)
}

Available Implementations

Authenticators

HeaderAuthenticator - Simple header-based authentication:

auth := security.NewHeaderAuthenticator()
// Expects: X-User-ID, X-User-Name, X-User-Level, etc.

DatabaseAuthenticator - Database session-based authentication (Recommended):

auth := security.NewDatabaseAuthenticator(db)
// Supports: Login, Logout, Session management, Token refresh
// All operations use stored procedures: resolvespec_login, resolvespec_logout,
// resolvespec_session, resolvespec_session_update, resolvespec_refresh_token
// Requires: users and user_sessions tables + stored procedures (see database_schema.sql)

JWTAuthenticator - JWT token authentication with login/logout:

auth := security.NewJWTAuthenticator("secret-key", db)
// Supports: Login, Logout, JWT token validation
// All operations use stored procedures: resolvespec_jwt_login, resolvespec_jwt_logout
// Note: Requires JWT library installation for token signing/verification

TwoFactorAuthenticator - Wraps any authenticator with TOTP 2FA:

baseAuth := security.NewDatabaseAuthenticator(db)

// Use in-memory provider (for testing)
tfaProvider := security.NewMemoryTwoFactorProvider(nil)

// Or use database provider (for production)
tfaProvider := security.NewDatabaseTwoFactorProvider(db, nil)
// Requires: users table with totp fields, user_totp_backup_codes table
// Requires: resolvespec_totp_* stored procedures (see totp_database_schema.sql)

auth := security.NewTwoFactorAuthenticator(baseAuth, tfaProvider, nil)
// Supports: TOTP codes, backup codes, QR code generation
// Compatible with Google Authenticator, Microsoft Authenticator, Authy, etc.

Column Security Providers

DatabaseColumnSecurityProvider - Loads rules from database:

colSec := security.NewDatabaseColumnSecurityProvider(db)
// Uses stored procedure: resolvespec_column_security
// Queries core.secaccess and core.hub_link tables

ConfigColumnSecurityProvider - Static configuration:

rules := map[string][]security.ColumnSecurity{
    "public.employees": {
        {Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5},
    },
}
colSec := security.NewConfigColumnSecurityProvider(rules)

Row Security Providers

DatabaseRowSecurityProvider - Loads filters from database:

rowSec := security.NewDatabaseRowSecurityProvider(db)
// Uses stored procedure: resolvespec_row_security

ConfigRowSecurityProvider - Static templates:

templates := map[string]string{
    "public.orders": "user_id = {UserID}",
}
blocked := map[string]bool{
    "public.admin_logs": true,
}
rowSec := security.NewConfigRowSecurityProvider(templates, blocked)

Usage Examples

Example 1: Complete Database-Backed Security with Sessions (restheadspec)

func main() {
    db := setupDatabase()

    // Run migrations (see database_schema.sql)
    // db.Exec("CREATE TABLE users ...")
    // db.Exec("CREATE TABLE user_sessions ...")

    // Create handler
    handler := restheadspec.NewHandlerWithGORM(db)

    // Create security providers
    auth := security.NewDatabaseAuthenticator(db) // Session-based auth
    colSec := security.NewDatabaseColumnSecurityProvider(db)
    rowSec := security.NewDatabaseRowSecurityProvider(db)

    // Combine providers
    provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
    securityList := security.NewSecurityList(provider)

    // Register security hooks for this spec
    restheadspec.RegisterSecurityHooks(handler, securityList)

    // Setup routes
    router := mux.NewRouter()

    // Add auth endpoints
    router.HandleFunc("/auth/login", handleLogin(securityList)).Methods("POST")
    router.HandleFunc("/auth/logout", handleLogout(securityList)).Methods("POST")
    router.HandleFunc("/auth/refresh", handleRefresh(securityList)).Methods("POST")

    // Setup API with security
    apiRouter := router.PathPrefix("/api").Subrouter()
    restheadspec.SetupMuxRoutes(apiRouter, handler)
    apiRouter.Use(security.NewAuthMiddleware(securityList))
    apiRouter.Use(security.SetSecurityMiddleware(securityList))

    http.ListenAndServe(":8080", router)
}

func handleLogin(securityList *security.SecurityList) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req security.LoginRequest
        json.NewDecoder(r.Body).Decode(&req)

        // Add client info to claims
        req.Claims = map[string]any{
            "ip_address": r.RemoteAddr,
            "user_agent": r.UserAgent(),
        }

        resp, err := securityList.Provider().Login(r.Context(), req)
        if err != nil {
            http.Error(w, err.Error(), http.StatusUnauthorized)
            return
        }

        // Set session cookie (optional)
        http.SetCookie(w, &http.Cookie{
            Name:     "session_token",
            Value:    resp.Token,
            Expires:  time.Now().Add(24 * time.Hour),
            HttpOnly: true,
            Secure:   true, // Use in production with HTTPS
            SameSite: http.SameSiteStrictMode,
        })

        json.NewEncoder(w).Encode(resp)
    }
}

func handleRefresh(securityList *security.SecurityList) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("X-Refresh-Token")

        if refreshable, ok := securityList.Provider().(security.Refreshable); ok {
            resp, err := refreshable.RefreshToken(r.Context(), token)
            if err != nil {
                http.Error(w, err.Error(), http.StatusUnauthorized)
                return
}

Two-Factor Authentication (2FA)

Overview

  • Optional per-user - Enable/disable 2FA individually
  • TOTP standard - Compatible with Google Authenticator, Microsoft Authenticator, Authy, 1Password, etc.
  • Configurable - SHA1/SHA256/SHA512, 6/8 digits, custom time periods
  • Backup codes - One-time recovery codes with secure hashing
  • Clock skew - Handles time differences between client/server

Setup

// 1. Wrap existing authenticator with 2FA support
baseAuth := security.NewDatabaseAuthenticator(db)
tfaProvider := security.NewMemoryTwoFactorProvider(nil) // Use custom DB implementation in production
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, tfaProvider, nil)

// 2. Use as normal authenticator
provider := security.NewCompositeSecurityProvider(tfaAuth, colSec, rowSec)
securityList := security.NewSecurityList(provider)

Enable 2FA for User

// 1. Initiate 2FA setup
secret, err := tfaAuth.Setup2FA(userID, "MyApp", "user@example.com")
// Returns: secret.Secret, secret.QRCodeURL, secret.BackupCodes

// 2. User scans QR code with authenticator app
// Display secret.QRCodeURL as QR code image

// 3. User enters verification code from app
code := "123456" // From authenticator app
err = tfaAuth.Enable2FA(userID, secret.Secret, code)
// 2FA is now enabled for this user

// 4. Store backup codes securely and show to user once
// Display: secret.BackupCodes (10 codes)

Login Flow with 2FA

// 1. User provides credentials
req := security.LoginRequest{
    Username: "user@example.com",
    Password: "password",
}

resp, err := tfaAuth.Login(ctx, req)

// 2. Check if 2FA required
if resp.Requires2FA {
    // Prompt user for 2FA code
    code := getUserInput() // From authenticator app or backup code
    
    // 3. Login again with 2FA code
    req.TwoFactorCode = code
    resp, err = tfaAuth.Login(ctx, req)
    
    // 4. Success - token is returned
    token := resp.Token
}

Manage 2FA

// Disable 2FA
err := tfaAuth.Disable2FA(userID)

// Regenerate backup codes
newCodes, err := tfaAuth.RegenerateBackupCodes(userID, 10)

// Check status
has2FA, err := tfaProvider.Get2FAStatus(userID)

Custom 2FA Storage

Option 1: Use DatabaseTwoFactorProvider (Recommended)

// Uses PostgreSQL stored procedures for all operations
db := setupDatabase()

// Run migrations from totp_database_schema.sql
// - Add totp_secret, totp_enabled, totp_enabled_at to users table
// - Create user_totp_backup_codes table
// - Create resolvespec_totp_* stored procedures

tfaProvider := security.NewDatabaseTwoFactorProvider(db, nil)
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, tfaProvider, nil)

Option 2: Implement Custom Provider

Implement TwoFactorAuthProvider for custom storage:

type DBTwoFactorProvider struct {
    db *gorm.DB
}

func (p *DBTwoFactorProvider) Enable2FA(userID int, secret string, backupCodes []string) error {
    // Store secret and hashed backup codes in database
    return p.db.Exec("UPDATE users SET totp_secret = ?, backup_codes = ? WHERE id = ?", 
        secret, hashCodes(backupCodes), userID).Error
}

func (p *DBTwoFactorProvider) Get2FASecret(userID int) (string, error) {
    var secret string
    err := p.db.Raw("SELECT totp_secret FROM users WHERE id = ?", userID).Scan(&secret).Error
    return secret, err
}

// Implement remaining methods: Generate2FASecret, Validate2FACode, Disable2FA,
// Get2FAStatus, GenerateBackupCodes, ValidateBackupCode

Configuration

config := &security.TwoFactorConfig{
    Algorithm:  "SHA256",  // SHA1, SHA256, SHA512
    Digits:     8,         // 6 or 8
    Period:     30,        // Seconds per code
    SkewWindow: 2,         // Accept codes ±2 periods
}

totp := security.NewTOTPGenerator(config)
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, tfaProvider, config)

API Response Structure

// LoginResponse with 2FA
type LoginResponse struct {
    Token              string              `json:"token"`
    Requires2FA        bool                `json:"requires_2fa"`
    TwoFactorSetupData *TwoFactorSecret    `json:"two_factor_setup,omitempty"`
    User               *UserContext        `json:"user"`
}

// TwoFactorSecret for setup
type TwoFactorSecret struct {
    Secret      string   `json:"secret"`         // Base32 encoded
    QRCodeURL   string   `json:"qr_code_url"`    // otpauth://totp/...
    BackupCodes []string `json:"backup_codes"`   // 10 recovery codes
}

// UserContext includes 2FA status
type UserContext struct {
    UserID           int    `json:"user_id"`
    TwoFactorEnabled bool   `json:"two_factor_enabled"`
    // ... other fields
}

Security Best Practices

  • Store secrets encrypted - Never store TOTP secrets in plain text

  • Hash backup codes - Use SHA-256 before storing

  • Rate limit - Limit 2FA verification attempts

  • Require password - Always verify password before disabling 2FA

  • Show backup codes once - Display only during setup/regeneration

  • Log 2FA events - Track enable/disable/failed attempts

  • Mark codes as used - Backup codes are single-use only

          json.NewEncoder(w).Encode(resp)
      } else {
          http.Error(w, "Refresh not supported", http.StatusNotImplemented)
      }
    

    } }


### Example 2: Config-Based Security (No Database)

```go
func main() {
    db := setupDatabase()
    handler := restheadspec.NewHandlerWithGORM(db)

    // Static column security rules
    columnRules := map[string][]security.ColumnSecurity{
        "public.employees": {
            {Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5},
            {Path: []string{"salary"}, Accesstype: "hide"},
        },
    }

    // Static row security templates
    rowTemplates := map[string]string{
        "public.orders": "user_id = {UserID}",
    }

    // Create providers
    auth := security.NewHeaderAuthenticator()
    colSec := security.NewConfigColumnSecurityProvider(columnRules)
    rowSec := security.NewConfigRowSecurityProvider(rowTemplates, nil)

    // Combine providers and register hooks
    provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
    securityList := security.NewSecurityList(provider)
    restheadspec.RegisterSecurityHooks(handler, securityList)

    // Setup routes...
}

Example 3: FuncSpec Security (SQL Query API)

import (
    "github.com/bitechdev/ResolveSpec/pkg/funcspec"
    "github.com/bitechdev/ResolveSpec/pkg/security"
)

func main() {
    db := setupDatabase()

    // Create funcspec handler
    handler := funcspec.NewHandler(db)

    // Create security providers
    auth := security.NewJWTAuthenticator("secret-key", db)
    colSec := security.NewDatabaseColumnSecurityProvider(db)
    rowSec := security.NewDatabaseRowSecurityProvider(db)

    // Combine providers
    provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
    securityList := security.NewSecurityList(provider)

    // Register security hooks (audit logging)
    funcspec.RegisterSecurityHooks(handler, securityList)

    // Note: funcspec operates on raw SQL queries, so row/column
    // security is limited. Security should be enforced at the
    // SQL function level or via database policies.

    // Setup routes...
}

Example 4: ResolveSpec Security (REST API)

import (
    "github.com/bitechdev/ResolveSpec/pkg/resolvespec"
    "github.com/bitechdev/ResolveSpec/pkg/security"
)

func main() {
    db := setupDatabase()
    registry := common.NewModelRegistry()

    // Register models
    registry.RegisterModel("public.users", &User{})
    registry.RegisterModel("public.orders", &Order{})

    // Create resolvespec handler
    handler := resolvespec.NewHandler(db, registry)

    // Create security providers
    auth := security.NewDatabaseAuthenticator(db)
    colSec := security.NewDatabaseColumnSecurityProvider(db)
    rowSec := security.NewDatabaseRowSecurityProvider(db)

    // Combine providers
    provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
    securityList := security.NewSecurityList(provider)

    // Register security hooks for resolvespec
    resolvespec.RegisterSecurityHooks(handler, securityList)

    // Setup routes...
}

Example 5: Custom Provider

Implement your own provider for complete control:

type MySecurityProvider struct {
    db *gorm.DB
}

func (p *MySecurityProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
    // Your custom login logic
}

func (p *MySecurityProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
    // Your custom logout logic
}

func (p *MySecurityProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
    // Your custom authentication logic
}

func (p *MySecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
    // Your custom column security logic
}

func (p *MySecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
    // Your custom row security logic
}

// Use it with any spec
provider := &MySecurityProvider{db: db}
securityList := security.NewSecurityList(provider)

// Register with restheadspec
restheadspec.RegisterSecurityHooks(restHandler, securityList)

// Or with funcspec
funcspec.RegisterSecurityHooks(funcHandler, securityList)

// Or with resolvespec
resolvespec.RegisterSecurityHooks(resolveHandler, securityList)

Security Features

Column Security (Masking/Hiding)

Mask SSN (show last 4 digits):

{
    Path:       []string{"ssn"},
    Accesstype: "mask",
    MaskStart:  5,
    MaskChar:   "*",
}
// "123-45-6789" → "*****6789"

Hide entire field:

{
    Path:       []string{"salary"},
    Accesstype: "hide",
}
// Field returns 0 or empty

Nested JSON field masking:

{
    Path:       []string{"address", "street"},
    Accesstype: "mask",
    MaskStart:  10,
}

Row Security (Filtering)

User isolation:

{
    Template: "user_id = {UserID}",
}
// Users only see their own records

Tenant isolation:

{
    Template: "tenant_id = {TenantID} AND user_id = {UserID}",
}

Block all access:

{
    HasBlock: true,
}
// Completely blocks access to the table

Template variables:

  • {UserID} - Current user's ID
  • {PrimaryKeyName} - Primary key column
  • {TableName} - Table name
  • {SchemaName} - Schema name

Request Flow

HTTP Request
    ↓
NewOptionalAuthMiddleware (security package)  ← recommended for spec routes
    ├─ Calls provider.Authenticate(request)
    ├─ On success: adds authenticated UserContext to context
    └─ On failure: adds guest UserContext (UserID=0) to context
    ↓
SetSecurityMiddleware (security package)
    └─ Adds SecurityList to context
    ↓
Spec Handler (restheadspec/funcspec/resolvespec/websocketspec/mqttspec)
    └─ Resolves schema + entity + model from request
    ↓
BeforeHandle Hook (registered by spec via RegisterSecurityHooks)
    ├─ Adapts spec's HookContext → SecurityContext
    ├─ Calls security.CheckModelAuthAllowed(secCtx, operation)
    │   ├─ Loads model rules from context or registry
    │   ├─ SecurityDisabled → allow
    │   ├─ CanPublicRead/Create/Update/Delete → allow unauthenticated
    │   └─ UserID == 0 → 401 unauthorized
    └─ On error: aborts with 401
    ↓
BeforeRead Hook (registered by spec)
    ├─ Adapts spec's HookContext → SecurityContext
    ├─ Calls security.LoadSecurityRules(secCtx, securityList)
    │   ├─ Calls provider.GetColumnSecurity()
    │   └─ Calls provider.GetRowSecurity()
    └─ Caches security rules
    ↓
BeforeScan Hook (registered by spec)
    ├─ Adapts spec's HookContext → SecurityContext
    ├─ Calls security.ApplyRowSecurity(secCtx, securityList)
    └─ Applies row security (adds WHERE clause to query)
    ↓
Database Query (with security filters)
    ↓
AfterRead Hook (registered by spec)
    ├─ Adapts spec's HookContext → SecurityContext
    ├─ Calls security.ApplyColumnSecurity(secCtx, securityList)
    ├─ Applies column security (masks/hides fields)
    └─ Calls security.LogDataAccess(secCtx)
    ↓
HTTP Response (secured data)

Key Points:

  • NewOptionalAuthMiddleware never rejects — it sets guest context on auth failure; BeforeHandle enforces auth after model resolution
  • BeforeHandle fires after model resolution, giving access to model rules and user context simultaneously
  • Each spec registers its own hooks that adapt to SecurityContext
  • Security rules are loaded once and cached for the request
  • Row security is applied to the query (database level)
  • Column security is applied to results (application level)

Testing

The interface-based design makes testing straightforward:

// Mock authenticator for tests
type MockAuthenticator struct {
    UserToReturn *security.UserContext
    ErrorToReturn error
}

func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
    return m.UserToReturn, m.ErrorToReturn
}

// Use in tests
func TestMyHandler(t *testing.T) {
    mockAuth := &MockAuthenticator{
        UserToReturn: &security.UserContext{UserID: 123},
    }

    provider := security.NewCompositeSecurityProvider(
        mockAuth,
        &MockColumnSecurity{},
        &MockRowSecurity{},
    )

    securityList := security.SetupSecurityProvider(handler, provider)
    // ... test your handler
}

Migration Guide

From Old Callback System

If you're upgrading from the old callback-based system:

Old:

security.GlobalSecurity.AuthenticateCallback = myAuthFunc
security.GlobalSecurity.LoadColumnSecurityCallback = myColSecFunc
security.GlobalSecurity.LoadRowSecurityCallback = myRowSecFunc
security.SetupSecurityProvider(handler, &security.GlobalSecurity)

New:

// 1. Wrap your functions in a provider
type MyProvider struct{}

func (p *MyProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
    userID, roles, err := myAuthFunc(r)
    return &security.UserContext{UserID: userID, Roles: strings.Split(roles, ",")}, err
}

func (p *MyProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
    return myColSecFunc(userID, schema, table)
}

func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
    return myRowSecFunc(userID, schema, table)
}

func (p *MyProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
    return nil, fmt.Errorf("not implemented")
}

func (p *MyProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
    return nil
}

// 2. Create security list and register hooks
provider := &MyProvider{}
securityList := security.NewSecurityList(provider)

// 3. Register with your spec
restheadspec.RegisterSecurityHooks(handler, securityList)

From Old SetupSecurityProvider API

If you're upgrading from the previous interface-based system:

Old:

securityList := security.SetupSecurityProvider(handler, provider)

New:

securityList := security.NewSecurityList(provider)
restheadspec.RegisterSecurityHooks(handler, securityList) // or funcspec/resolvespec

OAuth2 Authorization Server

OAuthServer is a generic OAuth 2.1 + PKCE authorization server. It is not tied to any spec — pkg/resolvemcp uses it, but it can be used standalone with any http.ServeMux.

Endpoints

Method Path RFC
GET /.well-known/oauth-authorization-server RFC 8414 — server metadata
POST /oauth/register RFC 7591 — dynamic client registration
GET /oauth/authorize OAuth 2.1 — start authorization / provider selection
POST /oauth/authorize OAuth 2.1 — login form submission
POST /oauth/token OAuth 2.1 — code exchange + refresh
POST /oauth/revoke RFC 7009 — token revocation
POST /oauth/introspect RFC 7662 — token introspection
GET {ProviderCallbackPath} External provider redirect target

Config

cfg := security.OAuthServerConfig{
    Issuer:               "https://example.com",      // Required — token issuer URL
    ProviderCallbackPath: "/oauth/provider/callback", // External provider redirect target
    LoginTitle:           "My App Login",             // HTML login page title
    PersistClients:       true,  // Store clients in DB (multi-instance safe)
    PersistCodes:         true,  // Store codes in DB (multi-instance safe)
    DefaultScopes:        []string{"openid", "profile"}, // Returned when no scope requested
    AccessTokenTTL:       time.Hour,
    AuthCodeTTL:          5 * time.Minute,
}
Field Default Notes
Issuer Required; trailing slash is trimmed automatically
ProviderCallbackPath /oauth/provider/callback
LoginTitle "Sign in"
PersistClients false Set true for multi-instance
PersistCodes false Set true for multi-instance; does not require PersistClients
DefaultScopes ["openid","profile","email"]
AccessTokenTTL 24h Also used as expires_in in token responses
AuthCodeTTL 2m

Operating Modes

Mode 1 — Direct login (username/password form)

Pass a *DatabaseAuthenticator to NewOAuthServer. The server renders a login form at GET /oauth/authorize and issues tokens via the stored session after login.

auth := security.NewDatabaseAuthenticator(db)
srv := security.NewOAuthServer(cfg, auth)

Mode 2 — External provider federation

Pass a *DatabaseAuthenticator for persistence (authorization codes, revoke, introspect) and register external providers. The authorize endpoint redirects to the specified provider (via the provider query param) or to the first registered provider by default.

auth := security.NewDatabaseAuthenticator(db)
srv := security.NewOAuthServer(cfg, auth)
srv.RegisterExternalProvider(googleAuth, "google")
srv.RegisterExternalProvider(githubAuth, "github")

Mode 3 — Both

Pass auth for the login form and also register external providers. The authorize page shows both a login form and provider buttons.

srv := security.NewOAuthServer(cfg, auth)
srv.RegisterExternalProvider(googleAuth, "google")

Standalone Usage

mux := http.NewServeMux()
mux.Handle("/.well-known/", srv.HTTPHandler())
mux.Handle("/oauth/", srv.HTTPHandler())
mux.Handle(cfg.ProviderCallbackPath, srv.HTTPHandler())

http.ListenAndServe(":8080", mux)

DB Persistence

When PersistClients: true or PersistCodes: true, the server calls the corresponding DatabaseAuthenticator methods. Both flags default to false (in-memory maps). Enable both for multi-instance deployments.

Requires oauth_clients and oauth_codes tables + 6 stored procedures from database_schema.sql.

New DB Types

type OAuthServerClient struct {
    ClientID      string   `json:"client_id"`
    RedirectURIs  []string `json:"redirect_uris"`
    ClientName    string   `json:"client_name,omitempty"`
    GrantTypes    []string `json:"grant_types"`
    AllowedScopes []string `json:"allowed_scopes,omitempty"`
}

type OAuthCode struct {
    Code                string    `json:"code"`
    ClientID            string    `json:"client_id"`
    RedirectURI         string    `json:"redirect_uri"`
    ClientState         string    `json:"client_state,omitempty"`
    CodeChallenge       string    `json:"code_challenge"`
    CodeChallengeMethod string    `json:"code_challenge_method"`
    SessionToken        string    `json:"session_token"`
    Scopes              []string  `json:"scopes,omitempty"`
    ExpiresAt           time.Time `json:"expires_at"`
}

type OAuthTokenInfo struct {
    Active   bool     `json:"active"`
    Sub      string   `json:"sub,omitempty"`
    Username string   `json:"username,omitempty"`
    Email    string   `json:"email,omitempty"`
    Roles    []string `json:"roles,omitempty"`
    Exp      int64    `json:"exp,omitempty"`
    Iat      int64    `json:"iat,omitempty"`
}

DatabaseAuthenticator OAuth Methods

auth.OAuthRegisterClient(ctx, client)  // RFC 7591 — persist client
auth.OAuthGetClient(ctx, clientID)     // retrieve client
auth.OAuthSaveCode(ctx, code)          // persist authorization code
auth.OAuthExchangeCode(ctx, code)      // consume code (single-use, deletes on read)
auth.OAuthIntrospectToken(ctx, token)  // RFC 7662 — returns OAuthTokenInfo
auth.OAuthRevokeToken(ctx, token)      // RFC 7009 — revoke session

SQLNames Fields

type SQLNames struct {
    // ... existing fields ...
    OAuthRegisterClient string // default: "resolvespec_oauth_register_client"
    OAuthGetClient      string // default: "resolvespec_oauth_get_client"
    OAuthSaveCode       string // default: "resolvespec_oauth_save_code"
    OAuthExchangeCode   string // default: "resolvespec_oauth_exchange_code"
    OAuthIntrospect     string // default: "resolvespec_oauth_introspect"
    OAuthRevoke         string // default: "resolvespec_oauth_revoke"
}

The main changes:

  1. Security package no longer knows about specific spec types
  2. Each spec registers its own security hooks
  3. More flexible - same security provider works with all specs

Documentation

File Description
QUICK_REFERENCE.md Quick reference guide with examples
INTERFACE_GUIDE.md Complete implementation guide
examples.go Working provider implementations
setup_example.go 6 complete integration examples

API Reference

Context Helpers

Get user information from request context:

userCtx, ok := security.GetUserContext(ctx)
userID, ok := security.GetUserID(ctx)
userName, ok := security.GetUserName(ctx)
userLevel, ok := security.GetUserLevel(ctx)
sessionID, ok := security.GetSessionID(ctx)
remoteID, ok := security.GetRemoteID(ctx)
roles, ok := security.GetUserRoles(ctx)
email, ok := security.GetUserEmail(ctx)

Optional Interfaces

Implement these for additional features:

Refreshable - Token refresh support:

type Refreshable interface {
    RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error)
}

Validatable - Token validation:

type Validatable interface {
    ValidateToken(ctx context.Context, token string) (bool, error)
}

Cacheable - Cache management:

type Cacheable interface {
    ClearCache(ctx context.Context, userID int, schema, table string) error
}

Benefits Over Callbacks

Feature Old (Callbacks) New (Interfaces)
Type Safety Callbacks can be nil Compile-time verification
Global State GlobalSecurity variable Dependency injection
Testability ⚠️ Need to set globals Easy to mock
Composability Single provider only Mix and match
Login/Logout Not supported Built-in
Extensibility ⚠️ Limited Optional interfaces

Common Patterns

Caching Security Rules

type CachedProvider struct {
    inner security.ColumnSecurityProvider
    cache *cache.Cache
}

func (p *CachedProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
    key := fmt.Sprintf("%d:%s.%s", userID, schema, table)
    if cached, found := p.cache.Get(key); found {
        return cached.([]security.ColumnSecurity), nil
    }

    rules, err := p.inner.GetColumnSecurity(ctx, userID, schema, table)
    if err == nil {
        p.cache.Set(key, rules, cache.DefaultExpiration)
    }
    return rules, err
}

Role-Based Security

func (p *MyProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
    userCtx, _ := security.GetUserContext(ctx)

    if contains(userCtx.Roles, "admin") {
        return []security.ColumnSecurity{}, nil // No restrictions
    }

    return loadRestrictionsForUser(userID, schema, table), nil
}

Multi-Tenant Isolation

func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
    tenantID := getUserTenant(userID)

    return security.RowSecurity{
        Template: fmt.Sprintf("tenant_id = %d AND user_id = {UserID}", tenantID),
    }, nil
}

Model-Level Access Control

Use ModelRules (from pkg/modelregistry) to control per-entity auth behavior:

modelregistry.RegisterModelWithRules("public.products", &Product{}, modelregistry.ModelRules{
    SecurityDisabled: false,   // true = skip all auth checks
    CanPublicRead:    true,    // unauthenticated GET allowed
    CanPublicCreate:  false,   // requires auth
    CanPublicUpdate:  false,   // requires auth
    CanPublicDelete:  false,   // requires auth
    CanUpdate:        true,    // authenticated users can update
    CanDelete:        false,   // authenticated users cannot delete
})

CheckModelAuthAllowed(secCtx, operation) applies these rules in BeforeHandle:

  1. SecurityDisabled → allow all
  2. CanPublicRead/Create/Update/Delete → allow unauthenticated for that operation
  3. Guest (UserID == 0) → return 401
  4. Authenticated → allow (operation-specific CanUpdate/CanDelete checked in BeforeUpdate/BeforeDelete)

Middleware and Handler API

NewAuthMiddleware

Standard middleware that authenticates all requests and returns 401 on failure:

router.Use(security.NewAuthMiddleware(securityList))

NewOptionalAuthMiddleware

Middleware for spec routes — always continues; sets guest context on auth failure:

// Use with RegisterSecurityHooks — auth enforcement is deferred to BeforeHandle
apiRouter.Use(security.NewOptionalAuthMiddleware(securityList))
apiRouter.Use(security.SetSecurityMiddleware(securityList))
restheadspec.RegisterSecurityHooks(handler, securityList)  // registers BeforeHandle

Routes can skip authentication using the SkipAuth helper:

func PublicHandler(w http.ResponseWriter, r *http.Request) {
    ctx := security.SkipAuth(r.Context())
    // This route will bypass authentication
    // A guest user context will be set instead
}

router.Handle("/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx := security.SkipAuth(r.Context())
    PublicHandler(w, r.WithContext(ctx))
}))

When authentication is skipped, a guest user context is automatically set:

  • UserID: 0
  • UserName: "guest"
  • Roles: ["guest"]
  • RemoteID: Request's remote address

Routes can use optional authentication with the OptionalAuth helper:

func OptionalAuthHandler(w http.ResponseWriter, r *http.Request) {
    ctx := security.OptionalAuth(r.Context())
    r = r.WithContext(ctx)

    // This route will try to authenticate
    // If authentication succeeds, authenticated user context is set
    // If authentication fails, guest user context is set instead

    userCtx, _ := security.GetUserContext(r.Context())
    if userCtx.UserID == 0 {
        // Guest user
        fmt.Fprintf(w, "Welcome, guest!")
    } else {
        // Authenticated user
        fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
    }
}

router.Handle("/home", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx := security.OptionalAuth(r.Context())
    OptionalAuthHandler(w, r.WithContext(ctx))
}))

Authentication Modes Summary:

  • Required (default): Authentication must succeed or returns 401
  • SkipAuth: Bypasses authentication entirely, always sets guest context
  • OptionalAuth: Tries authentication, falls back to guest context if it fails

NewAuthHandler

Standalone authentication handler (without middleware wrapping):

// Use when you need authentication logic without middleware
authHandler := security.NewAuthHandler(securityList, myHandler)
http.Handle("/api/protected", authHandler)

NewOptionalAuthHandler

Standalone optional authentication handler that tries to authenticate but falls back to guest:

// Use for routes that should work for both authenticated and guest users
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
http.Handle("/home", optionalHandler)

// Example handler that checks user context
func myHandler(w http.ResponseWriter, r *http.Request) {
    userCtx, _ := security.GetUserContext(r.Context())
    if userCtx.UserID == 0 {
        fmt.Fprintf(w, "Welcome, guest!")
    } else {
        fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
    }
}

Helper Functions

Extract user information from context:

// Get full user context
userCtx, ok := security.GetUserContext(ctx)

// Get specific fields
userID, ok := security.GetUserID(ctx)
userName, ok := security.GetUserName(ctx)
userLevel, ok := security.GetUserLevel(ctx)
sessionID, ok := security.GetSessionID(ctx)
remoteID, ok := security.GetRemoteID(ctx)
roles, ok := security.GetUserRoles(ctx)
email, ok := security.GetUserEmail(ctx)
meta, ok := security.GetUserMeta(ctx)

Metadata Support

The Meta field in UserContext can hold any JSON-serializable values:

// Set metadata during login
loginReq := security.LoginRequest{
    Username: "user@example.com",
    Password: "password",
    Meta: map[string]any{
        "department": "engineering",
        "location": "US",
        "preferences": map[string]any{
            "theme": "dark",
        },
    },
}

// Access metadata in handlers
meta, ok := security.GetUserMeta(ctx)
if ok {
    department := meta["department"].(string)
}

License

Part of the ResolveSpec project.