Skip to main content
Permission Mongo uses JWT (JSON Web Tokens) for authentication. The system supports both RS256 (asymmetric) and HS256 (symmetric) signing algorithms.

Overview

The authentication system:
  • Validates JWT tokens from the Authorization header
  • Extracts user identity, tenant ID, and roles from token claims
  • Supports custom claims for fine-grained access control
  • Integrates with RBAC policies for permission enforcement

Token Structure

A valid JWT token must contain these required claims:
{
  "sub": "user-123",          // Subject (user ID) - REQUIRED
  "tenant_id": "acme-corp",   // Tenant identifier
  "roles": ["manager", "editor"],  // User roles
  "iat": 1640000000,           // Issued at (Unix timestamp)
  "exp": 1640003600,           // Expiration (Unix timestamp)
  "iss": "your-auth-service", // Issuer (optional)
  "aud": ["permission-mongo"]  // Audience (optional)
}
Custom claims can also be included:
{
  "sub": "user-456",
  "tenant_id": "acme-corp",
  "roles": ["sales-rep"],
  "department": "sales",     // Custom claim
  "region": "us-west",        // Custom claim
  "iat": 1640000000,
  "exp": 1640003600
}
Source: /home/daytona/workspace/source/pkg/auth/jwt.go:39-49

Configuration

1
Step 1: Choose Your Algorithm
2
Decide between RS256 (recommended for production) or HS256:
3
RS256 (Asymmetric)
4
  • More secure
  • Uses public/private key pair
  • Private key stays with auth service, public key with Permission Mongo
  • Recommended for production
  • 5
    HS256 (Symmetric)
    6
  • Simpler setup
  • Uses shared secret
  • Secret must be kept secure on both sides
  • Good for development and internal services
  • 7
    Step 2: Configure RS256 Authentication
    8
    For RS256, you need to provide the public key:
    9
    auth:
      algorithm: RS256
      public_key_file: /path/to/public.pem
      # OR provide key directly:
      # public_key: |
      #   -----BEGIN PUBLIC KEY-----
      #   MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
      #   -----END PUBLIC KEY-----
      issuer: your-auth-service  # Optional: validate issuer
      audience: permission-mongo  # Optional: validate audience
    
    10
    Generate a key pair for testing:
    11
    # Generate private key
    openssl genrsa -out private.pem 2048
    
    # Extract public key
    openssl rsa -in private.pem -pubout -out public.pem
    
    12
    Source: /home/daytona/workspace/source/pkg/auth/jwt.go:29-37
    13
    Step 3: Configure HS256 Authentication
    14
    For HS256, provide a shared secret:
    15
    auth:
      algorithm: HS256
      secret: ${ENV.JWT_SECRET}  # Use environment variable
      issuer: your-auth-service
      audience: permission-mongo
    
    16
    Generate a strong secret:
    17
    openssl rand -base64 32
    

    Token Generation

    Your authentication service generates tokens. Here’s an example using Go:

    RS256 Token Generation

    import (
    	"crypto/rsa"
    	"time"
    	"github.com/golang-jwt/jwt/v5"
    )
    
    func generateRS256Token(privateKey *rsa.PrivateKey, userID, tenantID string, roles []string) (string, error) {
    	claims := jwt.MapClaims{
    		"sub":       userID,
    		"tenant_id": tenantID,
    		"roles":     roles,
    		"iat":       time.Now().Unix(),
    		"exp":       time.Now().Add(time.Hour).Unix(),
    		"iss":       "your-auth-service",
    		"aud":       []string{"permission-mongo"},
    	}
    
    	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    	return token.SignedString(privateKey)
    }
    

    HS256 Token Generation

    func generateHS256Token(secret, userID, tenantID string, roles []string) (string, error) {
    	claims := jwt.MapClaims{
    		"sub":       userID,
    		"tenant_id": tenantID,
    		"roles":     roles,
    		"iat":       time.Now().Unix(),
    		"exp":       time.Now().Add(time.Hour).Unix(),
    	}
    
    	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    	return token.SignedString([]byte(secret))
    }
    
    Source: /home/daytona/workspace/source/pkg/auth/jwt_test.go:53-71

    Making Authenticated Requests

    Include the JWT token in the Authorization header:
    curl -X GET https://api.example.com/products \
      -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
    
    The middleware extracts and validates the token: Source: /home/daytona/workspace/source/pkg/api/middleware.go:21-69

    Custom Claims in Policies

    Custom JWT claims can be used in RBAC policy conditions:
    policies:
      sales_leads:
        sales_rep:
          actions: [read, update]
          when: |
            doc.region == user.claims.region &&
            doc.status == "open"
    
    The user.claims context includes all JWT claims:
    // Available in policy expressions:
    user.claims.department  // "sales"
    user.claims.region      // "us-west"
    user.claims.level       // "senior"
    
    Source: /home/daytona/workspace/source/pkg/auth/context.go:101-118

    Token Validation

    Permission Mongo validates tokens automatically:
    1
    Step 1: Signature Validation
    2
    Verifies the token was signed by the expected key:
    3
    # RS256: Verifies with public key
    # HS256: Verifies with shared secret
    
    4
    Step 2: Expiration Check
    5
    Rejects expired tokens:
    6
    {
      "error": {
        "code": "expired_token",
        "message": "token has expired"
      }
    }
    
    7
    Step 3: Issuer & Audience
    8
    Validates iss and aud claims if configured:
    9
    auth:
      issuer: your-auth-service    # Must match token's "iss"
      audience: permission-mongo    # Must be in token's "aud" array
    
    10
    Step 4: Required Claims
    11
    Ensures sub (subject) claim is present:
    12
    {
      "sub": "user-123"  // Required - user identifier
    }
    
    Source: /home/daytona/workspace/source/pkg/auth/jwt.go:150-215

    Error Responses

    Invalid Token

    {
      "error": {
        "code": "invalid_token",
        "message": "invalid token"
      }
    }
    

    Expired Token

    {
      "error": {
        "code": "expired_token",
        "message": "token has expired"
      }
    }
    

    Invalid Signature

    {
      "error": {
        "code": "invalid_token",
        "message": "invalid token signature"
      }
    }
    

    Missing Authorization

    {
      "error": {
        "code": "unauthorized",
        "message": "missing authorization header"
      }
    }
    
    Source: /home/daytona/workspace/source/pkg/api/middleware.go:46-60

    Development Mode

    For local development, you can bypass authentication:
    server:
      dev_mode: true
      dev_tenant: test-corp
    
    This sets a default admin context without requiring tokens.
    Never enable dev_mode in production! It bypasses all authentication.
    Source: /home/daytona/workspace/source/pkg/api/middleware.go:107-131

    Multi-Tenancy

    The tenant_id claim automatically scopes all operations:
    {
      "sub": "user-123",
      "tenant_id": "acme-corp",  // All queries filtered by this
      "roles": ["admin"]
    }
    
    Queries are automatically filtered:
    // User queries:
    GET /products
    
    // Automatically becomes:
    GET /products?tenant_id=acme-corp
    
    Source: /home/daytona/workspace/source/pkg/auth/context.go:16-25

    Best Practices

    Use RS256 in production for better security
    Set short expiration times (15-60 minutes)
    Validate issuer and audience claims
    Store secrets in environment variables, not config files
    Rotate signing keys regularly
    Include minimal claims in tokens (roles, tenant_id)
    Use refresh tokens for long-lived sessions

    Testing Authentication

    Create test tokens for development:
    func TestCreateProduct(t *testing.T) {
    	// Generate test token
    	token := createHS256Token(jwt.MapClaims{
    		"sub":       "user-123",
    		"tenant_id": "test-corp",
    		"roles":     []string{"admin"},
    		"iat":       time.Now().Unix(),
    		"exp":       time.Now().Add(time.Hour).Unix(),
    	}, "test-secret")
    
    	// Make authenticated request
    	resp := makeRequest("POST", "/products", token, productData)
    	assert.Equal(t, 201, resp.StatusCode)
    }
    
    Source: /home/daytona/workspace/source/pkg/auth/jwt_test.go:190-233

    Next Steps

    Multi-Tenancy

    Configure tenant isolation and data scoping

    RBAC Policies

    Define role-based permissions

    Build docs developers (and LLMs) love