package auth import ( "context" "encoding/json" "net/http" "strings" ) // claimsCtxKey is the context key for a validated Claims set by Authorize. type claimsCtxKey struct{} // ClaimsFromContext returns the validated Claims stored by Authorize. func ClaimsFromContext(ctx context.Context) (*Claims, bool) { claims, ok := ctx.Value(claimsCtxKey{}).(*Claims) return claims, ok } // Authorize returns an HTTP middleware that requires a valid Bearer JWT whose // role satisfies at least one of the required roles. With no required roles it // admits any authenticated call. A missing/invalid/expired token yields 401 // (ErrUnauthorized); an authenticated token with insufficient privilege yields // 403 (ErrForbidden). The validated claims are stored in the request context // for downstream handlers. func (s *JWTService) Authorize(required ...Role) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token, ok := bearerToken(r) if !ok { s.log.Debug("missing bearer token") writeAuthError(w, http.StatusUnauthorized, ErrUnauthorized) return } claims, err := s.Verify(token) if err != nil { s.log.Debug("token verify failed", "err", err) writeAuthError(w, http.StatusUnauthorized, ErrUnauthorized) return } if !satisfies(claims.Role, required) { s.log.Debug("insufficient role", "role", claims.Role) writeAuthError(w, http.StatusForbidden, ErrForbidden) return } ctx := context.WithValue(r.Context(), claimsCtxKey{}, claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // satisfies reports whether role has enough privilege for at least one of the // required roles. An empty required list means any authenticated role passes. func satisfies(role Role, required []Role) bool { if len(required) == 0 { return true } for _, r := range required { if HasRole(role, r) { return true } } return false } // bearerToken extracts the token from an Authorization: Bearer header. func bearerToken(r *http.Request) (string, bool) { const prefix = "Bearer " h := r.Header.Get("Authorization") if !strings.HasPrefix(h, prefix) { return "", false } token := strings.TrimSpace(h[len(prefix):]) return token, token != "" } // writeAuthError writes an {error:{code,message}} JSON response matching the // project's client contract. No token material is ever logged. func writeAuthError(w http.ResponseWriter, status int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(map[string]any{ "error": map[string]any{ "code": authErrorCode(status), "message": err.Error(), }, }) } // authErrorCode maps an HTTP status to a stable error code string. func authErrorCode(status int) string { switch status { case http.StatusUnauthorized: return "unauthorized" case http.StatusForbidden: return "forbidden" default: return http.StatusText(status) } }