This is a Keycloak-side solution — you configure Keycloak itself to reject the token request for a specific user on a specific client, before your app even sees the token.
The advantage over app-side checks is that the user never gets a token at all.
User tries to login
↓
Keycloak evaluates Authorization policies
↓
┌─────────────────┐
│ Policy: DENY │ ← user is blocked here
└─────────────────┘
↓
Returns 403 — no token issued
↓
Your app never receives anything
Clients →
company-portal→ Authorization tab → Enabled: ON (requires client to beconfidentialorbearer-only)
Authorization → Policies → Create policy → User
Name: deny-dave-policy
Description: Explicitly deny dave from this client
Users: dave
Logic: NEGATIVE ← this is what makes it a DENY
⚠️ Logic: NEGATIVE means — "apply this as a denial for the selected users"
Authorization → Permissions → Create permission → Resource-based
Name: deny-dave-permission
Resources: (leave blank = applies to all)
Policies: deny-dave-policy
Decision: UNANIMOUS
Authorization → Evaluate tab
Client: company-portal
User: dave
→ Result: DENY ✅
User: carol
→ Result: PERMIT ✅
When Dave tries to log in through company-portal:
POST /realms/myrealm/protocol/openid-connect/token
HTTP/1.1 403 Forbidden
{
"error": "access_denied",
"error_description": "not_authorized"
}He gets no token. No access. No way around it from the app side.
| Scenario | Solution |
|---|---|
| Contractor ended their contract | Deny policy by user |
| Entire department loses access | Deny policy by group |
| Block access outside business hours | Deny policy by time |
| Block a specific IP range | Deny policy by JS rule |
| App-side check | Client-level block | |
|---|---|---|
| Where enforced | Your backend | Keycloak |
| Token issued? | ✅ Yes (then rejected) | ❌ No |
| Audit trail | In your app logs | In Keycloak logs |
| Scalability | Per endpoint | One rule covers all |
| Best for | Role-based routing | Explicit user/group ban |
The client-level block is the most secure option because the token is never issued — there's nothing for the user to intercept or reuse.