Heimdall is an authentication and authorization service written in Go. Handles user accounts, password hashing (Argon2), JWT issuance and validation, and multi-tenant access control for all tenant applications.
Features
- JWT Authentication - RSA-signed tokens with user and tenant claims
- Password Security - Argon2id hashing with OWASP-recommended parameters
- Multi-Factor Authentication (MFA) - TOTP-based MFA with backup codes for enhanced security
- Trusted Devices - Users can mark devices as trusted to skip MFA (30-day sliding window)
- Required MFA Enforcement - Roles can require MFA; users are guided through setup on first login
- Session Management - List active sessions, revoke individual sessions, or sign out everywhere
- Refresh Token Rotation - Family-based tracking with automatic theft detection
- Email Verification - Registration flow with pluggable email delivery (webhook, mailman, or console logging)
- Password Reset - Secure token-based password reset via email
- Account Lockout - Progressive lockout after failed login attempts (5, 10, 15, 20 thresholds)
- OAuth/OIDC Login - Support for Google, Microsoft, GitHub, and custom OIDC providers
- Corporate SSO - Enterprise SSO with auto-provisioning and domain restrictions
- RBAC - Role-based access control with permissions, roles, and user assignments
- Multi-tenancy - Row-Level Security (RLS) for tenant isolation
- Structured Logging - Event constants for audit trails and observability
- Health Checks - Database connectivity monitoring (200 OK / 503 Service Unavailable)
- Dual APIs - HTTP REST API and gRPC for service-to-service communication
- Type-safe Database - sqlc-generated queries with PostgreSQL + pgx
Architecture
- HTTP API (port 8080) - Login/logout endpoints for user authentication
- gRPC API (port 9090) - User creation for internal services
- Database Layer - Context-based tenant isolation with RLS policies
- Password Hashing - Argon2id with secure parameters
- JWT Tokens - RSA signatures with configurable expiration
Development
Prerequisites
- Go 1.26+
- PostgreSQL 16+
- protoc (Protocol Buffers compiler)
- Docker (required for linting, integration tests, and code generation)
Setup
# Generate code (sqlc + protobuf)
make sqlc
make protoc
# Build development binary
make dev
# Format code
make fmt
# Run linters
make lint
# Run unit tests only (no Docker needed)
make unit
# Run all tests including integration (requires Docker)
make test
Database Migrations
# Run migrations
./bin/heimdall migrate up --database-url "postgres://..."
# Rollback last migration
./bin/heimdall migrate down --database-url "postgres://..."
# Check migration version
./bin/heimdall migrate version --database-url "postgres://..."
Database Cleanup
# Clean up expired tokens and old unverified accounts
./bin/heimdall cleanup --database-url "postgres://..."
# Customize unverified account age threshold (default: 7 days)
./bin/heimdall cleanup --database-url "postgres://..." --unverified-user-age-days 30
Running the Service
./bin/heimdall start \
--database-url "postgres://heimdall:password@localhost:5432/heimdall?sslmode=disable" \
--http-address ":8080" \
--grpc-address ":9090" \
--jwt-private-key "/path/to/private-key.pem" \
--jwt-public-key "/path/to/public-key.pem" \
--refresh-token-expiration "24h" \
--public-url "http://localhost:8080" \
--environment "development"
With no email configuration, tokens are logged to stdout (console mode). To send emails via webhook:
./bin/heimdall start ... --email-webhook-url "https://your-email-service/api/send"
Docker
# Build image
docker build -t heimdall:latest .
# Run container
docker run -p 8080:8080 -p 9090:9090 \
-e DATABASE_URL="postgres://..." \
-e JWT_PRIVATE_KEY_PATH="/keys/private.pem" \
heimdall:latest
API Endpoints
HTTP (Port 8080)
Public Endpoints:
HEAD /health - Health check (returns 200 OK if healthy, 503 if database unavailable)
GET /v1/oauth/supported-types - List supported OAuth provider types
Authentication:
POST /v1/register - Register new user (sends verification email)
POST /v1/verify-email - Verify email address
POST /v1/login - Authenticate user, returns JWT
DELETE /v1/refresh - Logout (invalidates refresh token)
POST /v1/refresh - Refresh access token (rotates refresh token)
Session Management:
GET /v1/sessions - List active sessions with metadata (IP, user agent, timestamps)
DELETE /v1/sessions - Revoke all sessions (sign out everywhere)
DELETE /v1/sessions/{id} - Revoke specific session
Password Reset:
POST /v1/forgot-password - Request password reset (sends email)
POST /v1/reset-password - Reset password with token
Multi-Factor Authentication (MFA):
POST /v1/mfa/verify - Verify MFA code during login (can trust device)
POST /v1/mfa/setup - Initiate MFA setup (returns QR code and backup codes)
POST /v1/mfa/enable - Enable MFA after validating TOTP code
DELETE /v1/mfa/disable - Disable MFA (requires password and TOTP/backup code)
POST /v1/mfa/backup-codes/regenerate - Regenerate backup codes (requires password)
GET /v1/mfa/status - Get MFA status and remaining backup codes
POST /v1/mfa/required-setup - Start MFA setup when role requires it (uses setup token)
POST /v1/mfa/required-enable - Enable MFA and complete login flow
OAuth/OIDC Authentication:
POST /v1/oauth/login - Start individual OAuth login (Google, Microsoft, GitHub)
POST /v1/sso/login - Start corporate SSO login by email domain
GET /v1/oauth/callback - OAuth callback endpoint (handles both flows)
OIDC Provider Management (requires authentication):
POST /v1/oauth/providers - Create OIDC provider with dynamic registration
GET /v1/oauth/providers - List all OIDC providers for tenant
GET /v1/oauth/providers/{id} - Get OIDC provider details
PUT /v1/oauth/providers/{id} - Update OIDC provider configuration
DELETE /v1/oauth/providers/{id} - Delete OIDC provider
User Profile:
GET /v1/users/me - Get current user profile
RBAC (requires authentication):
GET /v1/permissions - List all system permissions
POST /v1/roles - Create role
GET /v1/roles - List roles for tenant
GET /v1/roles/{id} - Get role details
PUT /v1/roles/{id} - Update role
DELETE /v1/roles/{id} - Delete role
GET /v1/roles/{id}/permissions - Get role permissions
PUT /v1/roles/{id}/permissions - Set role permissions
GET /v1/users/{id}/roles - Get user roles
PUT /v1/users/{id}/roles - Set user roles
GET /v1/users/{id}/permissions - Get user direct permissions
PUT /v1/users/{id}/permissions - Set user direct permissions
Permission naming
The permissions table is global rather than tenant-scoped, and every service behind
Heimdall registers its scopes into it. Permission names are therefore namespaced by owning
service, as one or more colon-separated segments:
heimdall:user:create Heimdall's own scopes
scorecard:tournaments:write a consuming service's scopes
Heimdall owns everything under heimdall:. Consuming services must prefix their scopes
with their own service name — an unprefixed name like user:read squats on a name another
service is likely to want, and the UNIQUE constraint on permissions.name means the
second service to claim it fails at seed time.
Heimdall's own scopes are defined once in iam.AllScopes (internal/iam/jwt.go) and
seeded by migration 003_seed_permissions. An integration test asserts the two match
exactly, so adding a scope to one without the other fails the build.
Scope checks are exact string comparisons. There is no wildcard or prefix matching, so
heimdall:* grants nothing.
gRPC (Port 9090)
CreateUser(email, tenant_id, role_ids) - Create user in tenant with roles
- Returns: user_id, email, tenant_id, verification_token
GetUserByID(user_id) - Retrieve user by ID (used by other services)
Testing
The project has both unit tests and integration tests:
- Unit tests (
make unit) - Fast, no external dependencies
- Integration tests (
make integration) - Full end-to-end tests against docker-compose infrastructure (PostgreSQL + mock OIDC server + heimdall). Requires Docker.
- All tests (
make test) - Runs unit + integration tests, starts infrastructure automatically
The integration test suite covers authentication, RBAC, MFA, sessions, OIDC/SSO flows, tenant isolation, and server-side input validation. Tests are organized by feature in test/ subdirectories (password, rbac, session, mfa, oidc, isolation).
CI/CD Pipeline
The project uses GitHub Actions for continuous integration:
Automated Checks
- Linting -
golangci-lint
- Build - Compile and verify binary works
- Tests - Unit and integration tests with docker-compose infrastructure
- Security -
govulncheck for known vulnerabilities
- Code Generation - Verify sqlc/protobuf are up-to-date
- Docker Build - Validate Dockerfile builds
- Migrations - Test SQL migrations against PostgreSQL
Dependabot
Automated dependency updates run weekly for:
- Go modules
- Docker base images
- GitHub Actions versions
Security Considerations
Authentication & Cryptography
- Argon2id - Memory-hard password hashing (64MB, 2 iterations, 4 threads)
- JWT RSA signatures - Asymmetric keys for token signing/verification
- Constant-time comparison - Prevents timing attacks on passwords
- HTTPS recommended - For production deployments
Token Rotation & Theft Detection
Refresh tokens use family-based rotation to detect and respond to token theft:
- Each login creates a new token family (UUID)
- On refresh, the old token is revoked and a new one is issued with the same family ID
- If a revoked token is replayed (theft attempt), the entire token family is revoked
- Separate logins create independent families, so one compromised session doesn't affect others
This provides defense-in-depth: even if an attacker steals a refresh token, using it after the legitimate user refreshes will invalidate all tokens in that family.
Trusted Device Security
Trusted devices allow users to skip MFA on recognized devices:
- Device tokens are hashed (SHA-256) before storage
- Tokens have 30-day expiration with sliding window (extends on use)
- Automatically revoked on: sign out everywhere, password change, token reuse detection
- Token prefix (
hmdl_device_) enables secret scanning detection
Multi-Tenant Isolation
- Row-Level Security (RLS) - Database-enforced tenant isolation at the PostgreSQL layer
- JOIN-based RLS policies - Junction tables use EXISTS subqueries to validate entity relationships:
-- Example: user_roles table RLS policy
CREATE POLICY tenant_isolation_policy ON user_roles
FOR ALL TO PUBLIC
USING (EXISTS (
SELECT 1 FROM users WHERE users.id = user_roles.user_id
) AND EXISTS (
SELECT 1 FROM roles WHERE roles.id = user_roles.role_id
));
This approach provides:
- Normalized schema - No redundant
tenant_id in junction tables
- Referential integrity - Prevents cross-tenant associations (e.g., assigning Tenant A's role to Tenant B's user)
- Defense in depth - Database blocks invalid operations even if application code has bugs
- Security by design - RLS filtering happens automatically on parent tables
Container Security
- Non-root container - Docker runs as
heimdall:heimdall (uid 1000)
Configuration
Environment variables:
DATABASE_URL - PostgreSQL connection string
HTTP_ADDRESS - HTTP server address (default: :8080)
GRPC_ADDRESS - gRPC server address (default: :9090)
JWT_ISSUER - JWT issuer name (default: heimdall)
JWT_PRIVATE_KEY_PATH - Path to RSA private key (PEM format)
JWT_PUBLIC_KEY_PATH - Path to RSA public key (PEM format)
REFRESH_TOKEN_EXPIRATION - Session lifetime before re-authentication (default: 24h)
ACCESS_TOKEN_EXPIRATION - Access token lifetime (default: 15m)
PUBLIC_URL - Base URL for email verification and password reset links (default: http://localhost:8080)
ENVIRONMENT - Environment name: development, staging, production (default: development)
TRUSTED_PROXY_MODE - Enable IP extraction from X-Forwarded-For headers (default: false)
CORS_ALLOWED_ORIGINS - Comma-separated list of allowed CORS origins
ENCRYPTION_KEY - 32-byte hex key for encrypting sensitive data (OIDC client secrets, MFA TOTP secrets)
TOTP_PERIOD - TOTP time window in seconds (default: 30)
Email Delivery (if neither is set, tokens are logged to stdout):
EMAIL_WEBHOOK_URL - HTTP webhook URL for email delivery (receives JSON POST with email events)
MAILMAN_GRPC_ADDRESS - Mailman gRPC server address
OAuth Provider Configuration (optional):
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET - Google OAuth credentials
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, MICROSOFT_TENANT_ID - Microsoft OAuth credentials
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET - GitHub OAuth credentials
License
See LICENSE file.