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 # REQUIRED.
47 cookie_secret "change-me-to-a-secure-random-string-at-least-32-chars"
48 }
49}
50```
51
52### Authentication Portal (`atproto_portal`)
53
54The `atproto_portal` directive configures the central authentication server. This handles the OAuth flow, serves the login page, and issues session cookies.
55
56```caddyfile
57auth.example.com {
58 atproto_portal {
59 # The public domain of the portal.
60 # REQUIRED.
61 domain auth.example.com
62
63 # The display name shown on the login page.
64 # Default: "Authentication Portal"
65 name "My Services"
66
67 # Custom UI templates (optional)
68 ui {
69 # Path to a custom HTML template for the login page.
70 login_template /path/to/login.html
71 }
72 }
73}
74```
75
76### Authentication Gate (`atproto_gate`)
77
78The `atproto_gate` directive protects your services. It verifies the session cookie and enforces access control.
79
80```caddyfile
81app.example.com {
82 atproto_gate {
83 # List of allowed identities (DIDs or Handles).
84 # REQUIRED.
85 allow @alice.bsky.social
86 allow did:plc:1234abcd...
87
88 # URL of the central Auth Portal.
89 # Requests without a valid session will be redirected here.
90 # REQUIRED (unless in Standalone Mode).
91 portal_url https://auth.example.com
92
93 # Standalone Mode Configuration (Alternative to portal_url)
94 # If set, this gate acts as its own portal.
95 # domain app.example.com
96
97 # Custom UI templates (optional)
98 ui {
99 # Path to a custom HTML template for the "Access Denied" page.
100 forbidden_template /path/to/forbidden.html
101 }
102 }
103
104 reverse_proxy localhost:8080
105}
106```
107
108## Documentation
109
110See the `docs/` folder for detailed architectural constraints and implementation details.