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
}
}
}
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).
# REQUIRED.
allow @alice.bsky.social
allow did:plc:1234abcd...
# URL of the central Auth Portal.
# Requests without a valid session will be redirected here.
# REQUIRED (unless in Standalone Mode).
portal_url https://auth.example.com
# Standalone Mode Configuration (Alternative to portal_url)
# If set, this gate acts as its own portal.
# domain app.example.com
# 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
}
Documentation#
See the docs/ folder for detailed architectural constraints and implementation details.