Limited Time Offer!
For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!

Application logs
[KC] getAdminToken POST https://auth.local.com/realms/wizard/protocol/openid-connect/token
[KC] getAdminToken failed {"status":401,"body":"{\"error\":\"unauthorized_client\",\"error_description\":\"Client not enabled to retrieve service account\"}"}
Keycloak admin token error
Meaning
You tried to get a token with grant_type=client_credentials
using a Keycloak client that does not have Service Accounts enabled (or isn’t confidential). Keycloak rejects it with unauthorized_client
.
Why it happens (root cause)
- Client Credentials flow uses the service account of a confidential client.
- If the client is public or Service Accounts are OFF, Keycloak won’t issue a token.
- Even if Service Accounts are ON, the service account user needs enough realm-management roles for the admin APIs you call (users, groups, clients, roles).
What we want to achieve
- Use a dedicated admin client (e.g.,
wizbrand-admin-cli
) to call admin REST APIs. - Enable Service Accounts and assign least-privilege roles.
- Keep your app client (e.g.,
wizbrand-web
) separate from admin duties.
Prerequisites
- Realm:
wizbrand
- Base URL:
https://auth.holidaylandmark.com
- Admin access to Keycloak
- Laravel env variables available
Fix in Keycloak (UI steps)
- Create / Edit the admin client
- Go to Clients → Create (or open existing
wizbrand-admin-cli
) - Client Type: OpenID Connect
- Client ID:
wizbrand-admin-cli
- Authentication / Access settings (wording varies by Keycloak version):
- Client authentication: ON (makes it Confidential)
- Service accounts enabled: ON
- (Flows) Standard Flow / Direct Access Grants: not required for service accounts
- Go to Clients → Create (or open existing
- Copy the client secret
- Credentials tab → copy Secret
- Assign service account permissions (least-privilege)
- Service Account Roles tab → Assign role
- From Client = realm-management, add only what you need:
view-users
,manage-users
view-groups
,manage-groups
view-clients
(andmanage-clients
if you create client roles)view-realm
- If you create/manage client roles:
manage-roles
(or userealm-admin
for broad access in lower environments)
Tip: start with minimal roles; add more only if a call fails with 403.
Update your Laravel configuration
.env
KEYCLOAK_ADMIN_CLIENT_ID=wizbrand-admin-cli
KEYCLOAK_ADMIN_CLIENT_SECRET=<paste-secret>
KEYCLOAK_CLIENT_ID=wizbrand-web
KEYCLOAK_BASE_URL=https://auth.holidaylandmark.com
KEYCLOAK_REALM=wizbrand
config/services.php (example)
'keycloak' => [
'base_url' => env('KEYCLOAK_BASE_URL'),
'realm' => env('KEYCLOAK_REALM', 'master'),
'realms' => env('KEYCLOAK_REALM', 'master'), // if your code reads 'realms'
'org_group_prefix' => env('KEYCLOAK_ORG_PREFIX', ''),
],
Clear config caches
php artisan config:clear && php artisan cache:clear
Verify with cURL first (quick sanity check)
curl -s -X POST \
"https://auth.local.com/realms/wizard/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=wizbrand-admin-cli" \
-d "client_secret=<paste-secret>"
Expected: JSON containing access_token
and expires_in
.
If you still get 401, re-check: realm, client id, secret, service accounts toggle, and that the client is confidential.
How the Laravel service uses the token (your code)
Your KeycloakAdminService::getAdminToken()
calls the same token endpoint with:
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => $this->adminClientId, // wizbrand-admin-cli
'client_secret' => $this->adminClientSecret, // from env
]
Once Keycloak is correctly configured, the method will return a valid bearer token and the rest of the methods (users/groups/clients/roles) will work.
Post-fix validation checklist
getUserByEmail()
returns 200 and a user array (or empty list).- Creating a group returns 201 (or 204/409 with your fallback logic).
- Mapping client roles to groups succeeds with 201/204.
- Logs show:
[KC] getAdminToken OK
[KC] ... OK
for subsequent API calls.
Least-privilege reference
If your code needs to:
- Read & create users →
view-users
,manage-users
- Add to groups / create groups →
view-groups
,manage-groups
- Create client roles / composites →
view-clients
,manage-clients
,manage-roles
- General realm reads →
view-realm
For production, avoid
realm-admin
if possible; use it only in dev/test or during bootstrap.
Optional fallback (only if you can’t use service accounts)
You can implement a password grant fallback using Keycloak’s built-in admin-cli
with an admin username/password (less secure; not recommended long-term). Example:
KEYCLOAK_ADMIN_USERNAME=<kc-admin-user>
KEYCLOAK_ADMIN_PASSWORD=<kc-admin-pass>
In getAdminToken()
, if client_credentials fails, attempt:
grant_type=password
client_id=admin-cli
username=<user>
password=<pass>
Prefer fixing service accounts; use password grant only as a temporary workaround.
Troubleshooting tips
- Wrong realm: Token URL must match the realm you’re targeting (here
wizbrand
). - Clock skew: Ensure server time is correct (NTP); skew can cause auth issues.
- Env not loading: Make sure
.env
is read by the running PHP-FPM/queue workers; restart if needed. - Network/proxy: Confirm your app can reach
auth.holidaylandmark.com
over HTTPS. - 403 after token: You have a token, but missing realm-management roles; add the specific permission tied to the failing API.
Executive summary you can paste in Jira/README
- Issue: 401
unauthorized_client
— Client not enabled to retrieve service account when calling the token endpoint withclient_credentials
. - Root cause: The Keycloak client used for admin API calls was not a confidential client with Service Accounts enabled.
- Fix: In Keycloak, make
wizbrand-admin-cli
confidential, enable Service Accounts, and grant the service account realm-management roles (view/manage-users
,view/manage-groups
,view/manage-clients
,manage-roles
,view-realm
as needed). Update.env
with the client secret. - Verification: cURL to the token endpoint returns
access_token
; Laravel logs show[KC] getAdminToken OK
; subsequent admin operations succeed.
Leave a Reply