Files
paliad/internal/auth/user.go
m 3e20806aee fix(security): verify JWT signatures + plug 4 other critical gaps (t-paliad-016)
C-1. Session JWT signature verification (authZ bypass fix)
- Add SUPABASE_JWT_SECRET env var; fail-fast at startup if unset.
- auth.Client.VerifyToken uses github.com/golang-jwt/jwt/v5 to verify
  HS256 signatures, reject alg=none, enforce exp/nbf/iat.
- Middleware stores verified claims in request context; WithUserID
  reads only verified claims (no more raw-cookie sub decoding).
- API requests get 401 on missing/invalid token (was 302 redirect).
- Refresh flow only runs on expiry; other signature failures reject
  outright and clear cookies.

C-2. Dashboard Termine cross-user privacy leak
- dashboard_service.loadUpcomingAppointments now mirrors
  TerminService.canSee: personal Termine (akte_id IS NULL) are
  creator-only; admins do NOT see other users' personal Termine.

C-3. Role gate on Parteien + Termine mutations
- ParteienService.Delete now partner/admin only (matches FristService).
- TerminService.Update / Delete on Akte-linked Termine now require
  partner/admin (or the original creator). Personal Termine stay
  creator-only.

C-4. Email gate → ALLOWED_EMAIL_DOMAINS whitelist
- isHoganLovellsEmail → isAllowedEmailDomain reading the env var
  (default: hoganlovells.com,hlc.com,hlc.de). Case-insensitive,
  whitespace-tolerant.
- login.tsx placeholder: name@hoganlovells.comname@hlc.com
- Error strings + login.hint (de/en) rewritten for HLC branding.

C-5. Docker compose env wiring
- docker-compose.yml gains SUPABASE_JWT_SECRET, CALDAV_ENCRYPTION_KEY,
  and ALLOWED_EMAIL_DOMAINS passthrough; commented-out
  ANTHROPIC_API_KEY line for Phase H readiness.

Tests
- auth_test.go: valid/wrong-secret/expired/alg-none/missing-sub/garbage
  token cases for VerifyToken.
- handlers/auth_test.go: default + env-override cases for the email
  whitelist.
- go build ./..., go vet ./..., go test ./... all clean.
2026-04-18 02:23:50 +02:00

61 lines
1.8 KiB
Go

package auth
import (
"context"
"net/http"
"github.com/google/uuid"
)
type contextKey string
const (
userIDContextKey contextKey = "paliad.userID"
claimsContextKey contextKey = "paliad.claims"
)
// UserIDFromContext returns the authenticated user's UUID, populated by
// WithUserID middleware (which runs after the session middleware).
// Returns (uuid.Nil, false) if no user is in context.
func UserIDFromContext(ctx context.Context) (uuid.UUID, bool) {
v, ok := ctx.Value(userIDContextKey).(uuid.UUID)
if !ok {
return uuid.Nil, false
}
return v, true
}
// withVerifiedClaims stores signature-verified JWT claims in the request
// context. Called only from Client.Middleware after VerifyToken succeeds.
func withVerifiedClaims(ctx context.Context, claims *VerifiedClaims) context.Context {
return context.WithValue(ctx, claimsContextKey, claims)
}
// verifiedClaimsFromContext returns the signature-verified JWT claims
// attached by Client.Middleware.
func verifiedClaimsFromContext(ctx context.Context) (*VerifiedClaims, bool) {
v, ok := ctx.Value(claimsContextKey).(*VerifiedClaims)
return v, ok
}
// WithUserID reads the `sub` claim from verified JWT claims attached by
// Client.Middleware and injects the user's UUID into the request context.
// Must run after Client.Middleware — the claims are only set there, after
// signature verification succeeds.
func (c *Client) WithUserID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := verifiedClaimsFromContext(r.Context())
if !ok {
next.ServeHTTP(w, r)
return
}
uid, err := uuid.Parse(claims.Sub)
if err != nil {
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), userIDContextKey, uid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}