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
- 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.
- Decode the token to confirm the claim value matches exactly (case-sensitive) the roles listed in config.
- Update the identity provider's role assignments if the user genuinely needs access.
- 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
- Keep allowedRoles in config in the exact casing the IdP emits
- Check the audit log's Role field when users report 403s
- Synchronize role names between IdP and RBAC config in CI (shared constants or schema)
- Re-review role mappings after org/tenant restructuring
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Unauthorized: Missing or invalid role
- failed to parse YAML config file %s: %w
- failed to parse JSON config file %s: %w
- unsupported config file format: %s (supported: .json, .yaml,
- invalid RBAC config: %w
AI-assisted analysis of gofr-dev/gofr@187eb24962 (2026-09-01).
Data as JSON: /api/errors/eeff94d44d1dadde.
Report an issue: GitHub.