Securing Keycloak public clients: require PKCE and turn off the implicit flow
Single-page and mobile apps cannot keep a client secret. Here is how to protect them in Keycloak with PKCE, and which old flows to turn off.
Browser apps and mobile apps are public clients: anything shipped to a user's device can be read, so they cannot keep a client secret. Keycloak protects their logins with PKCE, but only requires it if you tell it to. Older setups also tend to have the implicit flow and password grant switched on.
What realmlint reports
MEDIUM Public client does not require PKCE [pkce-not-enforced] - client "web-spa": PKCE is not required MEDIUM Implicit flow is enabled [implicit-flow-enabled] - client "web-spa": implicit flow is enabled MEDIUM Password grant is enabled [direct-access-grants] - client "web-spa": direct access grants are enabled on a public client
Keycloak's own clients, such as account-console and admin-cli, are not reported. Their defaults are Keycloak's to manage.
Why it matters
- Without PKCE, an authorization code intercepted on its way back to the app, for example by another app registered for the same mobile URL scheme or through a leaky redirect, can be exchanged for tokens by whoever holds it. With PKCE the app proves it started the login, so a stolen code is useless.
- The implicit flow returns tokens directly in the browser address bar, where they can leak through history, logs and the Referer header. OAuth 2.1 drops it.
- Direct access grants let an app collect the user's password itself and send it to Keycloak, skipping the login page, second factors and brute force protection. OAuth 2.1 drops this too.
Fix it in the admin console
- Open the realm and go to Clients, then select the browser or mobile client.
- On the Settings tab, find Capability config.
- Check that Client authentication is off. That is what makes it a public client.
- Keep Standard flow on, and turn off Implicit flow and Direct access grants.
- Turn on Require PKCE, then click Save.
In Keycloak releases before 26.6 the PKCE setting may be on the client's Advanced tab instead. Set it to S256, never plain.
Make sure the app sends PKCE
Once PKCE is required, Keycloak rejects logins that do not include a code challenge, so check the app before you save in production. Current OIDC libraries for browsers and mobile, including keycloak-js, oidc-client-ts and AppAuth, support PKCE with S256. In keycloak-js, pass pkceMethod: "S256" to init() if your version does not use it by default. If the app relied on the implicit flow, switch it to the authorization code flow at the same time.
Fix it in a realm file
{
"clientId": "web-spa",
"publicClient": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"attributes": {
"pkce.code.challenge.method": "S256"
}
}
Check it worked
kc.sh export --realm myrealm --file myrealm.json realmlint check myrealm.json
The three findings should be gone for that client. Then log in to the app once to confirm it sends PKCE.