turnstile-caddy/cookie.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

112 lines
2.6 KiB
Go

package turnstile
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
const cookieVersion = "2"
type cookieSigner struct {
secret []byte
name string
ttl time.Duration
secure bool
}
func newCookieSigner(secret, name string, ttl time.Duration) (*cookieSigner, error) {
if secret == "" {
return nil, fmt.Errorf("cookie_secret is required")
}
if name == "" {
name = "__turnstile_pass"
}
if ttl < time.Minute {
ttl = time.Hour
}
return &cookieSigner{
secret: []byte(secret),
name: name,
ttl: ttl,
secure: true,
}, nil
}
func (c *cookieSigner) set(w http.ResponseWriter, r *http.Request) error {
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err != nil {
return err
}
exp := time.Now().Add(c.ttl).Unix()
ip := clientIP(r)
payload := fmt.Sprintf("%s|%d|%s|%s", cookieVersion, exp, hex.EncodeToString(nonce), ip)
mac := hmac.New(sha256.New, c.secret)
mac.Write([]byte(payload))
sig := hex.EncodeToString(mac.Sum(nil))
value := base64.RawURLEncoding.EncodeToString([]byte(payload + "|" + sig))
http.SetCookie(w, &http.Cookie{
Name: c.name,
Value: value,
Path: "/",
HttpOnly: true,
Secure: c.secure || r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https"),
SameSite: http.SameSiteLaxMode,
Expires: time.Unix(exp, 0),
MaxAge: int(c.ttl.Seconds()),
})
return nil
}
func (c *cookieSigner) valid(r *http.Request) bool {
cookie, err := r.Cookie(c.name)
if err != nil || cookie.Value == "" {
return false
}
raw, err := base64.RawURLEncoding.DecodeString(cookie.Value)
if err != nil {
return false
}
parts := strings.Split(string(raw), "|")
if len(parts) != 5 {
return false
}
ver, expStr, nonce, ip, sig := parts[0], parts[1], parts[2], parts[3], parts[4]
if ver != cookieVersion || nonce == "" || sig == "" {
return false
}
exp, err := strconv.ParseInt(expStr, 10, 64)
if err != nil || time.Now().Unix() > exp {
return false
}
if ip != clientIP(r) {
return false
}
payload := fmt.Sprintf("%s|%s|%s|%s", ver, expStr, nonce, ip)
mac := hmac.New(sha256.New, c.secret)
mac.Write([]byte(payload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
func clientIP(r *http.Request) string {
if ip, ok := caddyhttp.GetVar(r.Context(), caddyhttp.ClientIPVarKey).(string); ok && ip != "" {
return ip
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}