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.
This commit is contained in:
100
docs/passkeys/02-minimum-configuration.md
Normal file
100
docs/passkeys/02-minimum-configuration.md
Normal file
@@ -0,0 +1,100 @@
|
||||
[← 01 — Versions](01-versions.md) · [index](README.md) · next: [03 — The two ceremonies](03-the-two-ceremonies.md)
|
||||
|
||||
# The minimum configuration
|
||||
|
||||
```java
|
||||
http
|
||||
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.webAuthn((webAuthn) -> webAuthn
|
||||
.rpId("localhost")
|
||||
.rpName("ankurm passkeys demo")
|
||||
.allowedOrigins("http://localhost:8080"));
|
||||
```
|
||||
|
||||
Plus a `UserDetailsService` bean. Without one,
|
||||
`WebAuthnConfigurer.configure` throws `IllegalStateException: Missing UserDetailsService Bean`
|
||||
at startup — passkeys authenticate a *credential*, and Spring still needs somewhere to
|
||||
look up the authorities that go with the username the credential resolves to.
|
||||
|
||||
That is the whole thing. It gives you six endpoints and a working browser flow:
|
||||
|
||||
| method | path | what it does |
|
||||
|---|---|---|
|
||||
| `POST` | `/webauthn/register/options` | issues a challenge and the creation options; requires an authenticated session |
|
||||
| `POST` | `/webauthn/register` | verifies the attestation and stores a `CredentialRecord` |
|
||||
| `DELETE` | `/webauthn/register/{id}` | removes a credential, guarded by `CredentialRecordOwnerAuthorizationManager` |
|
||||
| `GET` | `/webauthn/register` | the built-in registration page |
|
||||
| `POST` | `/webauthn/authenticate/options` | issues a challenge and the request options |
|
||||
| `POST` | `/login/webauthn` | verifies the assertion and creates the session |
|
||||
|
||||
## rpId and allowedOrigins are two settings, not one
|
||||
|
||||
The **relying party id** is a domain. It is hashed into authenticator data and it scopes the
|
||||
credential: a passkey created for `example.com` will be offered on `app.example.com`, because
|
||||
the rpId must equal the origin's effective domain or be a registrable suffix of it.
|
||||
|
||||
The **allowed origin** is the exact scheme, host and port string the browser puts in client
|
||||
data. `http://localhost:8080` and `http://127.0.0.1:8080` are different origins even though
|
||||
they reach the same server, and `localhost` is not a registrable suffix of `127.0.0.1`. This
|
||||
is the single most common way to get a flat 401 out of a configuration that looks correct.
|
||||
|
||||
`localhost` is special: browsers treat it as a secure context, so WebAuthn works over plain
|
||||
HTTP there and nowhere else. The first deployment to a real hostname needs TLS before the
|
||||
ceremony will start at all.
|
||||
|
||||
## The bean that silently disables the DSL
|
||||
|
||||
```java
|
||||
private WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations(
|
||||
PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) {
|
||||
Optional<WebAuthnRelyingPartyOperations> webauthnOperationsBean = getBeanOrNull(
|
||||
WebAuthnRelyingPartyOperations.class);
|
||||
String rpName = (this.rpName != null) ? this.rpName : this.rpId;
|
||||
return webauthnOperationsBean
|
||||
.orElseGet(() -> new Webauthn4JRelyingPartyOperations(userEntities, userCredentials,
|
||||
PublicKeyCredentialRpEntity.builder().id(this.rpId).name(rpName).build(), this.allowedOrigins));
|
||||
}
|
||||
```
|
||||
|
||||
— `WebAuthnConfigurer`, Spring Security 7.1.1
|
||||
|
||||
If a `WebAuthnRelyingPartyOperations` bean exists, it is used **as is**. The `rpId`, `rpName`
|
||||
and `allowedOrigins` you set on the DSL are never read. That is not a bug, but it is a
|
||||
silent one: the configuration still compiles, still starts, and still points at whatever the
|
||||
bean was constructed with.
|
||||
|
||||
[`RelyingPartyConfig`](../../passkeys/src/main/java/com/ankurm/passkeys/config/RelyingPartyConfig.java)
|
||||
in this module exposes exactly such a bean under three profiles, which is why it repeats the
|
||||
rpId and origin rather than sharing them with
|
||||
[`SecurityConfig`](../../passkeys/src/main/java/com/ankurm/passkeys/config/SecurityConfig.java).
|
||||
|
||||
## Where the filters land
|
||||
|
||||
From [`docs/output/pk-filters.txt`](../output/pk-filters.txt), on the real running chain:
|
||||
|
||||
```
|
||||
5 CsrfFilter
|
||||
7 GenerateOneTimeTokenFilter
|
||||
8 UsernamePasswordAuthenticationFilter
|
||||
9 OneTimeTokenAuthenticationFilter
|
||||
16 WebAuthnAuthenticationFilter
|
||||
20 ExceptionTranslationFilter
|
||||
21 PublicKeyCredentialCreationOptionsFilter
|
||||
22 PublicKeyCredentialRequestOptionsFilter
|
||||
23 AuthorizationFilter
|
||||
24 WebAuthnRegistrationFilter
|
||||
25 DefaultWebAuthnRegistrationPageGeneratingFilter
|
||||
```
|
||||
|
||||
Note where the line falls. The two options filters sit **before** `AuthorizationFilter`;
|
||||
`WebAuthnRegistrationFilter` sits **after** it. So `/webauthn/register/options` is answered
|
||||
without an authorization check, and enforces its own requirement that the caller be
|
||||
authenticated — which it does by throwing. See
|
||||
[06 — The bootstrap problem](06-the-bootstrap-problem.md).
|
||||
|
||||
`CsrfFilter` at position 5 applies to all of them. Every WebAuthn endpoint is a `POST` that
|
||||
changes server state (it stores a challenge), so every call needs a CSRF token. A front end
|
||||
that fetches options without one gets a 403 and no explanation.
|
||||
|
||||
[← 01 — Versions](01-versions.md) · [index](README.md) · next: [03 — The two ceremonies](03-the-two-ceremonies.md)
|
||||
Reference in New Issue
Block a user