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.
5.3 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:
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.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
.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
(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"}
Related
docs/12-issuer-and-audience.md— the sameiss/audquestions from the resource server's sidedocs/05-hs256-vs-rs256.md— why the provider signs with RS256 here
Next: 04 — The consent page