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.
124 lines
5.3 KiB
Markdown
124 lines
5.3 KiB
Markdown
[← 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`
|
|
([`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)
|