gofr-dev/gofr · warning

Forbidden: Access denied

Error message

Forbidden: Access denied

What it means

The default error handler in handleAuthError writes "Forbidden: Access denied" with HTTP 403 for any authorization error that is NOT ErrRoleNotFound — most commonly ErrAccessDenied, returned when the role was extracted successfully but checkEndpointAuthorization denies it for the configured route. It is the terminal response when a valid, identified caller lacks permission for the endpoint.

Source

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

	// 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
		// This ensures JWT is the only method when configured
		return "", ErrRoleNotFound
	}

View on GitHub (pinned to 187eb24962)

Solutions

  1. Compare the user's actual role (from the audit log's Role field) against the roles configured for that endpoint in the RBAC Config and add the missing role or fix casing.
  2. Decode the token to confirm the claim value matches exactly (case-sensitive) the roles listed in config.
  3. Update the identity provider's role assignments if the user genuinely needs access.
  4. Supply a custom config.ErrorHandler if you need role-specific or localized 403 responses instead of the default plain text.

Example fix

// before
endpoints: {path: "/admin", method: "GET", allowedRoles: ["admin"]} // user is "Admin"
// after
endpoints: {path: "/admin", method: "GET", allowedRoles: ["admin", "Admin"]} // or normalize claim casing in IdP
Defensive patterns

Strategy: validation

Validate before calling

func roleAllowed(role string, endpoint Endpoint) bool {
    for _, r := range endpoint.AllowedRoles {
        if r == role { return true }
    }
    return false
}
// pre-check role casing at startup against a sample token:
// if !roleAllowed(sampleRole, ep) { log.Warn("sample role not in allowedRoles — check casing") }

Type guard

func isAccessDenied(err error) bool { return errors.Is(err, ErrAccessDenied) }

Try / catch

authorized, _ := checkEndpointAuthorization(role, endpoint, cfg)
if !authorized {
    logger.Warn("rbac denied", "role", role, "route", routeLabel, "allowed", endpoint.AllowedRoles)
    http.Error(w, "forbidden", http.StatusForbidden)
    return
}

Prevention

When it happens

Trigger: A request with a valid role hits an RBAC-configured endpoint whose allowed role list does not include that role (checkEndpointAuthorization returns false, handleAuthError receives ErrAccessDenied); also any non-ErrRoleNotFound error surfaces as 403 by default; if config.ErrorHandler is set, this branch never runs.

Common situations: User has role "viewer" but the endpoint only allows "admin"; role claim casing mismatch ("Admin" vs "admin") so no configured role matches; roles renamed in the IdP without updating RBAC route config; wildcard/case-sensitivity assumptions about role matching; stale role mappings after an org restructure.

Understand the failure class

Related errors


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