Files
vikingowl 0997d4befa feat(auth): D1 non-breaking security foundations
- CORS: rewrite middleware with Vary: Origin, regex origin patterns,
  startup validation, and prod boot-fail on empty allowlist; shared
  CORSConfig exported for CSRF reuse
- CSRF: new Origin/Referer check middleware sharing CORS allowlist;
  Bearer-token clients exempt; mounts globally after CORS
- Argon2id: new password package with PHC format, bcrypt dispatch, and
  NeedsRehash; lazy upgrade on login in auth service
- Rate limiting: add RateLimitByKey with custom key function; apply
  per-route limits to /auth/login, /refresh, /2fa/verify,
  /auth/magic-link, and /auth/password
- apierror: add CSRFMismatch and RefreshReuse error constructors
- Migrations: 000027 (session model schema columns for D2/D3),
  000028 (TOTP secret_v2 column + totp_backup_codes table)
- cmd/totp-encrypt: one-shot job to encrypt existing TOTP secrets
2026-04-26 11:54:37 +02:00

51 lines
1.2 KiB
Go

package middleware
import (
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"marktvogt.de/backend/internal/pkg/apierror"
)
// CSRF returns middleware that validates the Origin (or Referer) header for
// state-changing cookie-authed requests. Bearer-token requests (mobile/API)
// are exempt — they're not CSRF-vulnerable.
func CSRF(cfg CORSConfig) gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
// Safe methods and preflight are not CSRF-vulnerable.
if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions {
c.Next()
return
}
// Bearer-token clients (mobile, third-party API) are exempt.
if strings.HasPrefix(c.GetHeader("Authorization"), "Bearer ") {
c.Next()
return
}
origin := c.GetHeader("Origin")
if origin == "" {
// Fall back to Referer — extract scheme+host as origin.
if ref := c.GetHeader("Referer"); ref != "" {
if u, err := url.Parse(ref); err == nil && u.Host != "" {
origin = u.Scheme + "://" + u.Host
}
}
}
if !cfg.IsAllowedOrigin(origin) {
apiErr := apierror.CSRFMismatch()
c.AbortWithStatusJSON(apiErr.Status, apierror.NewResponse(apiErr))
return
}
c.Next()
}
}