caddy-atproto-auth#
A native Caddy module that provides Identity-Aware Proxy (IAP) capabilities using the atproto (Bluesky) OAuth 2.1 ecosystem.
Turn any atproto identity into a "Web Passport" for your self-hosted services. The module acts as an OAuth Confidential Client, managing the DPoP cryptographic handshake, session persistence, and DID-based authorization without requiring external authentication sidecars like Authelia.
Features#
- Zero-Dependency: Plugs directly into Caddy, no external databases (uses embedded SQLite).
- Stateless Verification: Uses signed, domain-scoped cookies for lightning-fast request verification at the edge without database lookups.
- Transparent Session Refresh: Automatically uses OAuth Refresh Tokens to extend sessions in the background, minimizing forced re-logins.
- Two Deployment Modes:
- Standalone: Add to any individual app's Caddyfile route directly.
- Centralized Hub: Act as an Identity Provider (
auth.example.com) granting SSO access to many subdomains (app.example.com).
- Full Customization: Fully override the login and forbidden pages with your own HTML templates.
Usage#
Build a custom Caddy binary with xcaddy:
xcaddy build \
--with tangled.org/vvill.dev/caddy-atproto-auth
Configuration#
Important Note on Local Development#
The AT Protocol OAuth flow requires the Authentication Server (PDS) to fetch client metadata from your application. If you are running Caddy on localhost, production PDS instances (like bsky.social) cannot reach your local server, resulting in an invalid_client error.
To test locally with a real PDS, you must expose your local server to the internet using a tunnel (e.g., ngrok, cloudflared, or tailscale funnel) and configure the domain directive with the public tunnel URL.
Global Options#
The atproto global block configures the shared storage and security settings.
{
atproto {
# Path to the SQLite database.
# Default: "atproto.db"
storage_path /var/lib/caddy/atproto.db
# A random 32+ character string used to sign session cookies.
# OPTIONAL. If omitted, a secure random key is generated and stored in the DB.
# Required for "Auth Hub" setups where Gate and Portal are on different machines.
# cookie_secret "change-me-to-a-secure-random-string-at-least-32-chars"
}
}
Authentication Portal (atproto_portal)#
The atproto_portal directive configures the central authentication server. This handles the OAuth flow, serves the login page, and issues session cookies.
auth.example.com {
atproto_portal {
# The public domain of the portal.
# REQUIRED.
domain auth.example.com
# The display name shown on the login page.
# Default: "Authentication Portal"
name "My Services"
# Custom UI templates (optional)
ui {
# Path to a custom HTML template for the login page.
login_template /path/to/login.html
}
# Path Prefix Configuration (Optional)
# Prepends a prefix to /login and /logout paths.
# Useful to avoid conflicts with downstream apps.
# e.g., path_prefix /auth -> /auth/login, /auth/logout
# path_prefix /auth
}
}
Authentication Gate (atproto_gate)#
The atproto_gate directive protects your services. It verifies the session cookie and enforces access control.
app.example.com {
atproto_gate {
# List of allowed identities (DIDs or Handles).
# OPTIONAL. If omitted, any authenticated user will be allowed.
allow @alice.bsky.social
allow did:plc:1234abcd...
# URL of the central Auth Portal.
# Requests without a valid session will be redirected here.
# REQUIRED.
# If the Portal uses a path_prefix (e.g. /auth), append it here (e.g. https://auth.example.com/auth)
portal_url https://auth.example.com
# Client ID for Transparent Refresh (Optional)
# If provided, enables background token refreshing using the shared DB.
# Should match the Portal's Client ID (usually https://domain/.well-known/oauth-client-metadata.json).
# client_id https://app.example.com/.well-known/oauth-client-metadata.json
# Custom UI templates (optional)
ui {
# Path to a custom HTML template for the "Access Denied" page.
forbidden_template /path/to/forbidden.html
}
}
reverse_proxy localhost:8080
}
Production Best Practices#
Enable Compression#
To ensure the Login and Forbidden HTML pages (which include inline CSS and SVGs) are delivered as quickly as possible, enable Gzip and Zstd compression in your Caddyfile. This reduces the transfer size significantly.
app.example.com {
# Enable compression for all responses (including auth pages)
encode zstd gzip
route {
atproto_portal { ... }
atproto_gate { ... }
reverse_proxy localhost:8080
}
}
Localhost Development#
The AT Protocol OAuth flow requires the Authentication Server (PDS) to fetch client metadata from your application. If you are running Caddy on localhost:
- Issue: Production PDS instances (like
bsky.social) cannot reachhttp://localhost. - Symptom: You will see an
invalid_clienterror during login. - Fix: Expose your local server to the internet using a tunnel (e.g.,
ngrok,cloudflared, ortailscale funnel) and set yourdomainconfig to that public URL.
Documentation#
See the docs/ folder for detailed architectural constraints and implementation details.