turnstile-caddy/cookie_test.go
Lukas Schaefer b8ab6783c8
All checks were successful
/ resolve-go (push) Successful in 4s
/ test (push) Successful in 6s
/ cleanup (push) Successful in 2s
Implement Authorization header support
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-08-31 17:46:56 -04:00

135 lines
3.9 KiB
Go

package turnstile
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestCookieRoundTrip(t *testing.T) {
signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
if err := signer.set(rec, req); err != nil {
t.Fatal(err)
}
cookies := rec.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("expected 1 cookie, got %d", len(cookies))
}
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil)
req2.AddCookie(cookies[0])
if !signer.valid(req2) {
t.Fatal("expected cookie to validate")
}
}
func TestCookieRejectsTamper(t *testing.T) {
signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
if err := signer.set(rec, req); err != nil {
t.Fatal(err)
}
c := rec.Result().Cookies()[0]
c.Value = c.Value + "x"
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
req2.AddCookie(c)
if signer.valid(req2) {
t.Fatal("tampered cookie must not validate")
}
}
func withTrustedClientIP(r *http.Request, ip string) *http.Request {
vars := map[string]any{caddyhttp.ClientIPVarKey: ip}
ctx := context.WithValue(r.Context(), caddyhttp.VarsCtxKey, vars)
return r.WithContext(ctx)
}
func TestCookieUsesTrustedClientIP(t *testing.T) {
signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
req := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "192.0.2.1")
req.RemoteAddr = "203.0.113.1:443"
if err := signer.set(rec, req); err != nil {
t.Fatal(err)
}
req2 := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "192.0.2.1")
req2.RemoteAddr = "203.0.113.2:443"
req2.AddCookie(rec.Result().Cookies()[0])
if !signer.valid(req2) {
t.Fatal("cookie must validate when trusted client IP matches across proxy hops")
}
req3 := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "198.51.100.1")
req3.RemoteAddr = "203.0.113.1:443"
req3.AddCookie(rec.Result().Cookies()[0])
if signer.valid(req3) {
t.Fatal("cookie from different trusted client IP must not validate")
}
}
func TestCookieRejectsIPMismatch(t *testing.T) {
signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
req.RemoteAddr = "192.0.2.1:1234"
if err := signer.set(rec, req); err != nil {
t.Fatal(err)
}
req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
req2.RemoteAddr = "198.51.100.1:5678"
req2.AddCookie(rec.Result().Cookies()[0])
if signer.valid(req2) {
t.Fatal("cookie from different IP must not validate")
}
}
func TestCookieExpired(t *testing.T) {
signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour)
if err != nil {
t.Fatal(err)
}
exp := time.Now().Add(-2 * time.Second).Unix()
payload := fmt.Sprintf("2|%d|deadbeef|192.0.2.1", exp)
mac := hmac.New(sha256.New, signer.secret)
mac.Write([]byte(payload))
sig := hex.EncodeToString(mac.Sum(nil))
value := base64.RawURLEncoding.EncodeToString([]byte(payload + "|" + sig))
req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil)
req.AddCookie(&http.Cookie{Name: signer.name, Value: value})
if signer.valid(req) {
t.Fatal("expired cookie must not validate")
}
}