1
0
Files
spring-auth-demo/docs/authorization-server/03-clients-and-pkce.md
Ankur Mhatre e9381dc5be Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.

Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:

  - OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
    in 7.0, and both configuration classes moved into spring-security-config
  - ClientSettings.requireProofKey flipped from false to true, on the authorization server
    (1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
  - requireProofKey(false) does not make PKCE optional for a public client; the code
    verifier is that client's only authentication at the token endpoint
  - MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
    API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called

Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
2026-08-24 08:20:38 +05:30

5.5 KiB

← 02 Minimum provider · index · next: 04 — The consent page

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. 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, output in 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.

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 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.txtrequireProofKey(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.txtrequireProofKey(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

.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). demo-web, identically registered but confidential, does get one (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"}

Next: 04 — The consent page