1
0
Files
spring-auth-demo/docs/passkeys/05-defaults.md
Ankur Mhatre f6dd692177 Add passkeys project: WebAuthn ceremonies, a software authenticator and the one-time-token fallback
Fourth Maven project in the repository. Registration and authentication run end to
end with no browser and no hardware key: VirtualAuthenticator emits real CBOR
attestation objects and real ES256 assertion signatures, and tools/PasskeyCeremony.java
drives the live HTTP endpoints with them.

Profiles cover userVerification REQUIRED, DIRECT attestation, a disallowed origin and
JDBC persistence. Eleven doc chapters and twelve captured transcripts under docs/passkeys
and docs/output/pk-*.txt, all regenerated by passkeys/scripts/run-all.sh.
2026-08-25 23:00:27 +05:30

127 lines
5.7 KiB
Markdown

[← 04 — A software authenticator](04-virtual-authenticator.md) · [index](README.md) · next: [06 — The bootstrap problem](06-the-bootstrap-problem.md)
# The defaults, and what they do not check
Spring Security's passkey defaults are reasonable. They are also weaker than most people
assume, in ways that do not show up until you go looking.
## userVerification is `preferred`, which means optional
`Webauthn4JRelyingPartyOperations.createPublicKeyCredentialCreationOptions` builds:
```java
AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.PREFERRED)
.residentKey(ResidentKeyRequirement.REQUIRED)
.build();
```
and both `registerCredential` and `authenticate` decide whether to enforce UV like this:
```java
boolean userVerificationRequired = UserVerificationRequirement.REQUIRED
.equals(creationOptions.getAuthenticatorSelection().getUserVerification());
```
`PREFERRED` is not `REQUIRED`, so the flag is not checked. An authenticator that answers with
UV clear registers successfully and logs in successfully. From
[`docs/output/pk-user-verification.txt`](../output/pk-user-verification.txt):
```
$ POST /webauthn/register
HTTP 200
{"success":true}
$ GET /diag/credentials (note uvInitialized)
{"credentials":[{"label":"no-uv", ... "signatureCount":0,"uvInitialized":false, ...}]}
$ POST /login/webauthn
HTTP 200
{"authenticated":true,"redirectUrl":"/"}
```
`uvInitialized: false` is recorded faithfully — and then nothing consults it. What you
have is a single-factor credential: possession of the authenticator, with no proof that the
person holding it is the enrolled user. For most consumer sites that is an acceptable trade
(it is roughly what a password gives you, minus the phishing). For anything where the passkey
*is* the second factor, it is not, and the fix is one setting on both ceremonies:
```java
operations.setCustomizeCreationOptions((options) -> options.authenticatorSelection(
AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.REQUIRED)
.residentKey(ResidentKeyRequirement.REQUIRED)
.build()));
operations.setCustomizeRequestOptions((options) ->
options.userVerification(UserVerificationRequirement.REQUIRED));
```
Setting only the first leaves logins unverified forever, which is the worst of the three
outcomes because registration looks correct. The `uvrequired` profile in this module sets
both; with it, the same authenticator is refused:
```
com.webauthn4j.verifier.exception.UserNotVerifiedException:
Verifier is configured to check user verified, but UV flag in authenticatorData is not set.
```
`AuthenticatorSelectionCriteria.builder()` has no copy constructor, so overriding
`userVerification` means restating `residentKey` too. Forget it and you silently drop back to
a non-discoverable credential, and usernameless login stops working.
## Asking for attestation is not verifying attestation
The default is `AttestationConveyancePreference.NONE`. Switch it to `DIRECT` and the options
change:
```json
{"attestation":"direct","authenticatorSelection":{"residentKey":"required","userVerification":"preferred"}, ...}
```
Then register with an authenticator that answers `fmt: "none"` and an all-zero AAGUID. From
[`docs/output/pk-attestation.txt`](../output/pk-attestation.txt):
```
$ POST /webauthn/register
HTTP 200
{"success":true}
```
The reason is the manager. `Webauthn4JRelyingPartyOperations` initialises with:
```java
private WebAuthnManager webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager();
```
and that factory installs `NullFIDOU2FAttestationStatementVerifier`,
`NullPackedAttestationStatementVerifier`, `NullTPMAttestationStatementVerifier`,
`NullAndroidKeyAttestationStatementVerifier`, `NullAndroidSafetyNetAttestationStatementVerifier`,
`NullAppleAnonymousAttestationStatementVerifier`, a `NullCertPathTrustworthinessVerifier` and a
`NullSelfAttestationTrustworthinessVerifier`. Everything that could check an attestation
statement is a null object.
WebAuthn4J 0.31.9 does not ship a strict counterpart. `WebAuthnManager` has exactly two static
factories, `createNonStrictWebAuthnManager()` and its `ObjectConverter` overload; a strict
manager has to be assembled from real verifiers and a `TrustAnchorRepository` by hand, and
then `setWebAuthnManager` on the operations bean. That is a real project, and it is only worth
starting if you have an enterprise reason to care which model of key your users hold. For a
consumer product, attestation is usually the wrong thing to spend effort on — but you
should know that the `direct` in your options object is currently decorative.
## What *is* checked, by default, and correctly
Not everything is a caveat. Out of the box, all of these hold:
- **Origin.** A mismatch is rejected in both ceremonies. This is the phishing defence and it works.
- **Challenge.** Persisted in the `HttpSession` between the options call and the ceremony call, so a replayed or fabricated challenge fails.
- **rpId hash.** Compared against the configured relying party.
- **Signature.** Verified against the stored COSE key, with the algorithm the key declares.
- **User presence.** `userPresenceRequired` is hard-coded `true` at registration, per the specification.
- **Duplicate credential ids.** `registerCredential` rejects an id that already exists.
The unchecked list is short: user verification unless you ask for it, attestation unless you
build for it, and the signature counter — which gets its own chapter,
[10 — The signature counter](10-signature-counter.md).
[← 04 — A software authenticator](04-virtual-authenticator.md) · [index](README.md) · next: [06 — The bootstrap problem](06-the-bootstrap-problem.md)