[← 02 Minimum provider](02-minimum-provider.md) · [index](README.md) · next: [04 — The consent page](04-consent-page.md) # Clients, PKCE and the defaults that moved A `RegisteredClient` is a policy, not a credential. It states which grants a caller may use, which redirect URIs are acceptable, which scopes it may request, whether consent is required, whether PKCE is mandatory, and how long the tokens live. Most “works in Postman, not in the browser” reports are one of those fields. Source: [`RegisteredClientConfig.java`](../../authorization-server/auth-server/src/main/java/com/ankurm/authserver/config/RegisteredClientConfig.java). Three clients: | client | authentication | grants | demonstrates | |---|---|---|---| | `demo-web` | `client_secret_basic` | code + refresh | consent, refresh rotation | | `demo-spa` | `none` (public) | code + refresh | PKCE, and no refresh token | | `demo-service` | `client_secret_basic` | client credentials | opaque vs JWT tokens | ## The default that flipped `ClientSettings.builder().build()` run against both jars ([`tools/SettingsDefaults.java`](../../authorization-server/tools/SettingsDefaults.java), output in [`as-settings-defaults.txt`](../output/as-settings-defaults.txt)): ``` === Spring Authorization Server 1.5.8 === requireProofKey = false === Spring Authorization Server 7.1.1 === requireProofKey = true ``` **PKCE is now mandatory for every client you did not think about.** `demo-service` in this project never touches `ClientSettings`, and `/diag/clients` reports `"requireProofKey": true` for it. An authorization request with no `code_challenge` is rejected at the authorization endpoint, before login: ``` 302 http://127.0.0.1:8080/authorized ?error=invalid_request &error_description=OAuth%202.0%20Parameter%3A%20code_challenge &error_uri=…rfc7636%23section-4.4.1 ``` That is [`as-authcode-pkce-enforced.txt`](../output/as-authcode-pkce-enforced.txt). The client side moved in the same release. `ClientRegistration.ClientSettings.Builder` initialises `requireProofKey` to `false` in Spring Security 6.5.1 and to `true` in 7.1.1 (same output file). So Spring-client-to-Spring-server keeps working; what breaks is a 7.1 server in front of a 6.x client, a non-Spring client, or a saved Postman collection. See [08](08-client.md) for that failure end to end. ## `requireProofKey(false)` does not make PKCE optional for a public client Two separate experiments, both in `docs/output`: 1. [`as-authcode-nopkce.txt`](../output/as-authcode-nopkce.txt) — `requireProofKey(false)`, but the authorization request still carries a challenge. The token endpoint still demands the verifier. Sending a challenge and then omitting the verifier is never accepted. 2. [`as-authcode-nochallenge.txt`](../output/as-authcode-nochallenge.txt) — `requireProofKey(false)` and no challenge at all. The authorization endpoint issues a code, and the token exchange then fails with **401 and an empty body**. The second is the interesting one. `PublicClientAuthenticationProvider` delegates entirely to `CodeVerifierAuthenticator` and raises `invalid_client` when there is nothing to verify: ``` private final CodeVerifierAuthenticator codeVerifierAuthenticator; … // String invalid_client // String https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1 ``` For a client registered with `ClientAuthenticationMethod.NONE`, the code verifier *is* the client authentication. Turning `requireProofKey` off does not make PKCE optional; it makes the client unable to authenticate. The setting relaxes the authorization endpoint only. ## Client secrets are hashed ```java .clientSecret(encoder.encode("web-secret")) ``` Registering the bare string and then sending it produces `invalid_client` with no further detail, because the server bcrypt-compares the presented secret against what it believes is a hash. This is the most common first-hour failure and the error message is deliberately unhelpful. ## Redirect URIs are exact Scheme, host, port and path, byte for byte. No wildcards. A mismatch is rejected *before* login and rendered by the authorization server rather than sent to the client — by design, since redirecting to an unvalidated URI is the vulnerability. ## Public clients get no refresh token `demo-spa` is registered with `AuthorizationGrantType.REFRESH_TOKEN` and the token response contains no `refresh_token`. The gate is in `OAuth2RefreshTokenGenerator`, which returns `null` when the authenticated client's method is `ClientAuthenticationMethod.NONE` — not in `OAuth2AuthorizationCodeAuthenticationProvider`, which only checks that the client is registered for the grant ([`as-authcode-pkce.txt`](../output/as-authcode-pkce.txt)). `demo-web`, identically registered but confidential, does get one ([`as-authcode-web.txt`](../output/as-authcode-web.txt)). ## Refresh rotation `reuseRefreshTokens` defaults to `true` in both 1.5.8 and 7.1.1. `demo-web` sets it to `false`, and the transcript shows the old token dying on first use: ``` old refresh token: 1BYxixPcmy4PLVmNvkTIo-00... new refresh token: P-7KeaSx9alEBp5CFpDBt-c0... DIFFERENT - reuseRefreshTokens(false), the old one is now dead Replaying the old one: {"error":"invalid_grant"} ``` ## Related - [`docs/12-issuer-and-audience.md`](../12-issuer-and-audience.md) — the same `iss`/`aud` questions from the resource server's side - [`docs/05-hs256-vs-rs256.md`](../05-hs256-vs-rs256.md) — why the provider signs with RS256 here Next: [04 — The consent page](04-consent-page.md)