This guide covers all configuration options for AuthGate, including environment variables, secrets management, and advanced features.
- Environment Variables
- TLS / HTTPS
- Bootstrap and Shutdown Timeouts
- Generate Strong Secrets
- Token Lifetime Profiles
- Default Test Data
- OAuth Third-Party Login
- Service-to-Service Authentication
- HTTP Retry with Exponential Backoff
- User Cache
- Client Cache
- Token Cache
- Rate Limiting
- CORS (Cross-Origin Resource Sharing)
Create a .env file in the project root:
# Server Configuration
SERVER_ADDR=:8080 # Listen address (e.g., :8080, 0.0.0.0:8080)
BASE_URL=http://localhost:8080 # Public URL for verification_uri
# TLS / HTTPS (optional) — set both to serve HTTPS on SERVER_ADDR
# TLS_CERT_FILE=/etc/authgate/tls/fullchain.pem
# TLS_KEY_FILE=/etc/authgate/tls/privkey.pem
# Security - CHANGE THESE IN PRODUCTION!
JWT_SECRET=your-256-bit-secret-change-in-production # HMAC-SHA256 signing key
SESSION_SECRET=session-secret-change-in-production # Cookie encryption key
# Database
DATABASE_DRIVER=sqlite # Database driver: "sqlite" or "postgres"
DATABASE_DSN=oauth.db # Connection string (file path for SQLite, DSN for PostgreSQL)
# PostgreSQL Example:
# DATABASE_DRIVER=postgres
# DATABASE_DSN="host=localhost user=authgate password=secret dbname=authgate port=5432 sslmode=disable"
# Database Log Level
# DB_LOG_LEVEL=warn # GORM log level: "silent", "error", "warn" (default), "info"
# Default Admin User
# Set a custom password for the default admin user created on first startup
# If not set, a random 16-character password will be generated and written to authgate-credentials.txt
# DEFAULT_ADMIN_PASSWORD=your-secure-admin-password
# Authentication Mode
# Options: local, http_api
# Default: local
AUTH_MODE=local
# HTTP API Authentication (when AUTH_MODE=http_api)
HTTP_API_URL=https://auth.example.com/api/verify
HTTP_API_TIMEOUT=10s
HTTP_API_INSECURE_SKIP_VERIFY=false
# HTTP API Retry Configuration
# Automatic retry with exponential backoff for failed requests
HTTP_API_MAX_RETRIES=3 # Maximum retry attempts (default: 3, set 0 to disable)
HTTP_API_RETRY_DELAY=1s # Initial retry delay (default: 1s)
HTTP_API_MAX_RETRY_DELAY=10s # Maximum retry delay (default: 10s)
# JWT Token Expiration
JWT_EXPIRATION=10h # Access token lifetime (default: 10h)
JWT_EXPIRATION_JITTER=30m # Max random jitter on access token expiry (default: 30m)
# Must be less than JWT_EXPIRATION. Prevents refresh thundering herd.
# Example: JWT_EXPIRATION=8h + JWT_EXPIRATION_JITTER=30m → lifetime [8h, 8h30m)
# Refresh Token Configuration
REFRESH_TOKEN_EXPIRATION=720h # Refresh token lifetime (default: 30 days)
ENABLE_REFRESH_TOKENS=true # Feature flag to enable/disable refresh tokens
ENABLE_TOKEN_ROTATION=false # Enable rotation mode (default: fixed mode)
# Client Credentials Flow (RFC 6749 §4.4)
# CLIENT_CREDENTIALS_TOKEN_EXPIRATION=1h # Access token lifetime for client_credentials grant (default: 1h)
# # Keep short — no refresh token means no rotation mechanism
# # Governed independently from per-client TokenProfile (see below)
# Per-Client Token Lifetime Profiles
# Each OAuth client selects one of three presets: "short", "standard" (default), or "long".
# "standard" defaults to JWT_EXPIRATION / REFRESH_TOKEN_EXPIRATION above; overrides below
# let you tailor the short/long presets without touching the base defaults.
# TOKEN_PROFILE_SHORT_ACCESS_TTL=15m # Short profile access token lifetime (default: 15m)
# TOKEN_PROFILE_SHORT_REFRESH_TTL=24h # Short profile refresh token lifetime (default: 24h)
# TOKEN_PROFILE_STANDARD_ACCESS_TTL=10h # Standard profile access TTL (default: JWT_EXPIRATION)
# TOKEN_PROFILE_STANDARD_REFRESH_TTL=720h # Standard profile refresh TTL (default: REFRESH_TOKEN_EXPIRATION)
# TOKEN_PROFILE_LONG_ACCESS_TTL=24h # Long profile access TTL (default: 24h)
# TOKEN_PROFILE_LONG_REFRESH_TTL=2160h # Long profile refresh TTL (default: 90 days)
#
# Hard caps — enforced at startup. No profile may exceed these values.
# JWT_EXPIRATION_MAX=24h # Upper bound for any access-token profile (default: 24h)
# REFRESH_TOKEN_EXPIRATION_MAX=2160h # Upper bound for any refresh-token profile (default: 90d)
# OAuth Configuration (optional - for third-party login)
# GitHub OAuth
GITHUB_OAUTH_ENABLED=false
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GITHUB_REDIRECT_URL=http://localhost:8080/auth/callback/github
GITHUB_SCOPES=user:email
# Gitea OAuth
GITEA_OAUTH_ENABLED=false
GITEA_URL=https://gitea.example.com
GITEA_CLIENT_ID=your_gitea_client_id
GITEA_CLIENT_SECRET=your_gitea_client_secret
GITEA_REDIRECT_URL=http://localhost:8080/auth/callback/gitea
GITEA_SCOPES=read:user
# Microsoft Entra ID (Azure AD) OAuth
MICROSOFT_OAUTH_ENABLED=false
MICROSOFT_TENANT_ID=common
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URL=http://localhost:8080/auth/callback/microsoft
MICROSOFT_SCOPES=openid,profile,email,User.Read
# OAuth Settings
OAUTH_AUTO_REGISTER=true # Allow OAuth to auto-create accounts (default: true)
OAUTH_TIMEOUT=15s # HTTP client timeout for OAuth requests (default: 15s)
OAUTH_INSECURE_SKIP_VERIFY=false # Skip TLS verification for OAuth (dev/testing only, default: false)
# Authorization Code Flow (RFC 6749 + RFC 7636)
AUTH_CODE_EXPIRATION=10m # Authorization code lifetime (default: 10 min)
PKCE_REQUIRED=false # Require PKCE for all clients, including confidential (default: false)
CONSENT_REMEMBER=true # Skip consent page if user already approved same scopes (default: true)
# Dynamic Client Registration (RFC 7591)
ENABLE_DYNAMIC_CLIENT_REGISTRATION=false # Enable POST /oauth/register (default: false)
DYNAMIC_CLIENT_REGISTRATION_TOKEN= # Optional Bearer token for protected registration
DYNAMIC_CLIENT_REGISTRATION_RATE_LIMIT=5 # Rate limit (default: 5 req/min)
# User Cache
# Caches GetUserByID results — called on every protected request (RequireAuth + RequireAdmin)
# USER_CACHE_TYPE=memory # Options: memory, redis, redis-aside (default: memory)
# USER_CACHE_TTL=5m # How long to cache a user object (default: 5m)
# USER_CACHE_CLIENT_TTL=30s # Client-side TTL for redis-aside mode only (default: 30s)
# USER_CACHE_SIZE_PER_CONN=32 # Client-side cache size per connection in MB for redis-aside (default: 32MB)
# Audit Logging
# Comprehensive audit logging for security and compliance
ENABLE_AUDIT_LOGGING=true # Enable audit logging (default: true)
AUDIT_LOG_RETENTION=2160h # Retention period: 90 days (default: 90 days = 2160h)
AUDIT_LOG_BUFFER_SIZE=1000 # Async buffer size (default: 1000)
AUDIT_LOG_CLEANUP_INTERVAL=24h # Cleanup frequency (default: 24h)AuthGate can serve HTTPS directly by setting two environment variables. When both are configured, the server listens on SERVER_ADDR using TLS. When both are empty (the default), it serves plain HTTP. Setting only one of the two is rejected at startup by Config.Validate() — this prevents silently falling back to HTTP when the operator meant to enable TLS.
TLS_CERT_FILE=/etc/authgate/tls/fullchain.pem # PEM-encoded certificate (full chain)
TLS_KEY_FILE=/etc/authgate/tls/privkey.pem # PEM-encoded private keyNotes:
- Both variables must be set together. Setting only one causes
Config.Validate()to fail at startup (prevents accidental HTTP fallback when TLS was intended). Leave both empty for plain HTTP. - Use a full chain certificate (leaf + intermediates). Clients often reject leaf-only certificates from non-root CAs.
- Update
BASE_URLtohttps://...so OAuth redirect URIs,verification_uri, and JWKS links use the correct scheme. - Cipher suites / TLS versions use Go's
crypto/tlsdefaults — modern, secure, no tuning needed for typical deployments. - No hot reload. Renewed certificates require restarting AuthGate. For zero-downtime certificate rotation (ACME/Let's Encrypt), terminate TLS at a reverse proxy (nginx, Caddy, Cloudflare) instead.
Quick local test with a self-signed certificate:
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \
-days 1 -subj "/CN=localhost"
TLS_CERT_FILE=cert.pem TLS_KEY_FILE=key.pem BASE_URL=https://localhost:8080 \
./bin/authgate server
curl -k https://localhost:8080/healthAuthGate supports configurable timeout durations for all lifecycle operations, enabling production tuning and graceful degradation.
Initialization operations share a unified context flow from the graceful shutdown manager, while shutdown operations run with independent, timeout-bound contexts:
- Initialization timeouts: Control how long to wait for database, Redis, and cache connections during startup and are cancelled if the manager context is stopped (for example, with Ctrl+C)
- Shutdown timeouts: Control how long to wait for graceful cleanup of resources; each shutdown job runs with a fresh context derived from
context.Background()and is bounded only by its configured timeout - Cancellation support: Pressing Ctrl+C during startup cancels in-flight initialization work via the manager context; once shutdown has begun, shutdown work continues until its timeout expires, even if Ctrl+C is pressed again
All timeout values use Go duration format (e.g., 30s, 1m, 5m30s):
# Database Initialization and Shutdown
DB_INIT_TIMEOUT=30s # Database connection and migration timeout (default: 30s)
DB_CLOSE_TIMEOUT=5s # Database connection close timeout (default: 5s)
# Redis Connection and Shutdown
REDIS_CONN_TIMEOUT=5s # Redis connection health check timeout (default: 5s)
REDIS_CLOSE_TIMEOUT=5s # Redis connection close timeout (default: 5s)
# Cache Initialization and Shutdown
CACHE_INIT_TIMEOUT=5s # Cache initialization timeout (default: 5s)
CACHE_CLOSE_TIMEOUT=5s # Cache close timeout (default: 5s)
# Server Graceful Shutdown
SERVER_SHUTDOWN_TIMEOUT=5s # HTTP server graceful shutdown timeout (default: 5s)
AUDIT_SHUTDOWN_TIMEOUT=10s # Audit service shutdown timeout (default: 10s)Slow Network Connections
# Increase timeouts for remote database/Redis
DB_INIT_TIMEOUT=60s
REDIS_CONN_TIMEOUT=15sLarge Audit Buffer
# Allow more time to flush audit logs on shutdown
AUDIT_SHUTDOWN_TIMEOUT=30sFast Deployment Rollouts
# Reduce shutdown timeouts for faster pod termination
SERVER_SHUTDOWN_TIMEOUT=3s
DB_CLOSE_TIMEOUT=2s- Keep close timeouts short (5s or less) to prevent hanging on shutdown
- Increase init timeouts for slow networks or large databases
- Match cache timeout to your connection reliability
- Test timeout values in staging before production
- Monitor timeout errors in logs to tune values
- Initialization: If a timeout is exceeded, the application exits with an error
- Shutdown: Shutdown waits up to the configured timeout for close operations; if the timeout elapses, shutdown continues and reports a timeout error
- Cancellation: Pressing Ctrl+C triggers graceful shutdown and cancels operations that honor the manager context, but does not forcibly abort in-progress shutdown jobs
- Errors: Timeout errors include context (e.g., "database close timeout: context deadline exceeded")
# Generate JWT_SECRET (64 characters recommended)
openssl rand -hex 32
# Generate SESSION_SECRET (64 characters recommended)
openssl rand -hex 32
# Or use this one-liner to update .env
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env
echo "SESSION_SECRET=$(openssl rand -hex 32)" >> .envAuthGate supports three JWT signing algorithms:
| Algorithm | Type | Key | Use Case |
|---|---|---|---|
HS256 |
Symmetric | JWT_SECRET (shared secret) |
Default, simple deployments |
RS256 |
Asymmetric | RSA private key (2048+ bits) | Resource servers verify with public key |
ES256 |
Asymmetric | ECDSA P-256 private key | Compact tokens, modern deployments |
# HS256 (default — no additional config needed)
JWT_SIGNING_ALGORITHM=HS256
# RS256
JWT_SIGNING_ALGORITHM=RS256
JWT_PRIVATE_KEY_PATH=/path/to/rsa-private.pem
JWT_KEY_ID= # Optional: auto-generated from key fingerprint
# ES256
JWT_SIGNING_ALGORITHM=ES256
JWT_PRIVATE_KEY_PATH=/path/to/ec-private.pem
JWT_KEY_ID= # Optional: auto-generated from key fingerprint# RSA 2048-bit key for RS256
openssl genrsa -out rsa-private.pem 2048
# ECDSA P-256 key for ES256
openssl ecparam -genkey -name prime256v1 -noout -out ec-private.pemWhen using RS256 or ES256, AuthGate exposes the public key at:
GET /.well-known/jwks.json
Resource servers can fetch this endpoint to verify JWT signatures without sharing secrets. The OIDC Discovery endpoint (/.well-known/openid-configuration) includes the jwks_uri field automatically when asymmetric keys are available. For a complete guide on verifying tokens at resource servers, see the JWT Verification Guide.
The JWKS response includes a Cache-Control: public, max-age=3600 header (1 hour). Resource servers should respect this cache directive; after key rotation, allow up to 1 hour for cached JWKS entries to expire.
For HS256, the JWKS endpoint returns an empty key set ({"keys":[]}) since symmetric secrets are never exposed.
AuthGate supports the following PEM-encoded private key formats:
| Format | PEM Header | Algorithms |
|---|---|---|
| PKCS#1 | BEGIN RSA PRIVATE KEY |
RS256 |
| PKCS#8 | BEGIN PRIVATE KEY |
RS256/ES256 |
| SEC1 | BEGIN EC PRIVATE KEY |
ES256 |
PEM files with multiple blocks (e.g., EC PARAMETERS followed by EC PRIVATE KEY) are scanned automatically — the loader iterates through all blocks until it finds a supported key.
AuthGate validates signing keys at startup and rejects invalid configurations:
| Rule | Detail |
|---|---|
| RS256 minimum key size | 2048 bits (smaller RSA keys are rejected) |
| ES256 curve | P-256 only (P-384, P-521, and other curves are not supported) |
| Key type must match algorithm | RSA key for RS256, ECDSA key for ES256 |
| Key pair match | Public key must correspond to the private key |
| Unknown algorithm | Algorithms other than HS256/RS256/ES256 are rejected at startup |
Use JWT_KEY_ID to set an explicit kid (Key ID) header in JWTs. This enables key rotation:
- Generate a new key pair
- Update
JWT_PRIVATE_KEY_PATHandJWT_KEY_IDto point to the new key - Restart the server — it will begin signing new tokens with the new key
- Resource servers match the
kidheader to select the correct verification key from JWKS
Note: The JWKS endpoint serves a single active public key at a time. For zero-downtime rotation, pre-cache the new JWKS at resource servers before switching, or accept a brief gap while cached JWKS entries expire (up to 1 hour due to
Cache-Control: max-age=3600). Multi-key JWKS is not currently supported.
If JWT_KEY_ID is not set, it is automatically derived from the SHA-256 hash of the DER-encoded public key (base64url-encoded, 43 characters). This derivation is deterministic — the same key always produces the same kid.
AuthGate assigns every OAuth client one of three token lifetime presets so admins can tune access and refresh token durations to each client's risk profile without touching the base JWT configuration. The preset is selectable from the admin UI (Admin → OAuth Clients → Token Lifetime) and recorded on the client as token_profile.
| Profile | When to use | Default access TTL | Default refresh TTL |
|---|---|---|---|
short |
High-security apps (admin consoles, financial dashboards) | 15 min | 24 h |
standard |
Typical web/SPA clients (default for new clients) | JWT_EXPIRATION (10 h) |
REFRESH_TOKEN_EXPIRATION (30 d) |
long |
CLI tools, IoT devices, long-lived background jobs | 24 h | 90 d |
Defaults are overridable per environment via the TOKEN_PROFILE_* variables listed in Environment Variables.
JWT_EXPIRATION_MAX and REFRESH_TOKEN_EXPIRATION_MAX bound every profile's TTL. The server refuses to start if any configured profile exceeds its cap — this guarantees that a stray env override cannot issue tokens longer than the operator intends.
JWT_EXPIRATION_JITTER is applied only when the resolved access-token TTL matches the base JWT_EXPIRATION (the standard-profile default). Explicit short/long overrides — and a standard profile that has been explicitly diverged from the base config — use the profile's TTL exactly, with no jitter added. This keeps jitter working for the high-volume default path (preventing refresh thundering herds) while respecting operator-chosen short/long lifetimes precisely.
The client_credentials grant is governed by CLIENT_CREDENTIALS_TOKEN_EXPIRATION and ignores the client's TokenProfile. M2M tokens carry a larger blast radius than user-delegated tokens (no refresh, no user-revoke UI), so their lifetime is managed separately and is typically kept much shorter than user-facing tokens. If you need per-client M2M TTLs, open an issue — it will require a dedicated field on TokenProfile rather than overloading the existing access TTL.
Updates take effect on the next token issuance or refresh. Existing tokens retain the lifetime they were originally issued with; AuthGate does not retroactively shorten live tokens. Every TokenProfile change is recorded in the audit log at WARNING severity with the previous value (previous_token_profile) for forensic traceability.
The server initializes with default test accounts:
- Username:
admin - Password: Set via
DEFAULT_ADMIN_PASSWORDenvironment variable, or auto-generated 16-character random password (written toauthgate-credentials.txton first run)
- Name:
AuthGate CLI - Client ID: Auto-generated UUID (written to
authgate-credentials.txt)
DEFAULT_ADMIN_PASSWORD environment variable. If not set, a random password will be generated and written to authgate-credentials.txt (mode 0600) on first run. Delete this file after retrieving the credentials.
AuthGate supports OAuth 2.0 authentication with third-party providers, allowing users to sign in with their existing accounts from GitHub, Gitea, and other OAuth providers.
- GitHub - Sign in with GitHub accounts
- Gitea - Sign in with self-hosted or public Gitea instances
- Microsoft Entra ID (Azure AD) - Sign in with Microsoft work, school, or personal accounts
- Extensible - Easy to add GitLab, Google, or other OAuth 2.0 providers
- Email-Based Account Linking: Automatically links OAuth accounts to existing users with matching email addresses
- Auto-Registration: New users can be automatically created via OAuth login
- Multiple Authentication Methods: Users can have both password and OAuth authentication
- Profile Sync: Avatar and profile information synced from OAuth providers
- Secure by Default: CSRF protection via state parameter, TLS verification enabled
- Create OAuth Application in your provider (GitHub/Gitea)
- Configure AuthGate with client credentials:
# Enable GitHub OAuth
GITHUB_OAUTH_ENABLED=true
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
GITHUB_REDIRECT_URL=http://localhost:8080/auth/callback/github
# Enable Gitea OAuth
GITEA_OAUTH_ENABLED=true
GITEA_URL=https://gitea.example.com
GITEA_CLIENT_ID=your_client_id
GITEA_CLIENT_SECRET=your_client_secret
GITEA_REDIRECT_URL=http://localhost:8080/auth/callback/gitea
# Enable Microsoft Entra ID OAuth
MICROSOFT_OAUTH_ENABLED=true
MICROSOFT_TENANT_ID=common
MICROSOFT_CLIENT_ID=your_client_id
MICROSOFT_CLIENT_SECRET=your_client_secret
MICROSOFT_REDIRECT_URL=http://localhost:8080/auth/callback/microsoft- Restart server and visit
/loginto see OAuth buttons
Scenario 1: New User
- User clicks "Sign in with GitHub"
- GitHub returns email: alice@example.com
- System creates new user with GitHub OAuth connection
- User is logged in
Scenario 2: Existing User (Email Match)
- User Bob already has account: bob@example.com
- Bob clicks "Sign in with GitHub"
- GitHub returns same email: bob@example.com
- System automatically links GitHub to Bob's account
- Bob can now login with either password or GitHub
Scenario 3: Multiple OAuth Accounts
- User can link multiple OAuth providers (GitHub + Gitea + Microsoft)
- All methods log into the same AuthGate account
- HTTPS Required: Always use HTTPS in production
- Email Validation: OAuth providers must return verified email addresses
- TLS Verification: Never set
OAUTH_INSECURE_SKIP_VERIFY=truein production - Token Storage: OAuth tokens stored in database (consider encryption at rest)
For complete setup instructions including:
- Step-by-step provider configuration
- Production deployment guidelines
- Troubleshooting common issues
- Adding custom OAuth providers
When AuthGate connects to external HTTP APIs (for authentication), you can secure these service-to-service communications with authentication headers.
External HTTP API providers (authentication services) need to verify that incoming requests are from a trusted AuthGate instance. Without authentication, these endpoints would be vulnerable to unauthorized access.
AuthGate supports three authentication modes for securing HTTP API communications:
1. None Mode (Default)
No authentication headers are added. Suitable for development or when the external API is secured by other means (e.g., network isolation).
# No configuration needed - this is the default
HTTP_API_AUTH_MODE=none2. Simple Mode
Adds a shared secret in a custom header (default: X-API-Secret). Quick to set up but less secure than HMAC.
HTTP_API_AUTH_MODE=simple
HTTP_API_AUTH_SECRET=your-shared-secret-here
HTTP_API_AUTH_HEADER=X-API-Secret # Optional, default shown3. HMAC Mode (Recommended)
Uses HMAC-SHA256 signature with timestamp validation to prevent replay attacks. Provides the highest security for production environments.
HTTP_API_AUTH_MODE=hmac
HTTP_API_AUTH_SECRET=your-hmac-secret-hereHMAC mode automatically adds these headers to each request:
X-Signature: HMAC-SHA256 signature oftimestamp + method + path + bodyX-Timestamp: Unix timestamp (validated within 5-minute window)X-Nonce: Unique request identifier
| Environment Variable | Purpose |
|---|---|
HTTP_API_AUTH_MODE |
Auth mode for user authentication |
HTTP_API_AUTH_SECRET |
Shared secret for authentication |
HTTP_API_AUTH_HEADER |
Custom header name (simple mode) |
Your external API must verify incoming requests. Here's a Go example for HMAC verification:
import httpclient "github.com/appleboy/go-httpclient"
// Initialize auth config (server side)
authConfig := httpclient.NewAuthConfig("hmac", "your-hmac-secret")
// Verify incoming request
err := authConfig.VerifyHMACSignature(req, 5*time.Minute)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}Scenario: Your company has a central authentication service that AuthGate should use for user login.
Setup:
- Configure AuthGate to use external authentication with HMAC:
# .env file
AUTH_MODE=http_api
HTTP_API_URL=https://auth.company.com/api/verify
HTTP_API_AUTH_MODE=hmac
HTTP_API_AUTH_SECRET=shared-secret-between-services-
Your authentication API validates the HMAC signature before processing login requests.
-
When users log into AuthGate, their credentials are forwarded to your API with HMAC signature verification.
AuthGate includes automatic HTTP retry capabilities for all external API communications (authentication and token operations) to improve reliability and resilience against transient network failures.
- Automatic Retries: Failed HTTP requests are automatically retried with configurable attempts
- Exponential Backoff: Retry delays increase exponentially to avoid overwhelming failing services
- Smart Retry Logic: Only retries on appropriate errors (network failures, 5xx errors, 429 rate limits)
- Non-Blocking: Retries respect context cancellation and timeouts
By default, AuthGate retries failed requests up to 3 times with the following pattern:
- Initial delay: 1 second
- Maximum delay: 10 seconds
- Multiplier: 2.0x (exponential backoff)
Example retry sequence:
- First attempt fails → wait 1s
- Second attempt fails → wait 2s
- Third attempt fails → wait 4s
- Fourth attempt fails → return error
Requests are automatically retried on:
- Network errors (connection failures, timeouts, DNS issues)
- HTTP 5xx server errors (500, 502, 503, 504)
- HTTP 429 (Too Many Requests)
Requests are not retried on:
- HTTP 4xx client errors (except 429)
- HTTP 2xx/3xx successful responses
- Context cancellation or timeout
Configure retry behavior for each external service independently:
HTTP_API_MAX_RETRIES=5 # Maximum retry attempts (default: 3)
HTTP_API_RETRY_DELAY=2s # Initial retry delay (default: 1s)
HTTP_API_MAX_RETRY_DELAY=30s # Maximum retry delay (default: 10s)To disable retries (not recommended for production):
HTTP_API_MAX_RETRIES=01. Handling Transient Network Issues
Temporary network glitches are automatically handled without failing the entire request:
- Brief network interruptions
- DNS resolution delays
- Connection pool exhaustion
2. Service Restarts
When external services restart, AuthGate automatically retries until the service is available:
- Rolling deployments
- Service updates
- Container restarts
3. Rate Limiting
When external APIs return 429 (rate limit), AuthGate backs off and retries:
- Automatic backoff on rate limits
- Prevents cascading failures
- Respects service quotas
- Production Settings: Use default retry settings (3 retries) for most production scenarios
- High-Traffic Environments: Consider increasing
MAX_RETRY_DELAYto 30s-60s to avoid overwhelming recovering services - Low-Latency Requirements: Reduce
MAX_RETRIESto 1-2 for time-sensitive operations - Monitoring: Track retry rates to identify unreliable external services
- Timeouts: Ensure
HTTP_API_TIMEOUTis set appropriately to account for retries
For critical services where availability is paramount:
# Retry up to 10 times with longer delays
HTTP_API_MAX_RETRIES=10
HTTP_API_RETRY_DELAY=500ms
HTTP_API_MAX_RETRY_DELAY=60s
HTTP_API_TIMEOUT=120s # Increase timeout to accommodate retriesFor fast-fail scenarios where latency matters more than resilience:
# Retry only once with short delays
HTTP_API_MAX_RETRIES=1
HTTP_API_RETRY_DELAY=500ms
HTTP_API_MAX_RETRY_DELAY=2s
HTTP_API_TIMEOUT=15s- Built using go-httpretry v0.2.0
- Retry logic wraps the authentication-enabled HTTP client
- All authentication headers (Simple, HMAC) are preserved across retries
- Request bodies are cloned for retries to avoid consumed stream issues
GetUserByID is called on every protected request — once by the RequireAuth middleware and once more by RequireAdmin. Without caching, each request incurs at least one synchronous DB round-trip. Under heavy traffic or DDoS conditions this translates directly into database pressure.
AuthGate ships with a built-in user cache (always enabled, no feature flag required) that absorbs these lookups before they reach the database.
The cache uses a cache-aside pattern:
- On the first request for a user ID, the DB is queried and the result is stored in cache with a TTL
- Subsequent requests within the TTL window are served entirely from cache
- Cache entries are invalidated automatically whenever user data is mutated (OAuth sync, profile updates)
| Backend | Env value | Use case |
|---|---|---|
| Memory | memory (default) |
Single-instance, zero external dependencies |
| Redis | redis |
2–5 pods, shared cache across instances |
| Redis-aside | redis-aside |
5+ pods, client-side caching with stampede protection — requires Redis >= 7.0 |
# Cache backend: memory (default), redis, or redis-aside
USER_CACHE_TYPE=memory
# How long a cached user object is valid (default: 5m); must be > 0
# Shorter → password/role changes propagate faster
# Longer → more aggressive DB protection
USER_CACHE_TTL=5m
# Client-side TTL for redis-aside mode only (default: 30s); must be > 0
USER_CACHE_CLIENT_TTL=30s
# Client-side cache size per connection in MB for redis-aside mode only (default: 32MB)
# Total memory per pod = cache_size × connections (~10 based on GOMAXPROCS) → default ~320MB
USER_CACHE_SIZE_PER_CONN=32Redis-based backends also require the shared Redis settings:
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0| TTL | Behaviour |
|---|---|
1m |
Role or password changes take effect within 1 minute |
5m |
Default — good balance between security and DB protection |
15m |
Aggressive DB protection; suitable when user data rarely changes |
For Kubernetes or cloud deployments with multiple replicas:
# 2–5 pods: Redis shared cache
USER_CACHE_TYPE=redis
REDIS_ADDR=redis-service:6379
# 5+ pods or DDoS protection: redis-aside with client-side caching
USER_CACHE_TYPE=redis-aside
REDIS_ADDR=redis-service:6379
USER_CACHE_CLIENT_TTL=30s
USER_CACHE_SIZE_PER_CONN=32 # Adjust based on available memory per podNote:
redis-asideuses RESP3 client-side caching for automatic invalidation across all pods and requires Redis >= 7.0. If you are running an older Redis version, useUSER_CACHE_TYPE=redisinstead. Memory usage per pod isUSER_CACHE_SIZE_PER_CONN × ~10 connections(default ~320MB). AdjustUSER_CACHE_SIZE_PER_CONNif memory is constrained.
Every OAuth flow (device code, authorization code, token exchange, client credentials) queries the OAuthApplication record to validate the client. Caching these lookups reduces database pressure on busy deployments.
The cache is always enabled with no feature flag required. Mutations (create, update, delete, secret regeneration, approve/reject) always invalidate the cache entry immediately.
The cache uses a cache-aside pattern:
- On the first request for a client ID, the DB is queried and the result is stored in cache with a TTL
- Client secrets are stripped before caching (defense-in-depth — secrets are never stored in the cache backend)
- Cache entries are invalidated immediately on any write operation (create, update, delete, secret rotation)
| Backend | Env value | Use case |
|---|---|---|
| Memory | memory (default) |
Single-instance, zero external dependencies |
| Redis | redis |
2–5 pods, shared cache across instances |
| Redis-aside | redis-aside |
5+ pods, client-side caching with stampede protection — requires Redis >= 7.0 |
# Cache backend: memory (default), redis, or redis-aside
CLIENT_CACHE_TYPE=memory
# How long a cached client record is valid (default: 5m); must be > 0
# Mutations always invalidate immediately, so this is only a fallback TTL.
CLIENT_CACHE_TTL=5m
# Client-side TTL for redis-aside mode only (default: 30s); must be > 0
CLIENT_CACHE_CLIENT_TTL=30s
# Client-side cache size per connection in MB for redis-aside mode only (default: 32MB)
# Total memory per pod = cache_size × connections (~10 based on GOMAXPROCS) → default ~320MB
CLIENT_CACHE_SIZE_PER_CONN=32Redis-based backends also require the shared Redis settings:
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0# 2–5 pods: Redis shared cache
CLIENT_CACHE_TYPE=redis
REDIS_ADDR=redis-service:6379
# 5+ pods or DDoS protection: redis-aside with client-side caching
CLIENT_CACHE_TYPE=redis-aside
REDIS_ADDR=redis-service:6379
CLIENT_CACHE_CLIENT_TTL=30s
CLIENT_CACHE_SIZE_PER_CONN=32 # Adjust based on available memory per podNote:
redis-asideuses RESP3 client-side caching for automatic invalidation across all pods and requires Redis >= 7.0. Memory usage per pod isCLIENT_CACHE_SIZE_PER_CONN × ~10 connections(default ~320MB).
/oauth/tokeninfo and every request protected by token-based auth call GetAccessTokenByHash, which hits the database on every validation. The token cache absorbs these lookups, reducing DB load significantly on high-traffic deployments.
The token cache is disabled by default (TOKEN_CACHE_ENABLED=false). Enable it for production deployments with significant token validation traffic.
The cache uses a cache-aside pattern:
- On the first validation of a token hash, the DB is queried and the result is stored in cache with a TTL
- Subsequent validations within the TTL window are served from cache
- Token revocation, rotation, and status changes always explicitly invalidate the cache entry — the TTL is a fallback only
| Backend | Env value | Use case |
|---|---|---|
| Memory | memory (default) |
Single-instance, zero external dependencies |
| Redis | redis |
2–5 pods, shared cache across instances |
| Redis-aside | redis-aside |
5+ pods, client-side caching with RESP3 real-time invalidation — requires Redis >= 7.0 |
# Enable token verification cache (default: false)
TOKEN_CACHE_ENABLED=false
# Cache backend: memory (default), redis, or redis-aside
TOKEN_CACHE_TYPE=memory
# Cache lifetime (default: 10h — matches JWT_EXPIRATION)
# Revocation uses explicit cache invalidation; this TTL is a fallback for rare missed invalidations.
TOKEN_CACHE_TTL=10h
# Client-side TTL for redis-aside mode only (default: 1h)
# RESP3 handles real-time invalidation; this TTL is a safety net for missed notifications.
TOKEN_CACHE_CLIENT_TTL=1h
# Client-side cache size per connection in MB for redis-aside mode only (default: 32MB)
# Total memory per pod = cache_size × connections (~10 based on GOMAXPROCS) → default ~320MB
TOKEN_CACHE_SIZE_PER_CONN=32Redis-based backends also require the shared Redis settings:
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0| Setting | Behaviour |
|---|---|
TOKEN_CACHE_TTL=10h |
Default — matches JWT expiry; cached tokens expire naturally alongside JWT |
TOKEN_CACHE_CLIENT_TTL=1h |
redis-aside client-side TTL; RESP3 invalidation fires immediately on revocation |
# Enable with Redis for multi-pod deployments
TOKEN_CACHE_ENABLED=true
TOKEN_CACHE_TYPE=redis
REDIS_ADDR=redis-service:6379
# Or redis-aside for real-time invalidation across all pods (requires Redis >= 7.0)
TOKEN_CACHE_ENABLED=true
TOKEN_CACHE_TYPE=redis-aside
REDIS_ADDR=redis-service:6379
TOKEN_CACHE_CLIENT_TTL=1h
TOKEN_CACHE_SIZE_PER_CONN=32Note:
redis-asideuses RESP3 client-side caching with real-time invalidation — when a token is revoked, all pods drop their client-side cache entry immediately via RESP3 push notifications. This requires Redis >= 7.0. Memory usage per pod isTOKEN_CACHE_SIZE_PER_CONN × ~10 connections(default ~320MB).
AuthGate includes built-in rate limiting to protect against brute force attacks, credential stuffing, and API abuse. The rate limiting system is production-ready with support for both single-instance and distributed deployments.
- Dual Storage Backends:
- Memory Store: Fast, in-memory storage for single-instance deployments
- Redis Store: Distributed storage for multi-pod Kubernetes/cloud deployments
- Per-Endpoint Configuration: Different rate limits for different endpoints
- IP-Based Tracking: Tracks requests per client IP address
- Hot Configuration: Enable/disable without code changes
- Graceful Degradation: Automatic fallback when Redis is unavailable
Single Instance (Default):
# Default configuration - rate limiting enabled with memory store
./bin/authgate serverMulti-Pod with Redis:
# .env
ENABLE_RATE_LIMIT=true
RATE_LIMIT_STORE=redis
REDIS_ADDR=redis-service:6379
REDIS_PASSWORD=your-passwordDefault Rate Limits:
| Endpoint | Limit | Purpose |
|---|---|---|
POST /login |
5 req/min | Prevent password brute force |
POST /oauth/device/code |
10 req/min | Prevent device code spam |
POST /oauth/token |
20 req/min | Allow polling while preventing abuse |
POST /device/verify |
10 req/min | Prevent user code guessing |
POST /oauth/register |
5 req/min | Prevent registration spam |
POST /oauth/introspect |
20 req/min | Prevent client secret brute force |
All rate limits are configurable via environment variables:
# Enable/disable rate limiting
ENABLE_RATE_LIMIT=true # Default: true
# Storage backend
RATE_LIMIT_STORE=memory # Options: memory, redis
# Redis configuration (only when RATE_LIMIT_STORE=redis)
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
# Per-endpoint limits (requests per minute)
LOGIN_RATE_LIMIT=5
DEVICE_CODE_RATE_LIMIT=10
TOKEN_RATE_LIMIT=20
DEVICE_VERIFY_RATE_LIMIT=10
DYNAMIC_CLIENT_REGISTRATION_RATE_LIMIT=5
INTROSPECT_RATE_LIMIT=20📖 For complete documentation, deployment scenarios, and troubleshooting, see RATE_LIMITING.md
When building a Single-Page Application (SPA) or mobile app that calls AuthGate's OAuth API endpoints from a different origin, you need to enable CORS. By default, CORS is disabled — enabling it only affects /oauth/* API endpoints (token, device code, introspect, revoke, userinfo). HTML page endpoints are never affected.
# .env
CORS_ENABLED=true
CORS_ALLOWED_ORIGINS=http://localhost:3000,https://app.example.com| Variable | Default | Description |
|---|---|---|
CORS_ENABLED |
false |
Enable CORS for API endpoints |
CORS_ALLOWED_ORIGINS |
(none) | Comma-separated list of allowed origins |
CORS_ALLOWED_METHODS |
GET,POST,PUT,DELETE,OPTIONS |
Allowed HTTP methods |
CORS_ALLOWED_HEADERS |
Origin,Content-Type,Authorization |
Allowed request headers |
CORS_MAX_AGE |
12h |
How long browsers cache preflight responses |
- Preflight requests (
OPTIONS) are handled automatically by the CORS middleware and return the appropriateAccess-Control-Allow-*headers. - Credentials (
cookies,Authorizationheader) are allowed —Access-Control-Allow-Credentials: trueis set so token introspection and authenticated requests work from browser JS. - Disallowed origins receive a
403 Forbiddenresponse with no CORS headers. - Same-origin requests (no
Originheader) are unaffected.
- Only list origins you trust — avoid using
*(wildcard) with credentials. - The CORS middleware is applied only to the
/oauth/*route group, not to login pages, admin UI, or static assets. - For maximum security, set
CORS_ALLOWED_ORIGINSto the exact origins of your frontend applications.
Next Steps:
- Architecture Guide - Understand the system design
- Deployment Guide - Deploy to production
- Security Guide - Security best practices