Skip to content

"Heimdall" will be a central, decoupled microservice written in Go. It will function as both an Identity Orchestrator and a Policy Decision Point (PDP).

License

Notifications You must be signed in to change notification settings

nexlified/heimdall

Heimdall

A decoupled, high-performance authentication and authorization orchestrator

Heimdall is a central, pluggable microservice written in Go that functions as both an Identity Orchestrator and a Policy Decision Point (PDP). It orchestrates best-in-class, cloud-native authentication and authorization services while maintaining complete decoupling from your business logic.

Go Version License

Table of Contents


Purpose

Heimdall solves the problem of tightly-coupled authentication and authorization in modern microservices architectures. Instead of building yet another custom auth service, Heimdall orchestrates industry-leading open-source tools into a cohesive, event-driven system.

Key Problems Solved

  1. Vendor Lock-In: Heimdall's pluggable architecture means you're never locked into a specific auth provider
  2. Business Logic Coupling: Heimdall knows nothing about your business domain—it only understands principals, resources, and attributes
  3. N+1 Query Problem: Built-in support for query planning enables efficient list filtering without individual checks
  4. Synchronous External Calls: Event-driven architecture prevents blocking calls to external services during authorization
  5. Token Security: Uses PASETO (not JWT) for cryptographically secure tokens by default

What Heimdall Does

  • Orchestrates Authentication: Wraps identity providers (like Ory Kratos/Hydra) for SSO, OIDC, and OAuth2 flows
  • Centralizes Authorization: Integrates with policy engines (like Cerbos) for fine-grained, attribute-based access control
  • Mints Secure Tokens: Issues PASETO v4 tokens with principal identity and attributes
  • Synchronizes State: Consumes events from business services (via NATS JetStream) to keep authorization attributes up-to-date
  • Enables Gateway Integration: Provides simple HTTP APIs that your API Gateway can call to make auth decisions

Architecture Philosophy

Heimdall follows a core principle: "Orchestrate, Don't Create"

Design Principles

  1. Pluggable by Design: All code is written against interfaces defined in /internal/core/interfaces.go. Implementations live in /internal/plugins/.

  2. Decoupled from Business Logic: Heimdall operates on generic concepts (principals, resources, attributes)—not your domain models.

  3. Event-Driven: External state synchronization happens asynchronously via message queues, never blocking auth decisions.

  4. Secure by Default: PASETO tokens, not JWT. Attribute-based access control, not role-based.

  5. Cloud-Native: Designed for Kubernetes, Docker Compose, and container orchestration from day one.


Tech Stack

Heimdall uses the following battle-tested technologies:

Component Technology Purpose
HTTP Framework go-chi/chi v5 Lightweight, idiomatic HTTP routing
Authentication Ory Kratos & Hydra Identity management and OAuth2/OIDC
Authorization Cerbos Policy decision point with attribute-based access control
Tokens PASETO v4 Platform-agnostic security tokens
Event Bus NATS JetStream Reliable, distributed messaging
Language Go 1.24+ Performance, concurrency, and simplicity

Getting Started

Prerequisites

  • Docker & Docker Compose (for local development)
  • Go 1.24+ (if building from source)
  • Make (optional, for build automation)

Quick Start with Docker Compose

The fastest way to run Heimdall with all dependencies:

# Clone the repository
git clone https://github.com/Nexlified/heimdall.git
cd heimdall

# Create .env file from example
cp .env.example .env

# Edit .env file and set secure values for:
# - POSTGRES_PASSWORD
# - KRATOS_DSN
# - HYDRA_OIDC_SALT
# - HYDRA_SECRETS_SYSTEM
# - PASETO_SYMMETRIC_KEY (must be exactly 32 bytes)

# Start all services (Heimdall, Kratos, Hydra, Cerbos, NATS)
docker-compose up -d

# Verify Heimdall is running
curl http://localhost:8080/health

This starts:

  • Heimdall on http://localhost:8080
  • Ory Kratos (admin: 4434, public: 4433)
  • Ory Hydra (admin: 4445, public: 4444)
  • Cerbos (gRPC: 3592, HTTP: 3593)
  • NATS JetStream (client: 4222, monitoring: 8222)

Configuration

Heimdall is configured via environment variables. For local development, copy the .env.example file to .env and customize the values:

cp .env.example .env

Required Environment Variables

The following environment variables must be set (they have no defaults):

# Database password for Kratos
POSTGRES_PASSWORD=your-secure-database-password

# Kratos database connection string
KRATOS_DSN=postgres://kratos:your-password@kratos-db:5432/kratos?sslmode=disable&max_conns=20

# Hydra OIDC pairwise salt (used for subject identifier generation)
HYDRA_OIDC_SALT=a-very-secret-salt-change-this

# Hydra system secrets (used for encryption)
HYDRA_SECRETS_SYSTEM=you-should-change-this-to-a-long-random-string

# PASETO symmetric key (MUST be exactly 32 bytes for AES-256)
PASETO_SYMMETRIC_KEY=change-this-to-32-byte-secret!!

Optional Environment Variables (with defaults)

# PostgreSQL database configuration
POSTGRES_USER=kratos                           # Default: kratos
POSTGRES_DB=kratos                             # Default: kratos

# Service connection URLs
NATS_URL=nats://nats:4222                      # Default: nats://nats:4222
KRATOS_ADMIN_URL=http://kratos:4434            # Default: http://kratos:4434
HYDRA_ADMIN_URL=http://hydra:4445              # Default: http://hydra:4445
CERBOS_GRPC_URL=cerbos:3592                    # Default: cerbos:3592

# Hydra DSN (use 'memory' for dev, postgres for production)
HYDRA_DSN=memory                               # Default: memory

# Server configuration
PORT=8080                                       # Default: 8080

Security Note: Never commit your .env file to version control. It's already included in .gitignore.

Example .env file

See .env.example for a complete template with all available options.


Usage Guide

Authentication Flow

1. Initiate Login

Your frontend redirects users to Heimdall's login endpoint:

# User navigates to:
GET http://localhost:8080/auth/login?redirect_uri=https://myapp.com/dashboard

This triggers the OAuth2/OIDC flow with Kratos and Hydra.

2. Handle Callback

After successful authentication, Kratos/Hydra redirects to:

GET http://localhost:8080/auth/callback?code=<authorization_code>

Heimdall exchanges the code for tokens and returns a PASETO token:

{
  "access_token": "v4.local.encoded-paseto-token...",
  "expires_in": 3600
}

3. Use the Token

Include the token in subsequent requests:

curl -H "Authorization: Bearer v4.local...." \
  http://localhost:8080/check

Authorization Checks

Check if a principal can perform actions on resources:

POST http://localhost:8080/check
Content-Type: application/json

{
  "principal": {
    "id": "user123",
    "roles": ["user", "editor"],
    "attr": {
      "department": "engineering",
      "plan": "pro"
    }
  },
  "resources": [
    {
      "kind": "document",
      "id": "doc456",
      "attr": {
        "owner": "user123",
        "status": "draft"
      },
      "actions": ["view", "edit", "delete"]
    }
  ]
}

Response (200 OK = allowed, 403 Forbidden = denied):

{
  "resourceId": "doc456",
  "actions": {
    "view": "EFFECT_ALLOW",
    "edit": "EFFECT_ALLOW",
    "delete": "EFFECT_DENY"
  }
}

Resource Planning

Solve the N+1 query problem by getting a query plan for filtering lists:

POST http://localhost:8080/plan/resources
Content-Type: application/json

{
  "principal": {
    "id": "user123",
    "roles": ["user"],
    "attr": {
      "department": "engineering"
    }
  },
  "resource": {
    "kind": "document",
    "attr": {}
  },
  "actions": ["view"]
}

Response:

{
  "filter": {
    "kind": "KIND_CONDITIONAL",
    "condition": {
      "expression": "request.resource.attr.owner == request.principal.id || request.resource.attr.public == true"
    }
  }
}

Use this filter in your database query to fetch only authorized resources.


Developer Documentation

Project Structure

heimdall/
├── cmd/
│   └── heimdall/
│       └── main.go              # Application entry point
├── internal/
│   ├── core/
│   │   └── interfaces.go        # Core interfaces (NEVER modify implementations here)
│   ├── handlers/
│   │   ├── http.go              # HTTP request handlers
│   │   └── http_test.go         # Handler tests
│   ├── plugins/                 # Plugin implementations
│   │   ├── authn/
│   │   │   └── kratos/          # Ory Kratos/Hydra identity provider
│   │   ├── authz/
│   │   │   └── cerbos/          # Cerbos policy engine
│   │   └── events/
│   │       └── nats/            # NATS JetStream event consumer
│   └── tokens/
│       └── paseto.go            # PASETO token service
├── infra/                       # Infrastructure configs
│   ├── kratos/
│   │   └── kratos.yml
│   └── cerbos/
│       └── policies/
├── docker-compose.yml           # Local development stack
├── Dockerfile                   # Multi-stage production build
└── go.mod                       # Go dependencies

Core Interfaces

All features are built against these interfaces (see /internal/core/interfaces.go):

IdentityProvider

type IdentityProvider interface {
    InitiateLogin(w http.ResponseWriter, r *http.Request)
    HandleAuthCallback(r *http.Request) (*TokenResponse, error)
    RefreshToken(refreshToken string) (*TokenResponse, error)
}

PolicyEngine

type PolicyEngine interface {
    Check(ctx context.Context, checkRequest []byte) ([]byte, error)
    PlanResources(ctx context.Context, planRequest []byte) ([]byte, error)
    UpdateAttributes(ctx context.Context, principalID string, attributes map[string]any) error
}

EventConsumer

type EventConsumer interface {
    Consume(pdp PolicyEngine) error
}

Building from Source

# Install dependencies
go mod download

# Build the binary
go build -o heimdall ./cmd/heimdall

# Run locally (requires env vars)
./heimdall

Running Tests

# Run all tests
go test ./...

# Run with verbose output
go test -v ./...

# Run specific package tests
go test ./internal/plugins/authz/cerbos/...

# Run with coverage
go test -cover ./...

Adding New Plugins

Heimdall's architecture makes it easy to add new implementations:

Example: Adding a new Identity Provider

  1. Create a new package under /internal/plugins/authn/<provider>/
  2. Implement the core.IdentityProvider interface
  3. Write comprehensive tests
  4. Update cmd/heimdall/main.go to wire it up
// internal/plugins/authn/auth0/auth0.go
package auth0

import "github.com/nexlified/heimdall/internal/core"

type Client struct {
    // Your implementation
}

func (c *Client) InitiateLogin(w http.ResponseWriter, r *http.Request) {
    // Implement Auth0 login flow
}

func (c *Client) HandleAuthCallback(r *http.Request) (*core.TokenResponse, error) {
    // Implement Auth0 callback handling
}

func (c *Client) RefreshToken(refreshToken string) (*core.TokenResponse, error) {
    // Implement token refresh
}

Integration Steps

Integrating with Your API Gateway

Heimdall is designed to work as a sidecar or upstream service for your API Gateway (Kong, Traefik, Nginx, etc.).

Pattern 1: Forward Auth (Traefik, Nginx)

Configure your gateway to call Heimdall before routing requests:

# Traefik example
http:
  middlewares:
    heimdall-auth:
      forwardAuth:
        address: "http://heimdall:8080/check"
        authResponseHeaders:
          - "X-User-ID"
          - "X-User-Roles"

Pattern 2: External Auth (Kong, Envoy)

# Kong example
plugins:
  - name: external-auth
    config:
      uri: "http://heimdall:8080/check"
      method: "POST"

Pattern 3: Direct Integration

Your application can call Heimdall's API directly:

// In your Go application
func (s *Server) ListDocuments(w http.ResponseWriter, r *http.Request) {
    // 1. Get user from token (set by gateway or middleware)
    userID := r.Header.Get("X-User-ID")
    
    // 2. Get query plan from Heimdall
    plan, err := s.heimdallClient.PlanResources(ctx, PlanRequest{
        Principal: Principal{ID: userID, Roles: []string{"user"}},
        Resource:  Resource{Kind: "document"},
        Actions:   []string{"view"},
    })
    
    // 3. Apply plan to database query
    docs, err := s.db.ListDocuments(ctx, plan.Filter)
    
    // 4. Return results
    json.NewEncoder(w).Encode(docs)
}

Event-Driven Attribute Synchronization

Heimdall stays up-to-date with your business state via NATS JetStream events.

Publishing Events from Your Services

Billing Service (when subscription changes):

// Publish subscription.updated event
natsConn.Publish("subscription.updated", []byte(`{
  "user_id": "user123",
  "plan": "enterprise",
  "attributes": {
    "max_users": 500,
    "features": ["sso", "audit", "priority_support"]
  }
}`))

Business Service (when usage metrics change):

// Publish usage.updated event
natsConn.Publish("usage.updated", []byte(`{
  "user_id": "user123",
  "attributes": {
    "current_users": 287,
    "storage_used_gb": 1250,
    "api_calls_today": 45000
  }
}`))

Heimdall's EventConsumer automatically:

  1. Listens for these events
  2. Validates and parses them
  3. Calls PolicyEngine.UpdateAttributes() to acknowledge receipt
  4. Ensures attributes are available for subsequent authorization checks

Example Integration Patterns

Microservices Architecture

┌─────────────┐      ┌──────────────┐      ┌─────────────┐
│   Frontend  │─────▶│ API Gateway  │─────▶│ Heimdall    │
│  (React)    │      │  (Traefik)   │      │  (AuthN/Z)  │
└─────────────┘      └──────────────┘      └─────────────┘
                             │                     │
                             │                     │
                             ▼                     ▼
                     ┌──────────────┐      ┌─────────────┐
                     │  Business    │─────▶│   NATS      │
                     │  Services    │      │ JetStream   │
                     └──────────────┘      └─────────────┘
                                                  │
                                                  │
                                           ┌──────▼──────┐
                                           │  Cerbos     │
                                           │  (Policies) │
                                           └─────────────┘

Single-Page Application

// Login flow
const login = async () => {
  window.location.href = 'http://heimdall.example.com/auth/login?redirect_uri=' 
    + encodeURIComponent(window.location.origin + '/dashboard');
};

// Handle callback
const handleCallback = async () => {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  
  const response = await fetch('http://heimdall.example.com/auth/callback', {
    method: 'GET',
    headers: { 'Authorization': `Bearer ${code}` }
  });
  
  const { access_token } = await response.json();
  localStorage.setItem('token', access_token);
};

// Make authorized requests
const fetchDocuments = async () => {
  const token = localStorage.getItem('token');
  
  const response = await fetch('http://api.example.com/documents', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  
  return response.json();
};

API Reference

Authentication Endpoints

Endpoint Method Description
/auth/login GET Initiates OAuth2/OIDC login flow
/auth/callback GET Handles OAuth2 callback, returns PASETO token
/auth/refresh POST Exchanges refresh token for new access token

Authorization Endpoints

Endpoint Method Description
/check POST Performs access control check
/plan/resources POST Returns query plan for list filtering

Request/Response Examples

See Usage Guide for detailed examples.


Contributing

We welcome contributions! Please follow these guidelines:

  1. Read the Architecture: Review /internal/core/interfaces.go and the Developer Documentation
  2. Write Tests: All new code must have comprehensive unit tests
  3. Follow Conventions: Use gofmt, go vet, and write idiomatic Go
  4. Open Issues First: Discuss significant changes before implementing
  5. Document Your Code: Add comments for complex logic

See Implemented-By-Copilot.md for examples of completed implementations.


License

This project is licensed under the MIT License. See LICENSE file for details.


Support


Built with ❤️ by the Nexlified team

About

"Heimdall" will be a central, decoupled microservice written in Go. It will function as both an Identity Orchestrator and a Policy Decision Point (PDP).

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •