gofr-dev/gofr · error

Unauthorized: Missing or invalid role

Error message

Unauthorized: Missing or invalid role

What it means

The default error handler in handleAuthError writes "Unauthorized: Missing or invalid role" with HTTP 401 when the authorization error matches ErrRoleNotFound via errors.Is. ErrRoleNotFound is returned by extractRole when neither JWT nor header extraction yields a role — i.e. the request arrived at a protected endpoint without an identifiable role. This is an authentication-adjacent problem: the middleware cannot even determine who/what role the caller has.

Source

Thrown at pkg/gofr/rbac/middleware.go:192

		span.RecordError(safeErr)
		span.SetStatus(codes.Error, safeErr.Error())
	}

	// Log audit event (always enabled when Logger is available)
	// Audit logging is automatically performed using GoFr's logger
	if config.Logger != nil {
		logAuditEvent(config.Logger, r, role, route, false)
	}

	// Use custom error handler if provided
	if config.ErrorHandler != nil {
		config.ErrorHandler(w, r, role, route, err)
		return
	}

	// Default error handling
	if errors.Is(err, ErrRoleNotFound) {
		http.Error(w, "Unauthorized: Missing or invalid role", http.StatusUnauthorized)
		return
	}

	http.Error(w, "Forbidden: Access denied", http.StatusForbidden)
}

// extractRole extracts the user's role from the request.
// Supports header-based extraction (via RoleHeader) or JWT-based extraction (via JWTClaimPath).
// Precedence: JWT takes precedence over header (JWT is more secure).
// No default role is supported - role must be explicitly provided.
func extractRole(r *http.Request, config *Config) (string, error) {
	// Try JWT-based extraction first (takes precedence - more secure)
	if config.JWTClaimPath != "" {
		role, err := extractRoleFromJWT(r, config.JWTClaimPath)
		if err == nil && role != "" {
			return role, nil
		}
		// If JWT extraction fails but JWTClaimPath is set, don't fall back to header

View on GitHub (pinned to 187eb24962)

Solutions

  1. Ensure the OAuth/JWT middleware runs before rbac.Middleware so JWTClaim is populated in the request context.
  2. Verify clients send the credential: Authorization: Bearer <token> or the configured RoleHeader on every request to protected routes.
  3. Confirm the token actually contains the claim at JWTClaimPath (decode the token); note JWTClaimPath disables header fallback entirely.
  4. Mount rbac.Middleware only on routes that require auth, and keep public endpoints in the Endpoints public list.

Example fix

// before (OAuth middleware missing, RBAC sees no claims)
r.Use(rbac.Middleware(cfg))
// after
r.Use(middleware.JWTValidation("secret"))
r.Use(rbac.Middleware(cfg))
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure middleware order and credentials before calling protected routes:
// server.Use(middleware.OAuth(...)); server.Use(rbac.Middleware(cfg))
func requestHasAuth(r *http.Request) bool {
    return r.Header.Get("Authorization") != ""
}

Type guard

func hasJWTClaims(r *http.Request) bool {
    claims, ok := r.Context().Value(middleware.JWTClaim).(jwt.MapClaims)
    return ok && claims != nil
}

Try / catch

role, err := extractRole(r, cfg)
if err != nil {
    if errors.Is(err, ErrRoleNotFound) {
        logger.Warn("no role: check token presence and middleware order", "path", r.URL.Path)
    }
    http.Error(w, "unauthorized", http.StatusUnauthorized)
    return
}

Prevention

When it happens

Trigger: JWTClaimPath is set but JWT extraction fails (claims missing from context or role claim absent) — extractRole then returns ErrRoleNotFound without falling back to the header; RoleHeader is set but the request lacks that header; neither JWTClaimPath nor RoleHeader configured and no default role exists; endpoint is RBAC-configured (non-public) but the caller sent no usable token.

Common situations: Client forgot to send the Authorization header or Bearer token; OAuth middleware not mounted before the RBAC middleware so JWT claims never reach the context; RoleHeader name mismatched (case or custom prefix like X- vs x- handled, but wrong name entirely); gateway strips the role header; tokens issued without the expected role claim.

Understand the failure class

Related errors


AI-assisted analysis of gofr-dev/gofr@187eb24962 (2026-09-01). Data as JSON: /api/errors/17880a807a3103ba. Report an issue: GitHub.