gofr-dev/gofr · error

failed to extract role from JWT: %w

Error message

failed to extract role from JWT: %w

What it means

A wrapped error returned by extractRoleFromJWT when extractClaimValue fails to resolve JWTClaimPath inside the token's claims. The original sentinel (errClaimPathNotFound, errEmptyClaimPath, errInvalidArrayNotation, errClaimValueNotArray, etc.) is preserved via %w, prefixed with "failed to extract role from JWT: ". Ultimately this bubbles up as ErrRoleNotFound from extractRole, producing a 401 by default.

Source

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

	}

	// No role found - no default role supported
	return "", ErrRoleNotFound
}

// extractRoleFromJWT extracts the role from JWT claims in the request context.
// It uses the JWTClaimPath from config to navigate the claim structure.
func extractRoleFromJWT(r *http.Request, claimPath string) (string, error) {
	// Get JWT claims from context (set by OAuth middleware)
	claims, ok := r.Context().Value(middleware.JWTClaim).(jwt.MapClaims)
	if !ok || claims == nil {
		return "", fmt.Errorf("%w", errJWTClaimsNotFound)
	}

	// Extract role using the configured claim path
	role, err := extractClaimValue(claims, claimPath)
	if err != nil {
		return "", fmt.Errorf("failed to extract role from JWT: %w", err)
	}

	// Convert to string
	roleStr, ok := role.(string)
	if !ok {
		// Try to convert if it's not a string
		return fmt.Sprintf("%v", role), nil
	}

	return roleStr, nil
}

// extractClaimValue extracts a value from JWT claims using a dot-notation or array notation path.
// Examples:
//   - "role" -> claims["role"]
//   - "roles[0]" -> claims["roles"].([]any)[0]
//   - "permissions.role" -> claims["permissions"].(map[string]any)["role"]
func extractClaimValue(claims jwt.MapClaims, path string) (any, error) {

View on GitHub (pinned to 187eb24962)

Solutions

  1. Read the wrapped sentinel in the error text (after the prefix) to identify the exact cause, then fix Config.JWTClaimPath accordingly.
  2. Decode a real production token (jwt.io) and mirror its exact claim structure in JWTClaimPath.
  3. Add a startup smoke test that parses a sample token with your configured path to fail fast on mismatch.
  4. If failures are per-user, ensure the IdP template includes the claim for all clients/users rather than changing the path.

Example fix

// before (Keycloak token nests roles)
config.JWTClaimPath = "roles[0]" // -> failed to extract role from JWT: claim key not found: roles
// after
config.JWTClaimPath = "realm_access.roles[0]"
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate JWTClaimPath against a sample token before deploy:
// _, err := extractClaimValue(sampleClaims, cfg.JWTClaimPath)
// if err != nil { log.Fatalf("JWTClaimPath invalid for sample token: %v", err) }

Type guard

func pathResolves(claims jwt.MapClaims, path string) bool {
    _, err := extractClaimValue(claims, path)
    return err == nil
}

Try / catch

role, err := extractRoleFromJWT(r, cfg.JWTClaimPath)
if err != nil {
    var cause error
    // unwrap to the sentinel beneath the "failed to extract role from JWT" prefix
    for e := err; e != nil; e = errors.Unwrap(e) { cause = e }
    logger.Error("jwt role extraction failed", "cause", cause)
    http.Error(w, "unauthorized", http.StatusUnauthorized)
    return
}

Prevention

When it happens

Trigger: Any invalid JWTClaimPath against real tokens: empty path, key that doesn't exist ("claim path not found: xyz"), malformed array notation ("roles[a]"), non-array value with index notation, out-of-range index, or broken nested structure — each wrapped with this prefix inside extractRoleFromJWT.

Common situations: Deploying a service with a claim path copied from a different environment/IdP where claim names differ; renaming claims in the IdP without updating config; typos in JWTClaimPath committed to config; Keycloak realm_access nesting forgotten (path "roles" instead of "realm_access.roles[0]").

Related errors


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