Files
httpauth/enrichment.go
Rene Nochebuena 601e6a5144 fix(httpauth): wildcard resource fallback, json.Number precision, xerrors JSON error bodies (#1)
ClaimsPermissionProvider.ResolveMask now falls back to the "*" resource key when the
exact resource is absent in the JWT masks claim. Specific resource takes precedence;
wildcard is the fallback. Enables ADMIN wildcard masks ({"*": MaxInt64}) to pass every
endpoint guard without per-resource entries.

Add json.Number handling alongside existing int64/float64 paths. json.Number is
produced by jwt.WithJSONNumber() (httpauth-jwt) and preserves math.MaxInt64 exactly —
float64 would round to 2^63 and overflow on cast back to int64.

Export WriteJSONError(w, status, code, message) — shared JSON error helper for all
httpauth-* provider packages. Ensures a consistent {"code":"...","message":"..."} body
and Content-Type: application/json across the full middleware stack.

AuthzMiddleware and EnrichmentMiddleware now use WriteJSONError with xerrors code
constants (UNAUTHENTICATED, PERMISSION_DENIED, INTERNAL) instead of http.Error which
writes text/plain. AuthzMiddleware also fixes a wrong code string: UNAUTHORIZED →
UNAUTHENTICATED (stable gRPC-aligned name, matching xerrors.ErrUnauthorized).

Add xerrors v1.0.1 as a direct dependency for the code constants.

Reviewed-on: #1
Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
2026-05-18 14:15:12 -06:00

69 lines
2.0 KiB
Go

package httpauth
import (
"context"
"net/http"
"code.nochebuena.dev/go/rbac"
"code.nochebuena.dev/go/xerrors"
)
// IdentityEnricher builds an rbac.Identity from verified token data.
// The application provides the implementation — typically reads from a user store.
type IdentityEnricher interface {
Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error)
}
// EnrichOpt configures EnrichmentMiddleware.
type EnrichOpt func(*enrichConfig)
type enrichConfig struct {
tenantHeader string
}
// WithTenantHeader configures the request header from which TenantID is read.
// If the header is absent on a request, TenantID remains "" — no error is returned.
func WithTenantHeader(header string) EnrichOpt {
return func(c *enrichConfig) {
c.tenantHeader = header
}
}
// EnrichmentMiddleware reads uid + claims injected by any upstream AuthMiddleware
// via SetTokenData, calls enricher.Enrich, and stores the resulting rbac.Identity
// in context via rbac.SetInContext.
// Returns 401 if no uid is present (SetTokenData was not called upstream).
// Returns 500 if the enricher fails.
func EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler {
cfg := &enrichConfig{}
for _, o := range opts {
o(cfg)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, ok := getUID(r.Context())
if !ok {
WriteJSONError(w, http.StatusUnauthorized, string(xerrors.ErrUnauthorized), "unauthorized")
return
}
claims, _ := getClaims(r.Context())
identity, err := enricher.Enrich(r.Context(), uid, claims)
if err != nil {
WriteJSONError(w, http.StatusInternalServerError, string(xerrors.ErrInternal), "internal server error")
return
}
if cfg.tenantHeader != "" {
if tenantID := r.Header.Get(cfg.tenantHeader); tenantID != "" {
identity = identity.WithTenant(tenantID)
}
}
ctx := rbac.SetInContext(r.Context(), identity)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}