···25252626## Configuration
27272828+### Important Note on Local Development
2929+3030+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.
3131+3232+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.
3333+2834### Global Options
29353036The `atproto` global block configures the shared storage and security settings.
+3-3
e2e/Caddyfile
···11{
22 admin off
33 atproto {
44- storage_path ./e2e/e2e.db
44+ storage_path ./e2e.db
55 cookie_secret "testing-secret-must-be-at-least-32-bytes-long"
66 }
77}
···1313 atproto_gate {
1414 # Standalone mode enabled by setting 'domain'
1515 domain localhost:8081
1616- allow @test.bsky.social
1616+ allow @vvill.dev
1717 }
18181919 # Protected content
···4040 atproto_gate {
4141 # Auth Hub mode (no 'domain' set)
4242 portal_url http://localhost:8082
4343- allow @test.bsky.social
4343+ allow @vvill.dev
4444 }
45454646 respond "Welcome to Service App! You authenticated via the Hub."
+17-1
gate.go
···265265 if r.URL.Path == "/login" {
266266 if r.Method == "POST" {
267267 handle := r.FormValue("handle")
268268+ // Strip leading @ if present
269269+ if len(handle) > 0 && handle[0] == '@' {
270270+ handle = handle[1:]
271271+ }
272272+268273 redirectURI, err := g.oauth.StartAuthFlow(r.Context(), handle)
269274 if err != nil {
270270- return caddyhttp.Error(http.StatusBadRequest, err)
275275+ // Render error on login page instead of raw JSON
276276+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
277277+ // We return 400 Bad Request
278278+ w.WriteHeader(http.StatusBadRequest)
279279+ if renderErr := g.renderer.RenderLogin(w, ui.LoginData{
280280+ AppName: g.Domain,
281281+ Redirect: "/",
282282+ Error: fmt.Sprintf("Authentication failed: %v", err),
283283+ }); renderErr != nil {
284284+ g.logger.Error("failed to render login error", zap.Error(renderErr))
285285+ }
286286+ return nil
271287 }
272288 http.Redirect(w, r, redirectURI, http.StatusFound)
273289 return nil