Keycloak 403 Forbidden: Complete Troubleshooting Guide
Last updated: March 2026
A 403 Forbidden response from Keycloak or a Keycloak-protected application means the server understood your request but refused to authorize it. Unlike a 401 Unauthorized (which means “you are not authenticated”), a 403 says “you are authenticated, but you do not have permission to do this.”
The challenge with 403 errors in Keycloak environments is that the cause can originate from multiple layers: Keycloak’s own authorization policies, missing or incorrect role mappings, wrong audience claims in tokens, CORS configuration, or reverse proxy settings. This guide walks through every common cause, shows how to diagnose each one, and provides the fix.
Quick Diagnostic Checklist
Before diving into specific causes, run through this checklist:
- Is the user authenticated? Check that you have a valid access token (not expired).
- Does the token contain the expected roles? Decode it with the JWT Token Analyzer.
- Is the 403 from Keycloak itself or from your application? Check the response body and headers.
- Are you hitting the Keycloak Admin API? Admin API access requires specific roles.
- Is CORS involved? Browser requests to a different origin may show as 403.
Cause 1: Missing Role Assignments
The most common cause. The user does not have the role your application requires.
Symptoms
- API returns 403 with a message like “insufficient permissions” or “access denied”
- The decoded token’s
realm_access.rolesorresource_accessdoes not include the expected role
Diagnosis
Get a token and decode it:
# Get a token
TOKEN=$(curl -s -X POST
http://localhost:8080/realms/myrealm/protocol/openid-connect/token
-d "grant_type=password"
-d "client_id=my-client"
-d "username=testuser"
-d "password=testpass" | jq -r '.access_token')
# Decode the payload (base64)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.realm_access, .resource_access'
Or paste the token into the JWT Token Analyzer for a visual breakdown.
Fix
- Go to the Keycloak admin console
- Navigate to Users > select the user > Role mappings
- Click Assign role and add the required role
Check whether your application expects realm roles or client roles:
- Realm roles appear under
realm_access.rolesin the token - Client roles appear under
resource_access.{client-id}.roles
This distinction catches many developers. If your application checks resource_access.my-api.roles but you assigned a realm role, the check will fail.
For a deeper understanding of role hierarchies, composite roles, and how they interact, see our RBAC feature guide and the Keycloak role documentation.
Cause 2: Wrong Audience (aud) Claim
If your API validates the aud (audience) claim in the access token, a mismatch will cause a 403.
Symptoms
- Token is valid and contains correct roles, but the API still rejects it
- API logs show “invalid audience” or “token not intended for this resource”
Diagnosis
Decode the token and check the aud claim:
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.aud'
By default, Keycloak access tokens may have aud set to account or may not include your API’s client ID at all.
Fix
Add an audience mapper to the client that requests the token:
- Go to Clients > select the client > Client scopes tab
- Click on the dedicated scope (e.g.,
my-client-dedicated) - Click Configure a new mapper > Audience
- Set:
- Name:
api-audience - Included Client Audience: Select your API client
- Add to access token: On
- Name:
After adding the mapper, get a new token and verify that aud now includes your API client ID.
Cause 3: Missing or Incorrect Scopes
OAuth 2.0 scopes control what permissions a token grants. If your application requires specific scopes and the token does not include them, you will get a 403.
Symptoms
- The login request does not include required scopes
- Token’s
scopeclaim does not match what the API expects
Diagnosis
Check the scope claim in the token:
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.scope'
Fix
Include the required scopes in the authorization request:
/realms/myrealm/protocol/openid-connect/auth?
client_id=my-client&
response_type=code&
scope=openid email profile my-custom-scope&
redirect_uri=http://localhost:3000/callback
In Keycloak, create optional client scopes and assign them to your client:
- Go to Client scopes > Create client scope
- Set the name and protocol (OpenID Connect)
- Go to Clients > your client > Client scopes tab
- Add the scope as either Default (always included) or Optional (included when requested)
Cause 4: Client Authorization Policies
Keycloak’s Authorization Services allow fine-grained access control beyond simple roles. If your client has authorization enabled, policies and permissions are evaluated and can result in 403.
Symptoms
- The client has Authorization enabled in its settings
- 403 responses even when the user has the correct roles
- Response may include
{"error": "access_denied"}from Keycloak itself
Diagnosis
- Go to Clients > your client > Authorization tab
- Check Policies: Do any policies deny access?
- Check Permissions: Are resources properly mapped to policies?
- Use the Evaluate tab to test access decisions for specific users
Fix
If you do not need fine-grained authorization, disable it:
- Go to Clients > your client > Settings
- Set Authorization to Off
If you do need it, review your policies:
- Positive policies grant access; negative policies deny access
- Permissions link resources to policies
- The Decision Strategy (Affirmative, Unanimous, Consensus) determines how multiple policies interact
See our post on fine-grained authorization in Keycloak for a complete walkthrough of policies and permissions.
Cause 5: CORS Configuration
Cross-Origin Resource Sharing (CORS) issues can manifest as 403 errors in the browser, especially when your frontend and Keycloak are on different origins.
Symptoms
- 403 errors only in the browser (curl works fine)
- Browser console shows CORS-related errors
- Preflight (OPTIONS) requests return 403
Diagnosis
Open your browser’s developer tools and check the Network tab. Look for:
- The
Originheader in the request - The
Access-Control-Allow-Originheader in the response - Whether preflight OPTIONS requests succeed
Fix
In Keycloak client settings:
- Go to Clients > your client > Settings
- Set Web origins to your frontend URL (e.g.,
http://localhost:3000) - Do not use
*in production
In your reverse proxy:
If Keycloak is behind Nginx or Traefik, make sure the proxy is not stripping or overriding CORS headers. Here is a working Nginx configuration:
location /realms/ {
proxy_pass http://keycloak:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# Do not add CORS headers here if Keycloak manages them
}
For a detailed walkthrough of CORS with Keycloak, see our dedicated post on configuring CORS with Keycloak OIDC clients.
Cause 6: Keycloak Admin API Access Denied
If you are calling the Keycloak Admin REST API and getting 403, the issue is almost always role-related.
Symptoms
- Calls to
/admin/realms/...return 403 - You are using a service account or admin user
Diagnosis
The Admin API requires specific roles from the realm-management client:
# Decode the token and check resource_access
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null |
jq '.resource_access["realm-management"].roles'
Fix
For a service account:
- Go to Clients > your client > Service account roles
- Click Assign role
- Filter by client:
realm-management - Assign the required roles:
view-usersfor reading user datamanage-usersfor creating/updating usersmanage-realmfor realm-level operationsrealm-adminfor full admin access
For a user account:
- Go to Users > select the user > Role mappings
- Assign the appropriate
realm-managementclient roles
Be careful with realm-admin. It grants full access to the realm. Follow the principle of least privilege and assign only the roles your application actually needs.
Cause 7: Token Expired or Invalid
While technically a 401 issue, some applications return 403 for expired tokens instead of the standard 401.
Symptoms
- Requests worked earlier but suddenly return 403
- Token’s
expclaim is in the past
Diagnosis
# Check token expiration
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null |
jq '.exp | todate'
Fix
Implement proper token refresh logic in your application. When you receive a 401 or 403, attempt to refresh the token before retrying:
import time
import jwt
def is_token_expired(token: str, buffer_seconds: int = 30) -> bool:
"""Check if a token is expired or about to expire."""
try:
payload = jwt.decode(token, options={"verify_signature": False})
exp = payload.get("exp", 0)
return time.time() > (exp - buffer_seconds)
except jwt.DecodeError:
return True
See our guide on session management for best practices on token lifecycle management.
Cause 8: Realm vs Client Role Confusion
Keycloak has two types of roles, and confusing them is a frequent source of 403 errors.
Realm Roles
- Defined at the realm level
- Available to all clients in the realm
- Found in
realm_access.rolesin the token - Good for application-wide permissions (e.g.,
admin,user)
Client Roles
- Defined within a specific client
- Scoped to that client
- Found in
resource_access.{client-id}.rolesin the token - Good for service-specific permissions (e.g.,
api:read,api:write)
Common Mistake
Your application checks for a realm role, but you assigned it as a client role (or vice versa).
# This checks realm roles
def check_realm_role(token_payload, role):
roles = token_payload.get("realm_access", {}).get("roles", [])
return role in roles
# This checks client roles
def check_client_role(token_payload, client_id, role):
roles = (
token_payload.get("resource_access", {})
.get(client_id, {})
.get("roles", [])
)
return role in roles
Fix
Decide on a role strategy and be consistent. For most applications, realm roles are simpler. Use client roles when you need per-service permissions in a microservices architecture.
Cause 9: Composite Role Not Expanding
Composite roles contain other roles. If a composite role is not properly configured, users may appear to have the parent role but not the child roles.
Symptoms
- User has a composite role assigned
- Token does not contain the expected child roles
- Application checks for child roles and denies access
Diagnosis
- Go to Realm roles > select the composite role
- Click the Associated roles tab
- Verify the child roles are listed
Fix
If child roles are missing from the token, check:
- The composite role’s associated roles are correctly configured
- The token mapper is set to include roles from composite roles (default behavior)
- The client’s scope includes the necessary role scope mappings
Cause 10: Reverse Proxy or Load Balancer Issues
If Keycloak is behind a reverse proxy that is not properly configured, authentication may succeed but authorization headers may be lost or modified.
Symptoms
- Authentication works (login succeeds)
- Subsequent API calls return 403
- Direct calls to the backend (bypassing the proxy) work
Diagnosis
Check the headers reaching your application:
@app.route("/debug-headers")
def debug_headers():
return jsonify(dict(request.headers))
Fix
Ensure your reverse proxy forwards all necessary headers. For Nginx:
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Authorization $http_authorization;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_header Authorization;
}
For Keycloak’s own proxy configuration, see the Keycloak reverse proxy guide and our login loop troubleshooting guide for related proxy issues.
Debugging with Keycloak Logs
Keycloak’s server logs are invaluable for diagnosing 403 issues.
Enable Debug Logging
For development, set the log level to DEBUG:
# Docker
docker run -p 8080:8080
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin
-e KC_LOG_LEVEL=DEBUG
quay.io/keycloak/keycloak:26.0 start-dev
For production, enable debug logging only for specific categories:
KC_LOG_LEVEL=INFO,org.keycloak.authorization:DEBUG,org.keycloak.services:DEBUG
Enable Event Logging
Keycloak’s audit logs capture authentication and authorization events:
- Go to Realm Settings > Events
- Enable Login events and Admin events
- Set appropriate expiration times
- Check the Events section in the sidebar after reproducing the issue
Events show exactly what happened: which user attempted what action, which client was involved, and what the result was. For production monitoring, Skycloak’s insights dashboard provides real-time event analysis.
Step-by-Step Debugging Workflow
When you encounter a 403, follow this systematic approach:
Step 1: Identify the Source
Determine whether the 403 comes from Keycloak, your application, or a proxy:
# Check the response body and headers
curl -v -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/protected
Step 2: Decode the Token
# Use the JWT Token Analyzer or decode manually
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq '.'
Check:
exp: Is the token expired?iss: Does the issuer match your Keycloak URL?aud: Does the audience include your API?realm_access.roles: Are the expected roles present?resource_access: Are client roles present?
Step 3: Test with curl
Remove the browser from the equation:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/protected
If curl works but the browser does not, it is a CORS issue.
Step 4: Check Keycloak Events
Look at Events in the Keycloak admin console for any error events around the time of the 403.
Step 5: Compare Working and Non-Working Tokens
If one user can access a resource and another cannot, decode both tokens side by side and compare the roles, scopes, and audience claims.
Prevention: Best Practices
To avoid 403 errors in production:
-
Use the Keycloak Evaluation tool: Under Clients > your client > Authorization > Evaluate, test policy decisions before deploying.
-
Implement proper error responses: Return descriptive error messages (in non-production environments) that include which role was expected and what roles the user has.
-
Use consistent role naming: Establish a naming convention (e.g.,
service:action) and document it. -
Automate role assignment: Use group-role mappings so that adding a user to a group automatically grants the correct roles. Use SCIM for automated provisioning from external systems. Test your SCIM endpoints with the SCIM Endpoint Tester.
-
Monitor authentication events: Set up audit logging and alerting for unusual patterns of 403 responses.
-
Document your authorization model: Keep a clear record of which roles grant access to which resources.
Try Skycloak
Debugging Keycloak issues is easier when you have proper logging, monitoring, and support. Skycloak provides managed Keycloak with built-in insights and monitoring, audit logging, and expert support. See our pricing or explore the documentation to get started.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.