Caddy module to require at-proto authentication and restrict routes to DIDs
1package caddyatprotoauth
2
3import (
4 "net/http"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/caddyserver/caddy/v2"
10 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
11 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
12 _ "github.com/caddyserver/caddy/v2/modules/standard"
13)
14
15func TestCaddyIntegration(t *testing.T) {
16 // Setup Caddyfile
17 // Try: Change the allow list on the gate to include a nonexistent handle.
18 // You should see a warning log that Caddy was unable to resolve it.
19 input := `
20 {
21 admin off
22 atproto {
23 storage_path :memory:
24 }
25 }
26
27 :8080 {
28 route /auth/* {
29 uri strip_prefix /auth
30 atproto_portal {
31 name "Test Portal"
32 }
33 }
34
35 route /protected/* {
36 atproto_gate {
37 allow @tangled.org
38 portal_url http://localhost:8080/auth
39 }
40 respond "Authorized Content"
41 }
42 }
43 `
44
45 // Parse and Load Config
46 adapter := caddyfile.Adapter{
47 ServerType: httpcaddyfile.ServerType{},
48 }
49
50 jsonConfig, warnings, err := adapter.Adapt([]byte(input), nil)
51 if err != nil {
52 t.Fatalf("Failed to adapt config: %v", err)
53 }
54 if len(warnings) > 0 {
55 t.Logf("Warnings: %v", warnings)
56 }
57
58 err = caddy.Load(jsonConfig, true)
59 if err != nil {
60 t.Fatalf("Failed to load caddy: %v", err)
61 }
62 defer caddy.Stop()
63
64 // Wait a moment for server start
65 time.Sleep(100 * time.Millisecond)
66
67 baseURL := "http://localhost:8080"
68 client := &http.Client{
69 CheckRedirect: func(req *http.Request, via []*http.Request) error {
70 return http.ErrUseLastResponse // Don't follow redirects
71 },
72 }
73
74 t.Run("Unauthorized Access Redirects to Portal", func(t *testing.T) {
75 resp, err := client.Get(baseURL + "/protected/resource")
76 if err != nil {
77 t.Fatalf("Request failed: %v", err)
78 }
79 defer resp.Body.Close()
80
81 if resp.StatusCode != http.StatusFound {
82 t.Errorf("Expected status 302, got %d", resp.StatusCode)
83 }
84
85 location := resp.Header.Get("Location")
86 if !strings.Contains(location, "/auth/login") {
87 t.Errorf("Expected redirect to portal login, got %s", location)
88 }
89 })
90
91 t.Run("Portal Serves Login Page", func(t *testing.T) {
92 resp, err := client.Get(baseURL + "/auth/login")
93 if err != nil {
94 t.Fatalf("Request failed: %v", err)
95 }
96 defer resp.Body.Close()
97
98 if resp.StatusCode != http.StatusOK {
99 t.Errorf("Expected status 200, got %d", resp.StatusCode)
100 }
101
102 // Verify content type (HTML)
103 ct := resp.Header.Get("Content-Type")
104 if !strings.Contains(ct, "text/html") {
105 t.Errorf("Expected HTML content, got %s", ct)
106 }
107 })
108
109 t.Run("Portal Serves Metadata", func(t *testing.T) {
110 resp, err := client.Get(baseURL + "/auth/.well-known/oauth-client-metadata.json")
111 if err != nil {
112 t.Fatalf("Request failed: %v", err)
113 }
114 defer resp.Body.Close()
115
116 if resp.StatusCode != http.StatusOK {
117 t.Errorf("Expected status 200, got %d", resp.StatusCode)
118 }
119
120 // Verify content type (JSON)
121 ct := resp.Header.Get("Content-Type")
122 if !strings.Contains(ct, "application/json") {
123 t.Errorf("Expected JSON content, got %s", ct)
124 }
125 })
126}