Caddy module to require at-proto authentication and restrict routes to DIDs
1package caddyatprotoauth
2
3import (
4 "fmt"
5
6 "github.com/caddyserver/caddy/v2"
7 "github.com/caddyserver/caddy/v2/caddyconfig"
8 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
9 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
10
11 "github.com/vvill/caddy-atproto-auth/internal/db"
12 "github.com/vvill/caddy-atproto-auth/internal/oauth"
13)
14
15func init() {
16 caddy.RegisterModule(App{})
17 httpcaddyfile.RegisterGlobalOption("atproto", parseGlobalAtproto)
18}
19
20// App configures the global atproto integration.
21type App struct {
22 StoragePath string `json:"storage_path,omitempty"`
23 CookieSecret string `json:"cookie_secret,omitempty"`
24
25 // Internal state
26 Store *db.Store `json:"-"`
27 OAuthManager *oauth.Manager `json:"-"`
28}
29
30// CaddyModule returns the Caddy module information.
31func (App) CaddyModule() caddy.ModuleInfo {
32 return caddy.ModuleInfo{
33 ID: "atproto",
34 New: func() caddy.Module { return new(App) },
35 }
36}
37
38// Provision sets up the global app state.
39func (a *App) Provision(ctx caddy.Context) error {
40 // Defaults
41 if a.StoragePath == "" {
42 a.StoragePath = "atproto.db" // Relative to workdir or specific path
43 }
44 // Resolve relative path against Caddy's storage or workdir if needed.
45 // For simplicity, we assume absolute or relative to CWD.
46
47 // Initialize DB
48 store, err := db.NewStore(a.StoragePath)
49 if err != nil {
50 return fmt.Errorf("failed to initialize atproto storage: %w", err)
51 }
52 a.Store = store
53
54 // Initialize OAuth Manager (requires client ID and callback URL to be fully configured,
55 // but those might be per-portal or global. The spec says "acts as an OAuth Client".
56 // If the plugin acts as a *single* client for many subdomains, we need global config for client ID.
57 // But spec says: "Path A: The Self-Contained Route" and "Path B: The Auth Hub".
58 // This implies potentially different client IDs for different sites OR one central hub.
59 // For now, let's defer OAuthManager creation to the Portal or Gate if it's per-route,
60 // OR we need to add ClientID/CallbackURL to the global config if it's shared.
61 //
62 // Looking at the spec:
63 // "The module acts as an OAuth Client"
64 // "Global Configuration: storage_path, cookie_secret"
65 //
66 // It seems the App module holds the *Storage* and *Keys*.
67 // The *Portal* (or Gate) defines the "Client" identity (metadata, callback).
68 // However, `oauth.NewManager` takes a `db.Store`. So the App owns the Store.
69 // The Portal will instantiate the Manager using the App's Store.
70
71 return nil
72}
73
74// Start starts the application.
75func (a *App) Start() error {
76 return nil
77}
78
79// Stop stops the application.
80func (a *App) Stop() error {
81 if a.Store != nil {
82 return a.Store.Close()
83 }
84 return nil
85}
86
87// Interface guards
88var (
89 _ caddy.App = (*App)(nil)
90 _ caddy.Provisioner = (*App)(nil)
91)
92
93// parseGlobalAtproto parses the global 'atproto' Caddyfile option.
94// Format:
95//
96// atproto {
97// storage_path /path/to/db
98// cookie_secret <secret>
99// }
100func parseGlobalAtproto(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
101 app := &App{}
102 for d.Next() {
103 for d.NextBlock(0) {
104 switch d.Val() {
105 case "storage_path":
106 if !d.NextArg() {
107 return nil, d.ArgErr()
108 }
109 app.StoragePath = d.Val()
110 case "cookie_secret":
111 if !d.NextArg() {
112 return nil, d.ArgErr()
113 }
114 app.CookieSecret = d.Val()
115 default:
116 return nil, d.Errf("unrecognized subdirective '%s'", d.Val())
117 }
118 }
119 }
120 return httpcaddyfile.App{
121 Name: "atproto",
122 Value: caddyconfig.JSON(app, nil),
123 }, nil
124}