// Package auth handles Supabase JWT validation and HTTP middleware. package auth import ( "context" "errors" "fmt" "net/http" "strings" "github.com/golang-jwt/jwt/v5" ) type contextKey int const ( driverIDKey contextKey = iota emailKey ) // Claims represents the parsed Supabase JWT claims we care about. type Claims struct { DriverID string Email string } // ValidateJWT parses and validates a Supabase HMAC-SHA256 JWT using the secret. func ValidateJWT(tokenStr string, secret string) (Claims, error) { token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return []byte(secret), nil }) if err != nil { return Claims{}, err } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { sub, _ := claims["sub"].(string) email, _ := claims["email"].(string) if sub == "" { return Claims{}, errors.New("missing sub claim") } return Claims{ DriverID: sub, Email: email, }, nil } return Claims{}, errors.New("invalid token") } // Middleware returns an HTTP middleware that extracts and validates the Bearer token. // When devMode is enabled, authentication is bypassed entirely. func Middleware(secret string, devMode bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if devMode { next.ServeHTTP(w, r) return } authHeader := r.Header.Get("Authorization") if authHeader == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized","message":"missing authorization header"}`)) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized","message":"invalid authorization format"}`)) return } claims, err := ValidateJWT(parts[1], secret) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(fmt.Sprintf(`{"error":"unauthorized","message":%q}`, err.Error()))) return } ctx := context.WithValue(r.Context(), driverIDKey, claims.DriverID) ctx = context.WithValue(ctx, emailKey, claims.Email) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // DriverIDFromContext retrieves the driver ID from the context. func DriverIDFromContext(ctx context.Context) (string, bool) { id, ok := ctx.Value(driverIDKey).(string) return id, ok } // EmailFromContext retrieves the email from the context. func EmailFromContext(ctx context.Context) (string, bool) { email, ok := ctx.Value(emailKey).(string) return email, ok }