···4343 route {
4444 atproto_portal {
4545 cookie_domain example.com
4646+ allowed_redirect_domains app1.example.com
4647 }
4748 }
4849 }
···7778 # For use where a Gate and its Portal are on different machines.
7879 # Default: 32 random bytes
7980 cookie_secret "change-me-to-a-secure-random-string-at-least-32-chars"
8181+8282+ # OAuth Session duration - how long before user needs to log in again
8383+ # Default 1 week (7d)
8484+ session_duration 1d
8585+8686+ # Number of OAuth managers in cache (to avoid dos, increase with scale)
8787+ # Default: 100
8888+ oauth_manager_cache_size 1000
8089 }
8190}
8291```
···113122 # Prepends a prefix to /login and /logout paths.
114123 # Useful to avoid conflicts with downstream apps.
115124 path_prefix atproto_auth
125125+126126+ # Allowed domains for the redirect_to parameter after login/logout.
127127+ # By default, only the portal's domain is allowed.
128128+ allowed_redirect_domains app.example.com *.app.example.com
129129+130130+ # Where to redirect users after they log out.
131131+ # Default: The login page
132132+ logout_redirect_url https://example.com/goodbye
116133117134 # Change cookie name, also to avoid conficts
118135 # Default: atproto_session
···137154 allow *
138155139156 # If the Portal uses a path_prefix, include it here
157157+ # Default: /
140158 portal_url https://auth.example.com/atproto_auth
141159142160 # Match Portal's values if set. Used for token refresh.
143161 cookie_domain example.com
144162 cookie_name caddy_atproto_session
163163+164164+ # The plugin resolves all handles to DIDs at startup. For security
165165+ # against account hijacking, dynamic handle resolution is disabled by default.
166166+ # Enable this if you want to follow specified handles to new DIDs.
167167+ # Default: false
168168+ resolve_handles_on_request false
145169 }
146170}
147171```
+21-5
gate.go
···88 "strings"
99 "sync"
10101111+ "strconv"
1212+1113 "github.com/caddyserver/caddy/v2"
1214 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
1315 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
···5557func (g *Gate) Provision(ctx caddy.Context) error {
5658 g.logger = ctx.Logger()
57595858- // 1. Get Global App
6060+ // Get Global App
5961 app, err := ctx.App("atproto")
6062 if err != nil {
6163 return fmt.Errorf("getting atproto app: %w", err)
6264 }
6365 g.app = app.(*App)
64666565- // 2. Initialize Session Manager (using global secret)
6767+ // Initialize Session Manager (using global secret)
6668 g.sessions = session.NewManager(g.app.CookieSecret, g.CookieName, g.CookieDomain)
67696870 g.oauthManagers = make(map[string]*oauth.Manager)
···7476 g.PortalURL = g.PortalURL[:len(g.PortalURL)-1]
7577 }
76787777- // 5. Initialize OAuth Manager for transparent refresh
7979+ // Initialize OAuth Manager for transparent refresh
7880 // We derive the ClientID from the PortalURL if it's absolute,
7981 // or from the Host header at request time if it's relative.
8082 // For now, if it's absolute, we can init the OAuth manager immediately.
···9092 }
9193 }
92949393- // 6. Pre-resolve allowed handles to DIDs
9595+ // Pre-resolve allowed handles to DIDs
9496 g.resolver = resolver.New()
95979698 g.resolvedDIDs = make([]string, 0, len(g.Allow))
···148150 }
149151 g.PortalURL = d.Val()
150152 case "resolve_handles_on_request":
151151- g.ResolveHandlesOnRequest = true
153153+ if d.NextArg() {
154154+ val, err := strconv.ParseBool(d.Val())
155155+ if err != nil {
156156+ return d.Errf("invalid boolean value '%s'", d.Val())
157157+ }
158158+ g.ResolveHandlesOnRequest = val
159159+ } else {
160160+ g.ResolveHandlesOnRequest = true
161161+ }
152162 default:
153163 return d.Errf("unrecognized subdirective '%s'", d.Val())
154164 }
···300310 currentURL := fmt.Sprintf("%s://%s%s", scheme, host, r.URL.RequestURI())
301311302312 portalURL := g.PortalURL
313313+ if portalURL == "/" {
314314+ portalURL = ""
315315+ }
303316 portalForbidden := fmt.Sprintf("%s/forbidden?redirect_to=%s", portalURL, url.QueryEscape(currentURL))
304317 http.Redirect(w, r, portalForbidden, http.StatusFound)
305318 return nil
···319332 currentURL := fmt.Sprintf("%s://%s%s", scheme, host, r.URL.RequestURI())
320333321334 portalURL := g.PortalURL
335335+ if portalURL == "/" {
336336+ portalURL = ""
337337+ }
322338 portalLogin := fmt.Sprintf("%s/login?redirect_to=%s", portalURL, url.QueryEscape(currentURL))
323339 http.Redirect(w, r, portalLogin, http.StatusFound)
324340 return nil
···1313)
14141515func TestCaddyIntegration(t *testing.T) {
1616- // 1. Setup Caddyfile
1616+ // Setup Caddyfile
1717+ // Try: Change the allow list on the gate to include a nonexistent handle.
1818+ // You should see a warning log that Caddy was unable to resolve it.
1719 input := `
1820 {
1921 admin off
···4042 }
4143 `
42444343- // 2. Parse and Load Config
4545+ // Parse and Load Config
4446 adapter := caddyfile.Adapter{
4547 ServerType: httpcaddyfile.ServerType{},
4648 }
+1-4
internal/db/db.go
···137137 err := s.db.QueryRowContext(ctx, "SELECT data FROM auth_sessions WHERE did = ? AND session_id = ?", did.String(), sessionID).Scan(&dataStr)
138138 if err != nil {
139139 if err == sql.ErrNoRows {
140140- // Some oauth methods in indigo might expect a specific error if not found, let's return it as nil/not found or check indigo docs.
141141- // Indigo's memstore returns (nil, nil) or custom error. We'll return nil for the session and potentially an error if we need to.
142142- // Currently indigo's oauth store interface doesn't strictly dictate `ErrNotFound`, but usually `nil, nil` or `nil, Err` is handled. Let's return nil, nil.
143140 return nil, nil
144141 }
145142 return nil, fmt.Errorf("failed to query session: %w", err)
···225222 return fmt.Errorf("failed to serialize auth request data: %w", err)
226223 }
227224228228- // Creating is fine. It shouldn't exist, but we can do an INSERT OR REPLACE just in case.
225225+ // It shouldn't exist, but we can do an INSERT OR REPLACE just in case.
229226 _, err = s.db.ExecContext(ctx, `
230227 INSERT OR REPLACE INTO auth_requests (state, data, created_at)
231228 VALUES (?, ?, CURRENT_TIMESTAMP)
-3
internal/session/session.go
···31313232// NewManager creates a new session manager with the given secret.
3333func NewManager(secret string, cookieName string, cookieDomain string) *Manager {
3434- if len(secret) < 32 {
3535- // Warn or error if secret is too short? For now, we assume user configures it properly.
3636- }
3734 if cookieName == "" {
3835 cookieName = "atproto_session"
3936 }