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-usersview-groups,manage-groupsview-clients(andmanage-clientsif you create client roles)view-realm- If you create/manage client roles:
manage-roles(or userealm-adminfor 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] ... OKfor 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-adminif 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
.envis read by the running PHP-FPM/queue workers; restart if needed. - Network/proxy: Confirm your app can reach
auth.holidaylandmark.comover 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-cliconfidential, enable Service Accounts, and grant the service account realm-management roles (view/manage-users,view/manage-groups,view/manage-clients,manage-roles,view-realmas needed). Update.envwith 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