Caddy module to require at-proto authentication and restrict routes to DIDs
1package caddyatprotoauth
2
3import (
4 "fmt"
5 "net"
6 "path/filepath"
7 "strconv"
8 "time"
9
10 "github.com/caddyserver/caddy/v2"
11 "github.com/caddyserver/caddy/v2/caddyconfig"
12 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
13 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
14
15 "tangled.org/vvill.dev/caddy-atproto-auth/internal/db"
16)
17
18func init() {
19 caddy.RegisterModule(&App{})
20 httpcaddyfile.RegisterGlobalOption("atproto", parseGlobalAtproto)
21}
22
23// App configures the global atproto integration.
24type App struct {
25 StoragePath string `json:"storage_path,omitempty"`
26 CookieSecret string `json:"cookie_secret,omitempty"`
27 SessionDurationStr string `json:"session_duration,omitempty"`
28 OAuthManagerCacheSize int `json:"oauth_manager_cache_size,omitempty"`
29 AllowPrivateCIDRs []net.IPNet `json:"allow_private_cidrs,omitempty"`
30
31 // Internal state
32 Store *db.Store `json:"-"`
33 SessionDuration time.Duration `json:"-"`
34}
35
36// CaddyModule returns the Caddy module information.
37func (*App) CaddyModule() caddy.ModuleInfo {
38 return caddy.ModuleInfo{
39 ID: "atproto",
40 New: func() caddy.Module { return new(App) },
41 }
42}
43
44// Provision sets up the global app state.
45func (a *App) Provision(ctx caddy.Context) error {
46 // Defaults
47 if a.StoragePath == "" {
48 a.StoragePath = filepath.Join(caddy.AppDataDir(), "atproto.db")
49 }
50
51 // Initialize DB
52 store, err := db.NewStore(a.StoragePath)
53 if err != nil {
54 return fmt.Errorf("failed to initialize atproto storage: %w", err)
55 }
56 a.Store = store
57
58 // Resolve Cookie Secret
59 if a.CookieSecret == "" {
60 // Try to load/generate from DB
61 secret, err := a.Store.GetCookieSecret(ctx)
62 if err != nil {
63 return fmt.Errorf("failed to resolve cookie secret: %w", err)
64 }
65 a.CookieSecret = secret
66 }
67
68 // Parse session duration
69 a.SessionDuration = 24 * 7 * time.Hour
70 if a.SessionDurationStr != "" {
71 d, err := caddy.ParseDuration(a.SessionDurationStr)
72 if err != nil {
73 return fmt.Errorf("invalid session_duration: %w", err)
74 }
75 a.SessionDuration = time.Duration(d)
76 }
77
78 if a.OAuthManagerCacheSize <= 0 {
79 a.OAuthManagerCacheSize = 100 // Default max oauth managers
80 }
81
82 return nil
83}
84
85// Start starts the application.
86func (a *App) Start() error {
87 return nil
88}
89
90// Stop stops the application.
91func (a *App) Stop() error {
92 if a.Store != nil {
93 return a.Store.Close()
94 }
95 return nil
96}
97
98// Interface guards
99var (
100 _ caddy.App = (*App)(nil)
101 _ caddy.Provisioner = (*App)(nil)
102)
103
104// parseGlobalAtproto parses the global 'atproto' Caddyfile option.
105// Format:
106//
107// atproto {
108// storage_path /path/to/db
109// cookie_secret <secret>
110// }
111func parseGlobalAtproto(d *caddyfile.Dispenser, _ any) (any, error) {
112 app := &App{}
113 for d.Next() {
114 for d.NextBlock(0) {
115 switch d.Val() {
116 case "storage_path":
117 if !d.NextArg() {
118 return nil, d.ArgErr()
119 }
120 app.StoragePath = d.Val()
121 case "cookie_secret":
122 if !d.NextArg() {
123 return nil, d.ArgErr()
124 }
125 app.CookieSecret = d.Val()
126 case "session_duration":
127 if !d.NextArg() {
128 return nil, d.ArgErr()
129 }
130 app.SessionDurationStr = d.Val()
131 case "oauth_manager_cache_size":
132 if !d.NextArg() {
133 return nil, d.ArgErr()
134 }
135 val, err := strconv.Atoi(d.Val())
136 if err != nil {
137 return nil, d.Errf("invalid oauth_manager_cache_size: %v", err)
138 }
139 app.OAuthManagerCacheSize = val
140 case "allow_private_cidrs":
141 for _, c := range d.RemainingArgs() {
142 _, ipnet, err := net.ParseCIDR(c)
143 if err != nil || ipnet == nil {
144 return nil, d.ArgErr()
145 }
146 app.AllowPrivateCIDRs = append(app.AllowPrivateCIDRs, *ipnet)
147 }
148 default:
149 return nil, d.Errf("unrecognized subdirective '%s'", d.Val())
150 }
151 }
152 }
153 return httpcaddyfile.App{
154 Name: "atproto",
155 Value: caddyconfig.JSON(app, nil),
156 }, nil
157}