Caddy module to require at-proto authentication and restrict routes to DIDs
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.