Caddy module to require at-proto authentication and restrict routes to DIDs
3

Configure Feed

Select the types of activity you want to include in your feed.

1# caddy-atproto-auth 2 3A native Caddy module that provides Identity-Aware Proxy (IAP) capabilities using the **atproto** (Bluesky) OAuth 2.1 ecosystem. 4 5Turn 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. 6 7## Features 8 9- **Zero-Dependency**: Plugs directly into Caddy, no external databases (uses embedded SQLite). 10- **Stateless Verification**: Uses signed, domain-scoped cookies for lightning-fast request verification at the edge without database lookups. 11- **Transparent Session Refresh**: Automatically uses OAuth Refresh Tokens to extend sessions in the background, minimizing forced re-logins. 12- **Two Deployment Modes**: 13 - *Standalone*: Add to any individual app's Caddyfile route directly. 14 - *Centralized Hub*: Act as an Identity Provider (`auth.example.com`) granting SSO access to many subdomains (`app.example.com`). 15- **Full Customization**: Fully override the login and forbidden pages with your own HTML templates. 16 17## Usage 18 19Build a custom Caddy binary with `xcaddy`: 20 21```bash 22xcaddy build \ 23 --with tangled.org/vvill.dev/caddy-atproto-auth 24``` 25 26## Configuration 27 28### Important Note on Local Development 29 30The 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. 31 32To 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. 33 34### Global Options 35 36The `atproto` global block configures the shared storage and security settings. 37 38```caddyfile 39{ 40 atproto { 41 # Path to the SQLite database. 42 # Default: "atproto.db" 43 storage_path /var/lib/caddy/atproto.db 44 45 # A random 32+ character string used to sign session cookies. 46 # OPTIONAL. If omitted, a secure random key is generated and stored in the DB. 47 # Required for "Auth Hub" setups where Gate and Portal are on different machines. 48 # cookie_secret "change-me-to-a-secure-random-string-at-least-32-chars" 49 } 50} 51``` 52 53### Authentication Portal (`atproto_portal`) 54 55The `atproto_portal` directive configures the central authentication server. This handles the OAuth flow, serves the login page, and issues session cookies. 56 57```caddyfile 58auth.example.com { 59 atproto_portal { 60 # The public domain of the portal. 61 # REQUIRED. 62 domain auth.example.com 63 64 # The display name shown on the login page. 65 # Default: "Authentication Portal" 66 name "My Services" 67 68 # Custom UI templates (optional) 69 ui { 70 # Path to a custom HTML template for the login page. 71 login_template /path/to/login.html 72 } 73 74 # Path Prefix Configuration (Optional) 75 # Prepends a prefix to /login and /logout paths. 76 # Useful to avoid conflicts with downstream apps. 77 # e.g., path_prefix /auth -> /auth/login, /auth/logout 78 # path_prefix /auth 79 } 80} 81``` 82 83### Authentication Gate (`atproto_gate`) 84 85The `atproto_gate` directive protects your services. It verifies the session cookie and enforces access control. 86 87```caddyfile 88app.example.com { 89 atproto_gate { 90 # List of allowed identities (DIDs or Handles). 91 # OPTIONAL. If omitted, any authenticated user will be allowed. 92 allow @alice.bsky.social 93 allow did:plc:1234abcd... 94 95 # URL of the central Auth Portal. 96 # Requests without a valid session will be redirected here. 97 # REQUIRED. 98 # If the Portal uses a path_prefix (e.g. /auth), append it here (e.g. https://auth.example.com/auth) 99 portal_url https://auth.example.com 100 101 # Client ID for Transparent Refresh (Optional) 102 # If provided, enables background token refreshing using the shared DB. 103 # Should match the Portal's Client ID (usually https://domain/.well-known/oauth-client-metadata.json). 104 # client_id https://app.example.com/.well-known/oauth-client-metadata.json 105 106 # Custom UI templates (optional) 107 ui { 108 # Path to a custom HTML template for the "Access Denied" page. 109 forbidden_template /path/to/forbidden.html 110 } 111 } 112 113 reverse_proxy localhost:8080 114} 115``` 116 117## Production Best Practices 118 119### Enable Compression 120 121To 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. 122 123```caddyfile 124app.example.com { 125 # Enable compression for all responses (including auth pages) 126 encode zstd gzip 127 128 route { 129 atproto_portal { ... } 130 atproto_gate { ... } 131 reverse_proxy localhost:8080 132 } 133} 134``` 135 136### Localhost Development 137 138The AT Protocol OAuth flow requires the Authentication Server (PDS) to fetch client metadata from your application. If you are running Caddy on `localhost`: 139 1401. **Issue**: Production PDS instances (like `bsky.social`) **cannot reach** `http://localhost`. 1412. **Symptom**: You will see an `invalid_client` error during login. 1423. **Fix**: Expose your local server to the internet using a tunnel (e.g., `ngrok`, `cloudflared`, or `tailscale funnel`) and set your `domain` config to that public URL. 143 144## Documentation 145 146See the `docs/` folder for detailed architectural constraints and implementation details.