backend/api/middleware/context.go (new): - Introduces an unexported ctxKey type and three constants (ctxKeyUserAddress, ctxKeyUserTrack, ctxKeyAuthenticated) that replace the bare string keys 'user_address', 'user_track', and 'authenticated'. Bare strings trigger go vet's SA1029 and collide with keys from any other package that happens to share the name. - Helpers: ContextWithAuth, UserAddress, UserTrack, IsAuthenticated. - Sentinel: ErrMissingAuthorization replaces the misuse of http.ErrMissingFile as an auth-missing signal. (http.ErrMissingFile belongs to multipart form parsing and was semantically wrong.) backend/api/middleware/auth.go: - RequireAuth, OptionalAuth, RequireTrack now all read/write via the helpers; no more string literals for context keys in this file. - extractAuth returns ErrMissingAuthorization instead of http.ErrMissingFile. - Dropped now-unused 'context' import. backend/api/track4/operator_scripts.go, backend/api/track4/endpoints.go, backend/api/rest/features.go: - Read user address / track via middleware.UserAddress() and middleware.UserTrack() instead of a raw context lookup with a bare string key. - Import 'github.com/explorer/backend/api/middleware'. backend/api/track4/operator_scripts_test.go: - Four test fixtures updated to seed the request context through middleware.ContextWithAuth (track 4, authenticated) instead of context.WithValue with a bare 'user_address' string. This is the load-bearing change that proves typed keys are required: a bare string key no longer wakes up the middleware helpers. backend/api/middleware/context_test.go (new): - Round-trip test for ContextWithAuth + UserAddress + UserTrack + IsAuthenticated. - Defaults: UserTrack=1, UserAddress="", IsAuthenticated=false on a bare context. - TestContextKeyIsolation: an outside caller that inserts 'user_address' as a bare string key must NOT be visible to UserAddress; proves the type discipline. - ErrMissingAuthorization sentinel smoke test. Verification: - go build ./... clean. - go vet ./... clean (removes SA1029 on the old bare keys). - go test ./api/middleware/... ./api/track4/... ./api/rest/... PASS. Advances completion criterion 3 (Auth correctness).
108 lines
3.1 KiB
Go
108 lines
3.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/explorer/backend/auth"
|
|
"github.com/explorer/backend/featureflags"
|
|
)
|
|
|
|
// AuthMiddleware handles authentication and authorization
|
|
type AuthMiddleware struct {
|
|
walletAuth *auth.WalletAuth
|
|
}
|
|
|
|
// NewAuthMiddleware creates a new auth middleware
|
|
func NewAuthMiddleware(walletAuth *auth.WalletAuth) *AuthMiddleware {
|
|
return &AuthMiddleware{
|
|
walletAuth: walletAuth,
|
|
}
|
|
}
|
|
|
|
// RequireAuth is middleware that requires authentication
|
|
func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
address, track, err := m.extractAuth(r)
|
|
if err != nil {
|
|
writeUnauthorized(w)
|
|
return
|
|
}
|
|
|
|
ctx := ContextWithAuth(r.Context(), address, track, true)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// RequireTrack is middleware that requires a specific track level
|
|
func (m *AuthMiddleware) RequireTrack(requiredTrack int) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
track := UserTrack(r.Context())
|
|
|
|
if !featureflags.HasAccess(track, requiredTrack) {
|
|
writeForbidden(w, requiredTrack)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// OptionalAuth is middleware that optionally authenticates (for Track 1 endpoints)
|
|
func (m *AuthMiddleware) OptionalAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
address, track, err := m.extractAuth(r)
|
|
if err != nil {
|
|
// No auth provided (or auth failed) — fall back to Track 1.
|
|
ctx := ContextWithAuth(r.Context(), "", defaultTrackLevel, false)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
ctx := ContextWithAuth(r.Context(), address, track, true)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// extractAuth extracts authentication information from the request.
|
|
// Returns ErrMissingAuthorization when no usable Bearer token is present;
|
|
// otherwise returns the error from JWT validation.
|
|
func (m *AuthMiddleware) extractAuth(r *http.Request) (string, int, error) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return "", 0, ErrMissingAuthorization
|
|
}
|
|
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
return "", 0, ErrMissingAuthorization
|
|
}
|
|
|
|
token := parts[1]
|
|
|
|
address, track, err := m.walletAuth.ValidateJWT(token)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
return address, track, nil
|
|
}
|
|
|
|
// writeUnauthorized writes a 401 Unauthorized response
|
|
func writeUnauthorized(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte(`{"error":{"code":"unauthorized","message":"Authentication required"}}`))
|
|
}
|
|
|
|
// writeForbidden writes a 403 Forbidden response
|
|
func writeForbidden(w http.ResponseWriter, requiredTrack int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":{"code":"forbidden","message":"Insufficient permissions","required_track":` + fmt.Sprintf("%d", requiredTrack) + `}}`))
|
|
}
|
|
|