1
0

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:
2026-08-25 22:54:40 +05:30
parent e9381dc5be
commit f6dd692177
59 changed files with 3567 additions and 4 deletions

109
README.md
View File

@@ -1,18 +1,20 @@
# spring-auth-demo
Runnable companion code for three articles on [ankurm.com](https://ankurm.com):
Runnable companion code for four articles on [ankurm.com](https://ankurm.com):
| | article | code |
|---|---|---|
| 1 | [Spring Security 7.1 JWT Authentication: The Complete Guide (Spring Boot 4.1)](https://ankurm.com/spring-security-7-1-jwt-authentication-guide/) | [`jwt-authentication/`](jwt-authentication) |
| 2 | [Spring Security OAuth2 Resource Server: JWT Validation, JWKS and Key Rotation](https://ankurm.com/spring-security-oauth2-resource-server-jwks-key-rotation/) | [`oauth2-resource-server/`](oauth2-resource-server) |
| 3 | [Spring Authorization Server: Running Your Own OAuth2 / OIDC Provider](https://ankurm.com/spring-authorization-server-oauth2-oidc-provider/) | [`authorization-server/`](authorization-server) |
| 4 | [Passkeys and WebAuthn with Spring Security 7](https://ankurm.com/passkeys-webauthn-spring-security-7/) | [`passkeys/`](passkeys) |
Three Maven projects, one shared [`docs/`](docs) tree. The first mints and verifies its own
Four Maven projects, one shared [`docs/`](docs) tree. The first mints and verifies its own
tokens with a hand-written filter. The second verifies tokens minted by somebody else —
a real Keycloak, and a stub issuer whose signing keys can be rotated on command. The third
*is* the somebody else: a real OAuth2 / OIDC provider, with a client and a resource server
in front of it.
in front of it. The fourth gets rid of the password entirely, and runs both WebAuthn
ceremonies with no browser and no hardware key.
> This repository was called `jwt-auth-demo` until the third project landed. Gitea keeps the
> old URL redirecting, but please update any bookmarks to `spring-auth-demo`.
@@ -32,6 +34,8 @@ real program output, regenerated by a script — not transcribed by hand.
| Spring Authorization Server | **7.1.1** — the same artifact, now versioned with Spring Security |
| Keycloak | **26.7.2** (resource server project only) |
| Caffeine | **3.2.4** (resource server project only) |
| WebAuthn4J | **0.31.9.RELEASE** (passkeys project only) |
| H2 | **2.4.240** (passkeys project only) |
---
@@ -78,6 +82,16 @@ cd spring-auth-demo/authorization-server
./scripts/authcode-pkce.sh
```
### Project 4 — passkeys, with no browser and no hardware key
```bash
cd spring-auth-demo/passkeys
./scripts/run.sh # http://localhost:8080/login, user/password
./scripts/ceremony.sh # registration and authentication, end to end
./scripts/counter.sh # a replayed signature counter, accepted
```
---
## Project 1 — `jwt-authentication/`
@@ -249,9 +263,55 @@ Regenerate its captured output with `./authorization-server/scripts/run-all.sh`
---
## Project 4 — `passkeys/`
One application on **:8080**, with `rpId` `localhost` — the only host browsers treat as a
secure context without TLS.
Two users: `user` / `password` and `admin` / `password`. Both start with a password, because
[a passkey cannot be a user's first credential](docs/passkeys/06-the-bootstrap-problem.md).
The interesting part is that this module needs no browser.
[`VirtualAuthenticator`](passkeys/src/main/java/com/ankurm/passkeys/virtual/VirtualAuthenticator.java)
is a software authenticator that emits genuine CBOR attestation objects and genuine ES256
assertion signatures; [`tools/PasskeyCeremony.java`](passkeys/tools/PasskeyCeremony.java)
drives the real HTTP endpoints with it, CSRF tokens and cookie jar included.
### Profiles
| profile | what it changes |
|---|---|
| *(none)* | `rpId` `localhost`, user verification `preferred`, credentials in memory |
| `uvrequired` | `UserVerificationRequirement.REQUIRED` on **both** ceremonies |
| `attestationdirect` | asks for `DIRECT` attestation, and accepts `none` anyway |
| `badorigin` | the relying party expects an origin the client will not send |
| `jdbc` | H2, using the DDL that ships inside `spring-security-web` |
| `trace` | `DEBUG` for Spring Security and WebAuthn4J — the only place failures are visible |
### Endpoints
| method | path | rule | why it exists |
|---|---|---|---|
| `POST` | `/webauthn/register/options` | authenticated | issues the creation options; **400** if nobody is logged in |
| `POST` | `/webauthn/register` | authenticated | verifies the attestation; **500** on any failure |
| `DELETE` | `/webauthn/register/{id}` | owner only | guarded by `CredentialRecordOwnerAuthorizationManager` |
| `POST` | `/webauthn/authenticate/options` | `permitAll()` | issues the request options |
| `POST` | `/login/webauthn` | `permitAll()` | verifies the assertion; a bare **401** on any failure |
| `POST` | `/ott/generate` | `permitAll()` | the magic-link fallback. No rate limit ships with it |
| `POST` | `/login/ott` | `permitAll()` | redeems a one-time token, once |
| `GET` | `/me` | authenticated | prints which factor the session actually carries |
| `GET` | `/passkey-only` | `hasAuthority('FACTOR_WEBAUTHN')` | step-up: a magic-link session gets redirected, not admitted |
| `GET` | `/diag/credentials` | authenticated | **the stored `CredentialRecord`. Delete before shipping** |
| `GET` | `/diag/filters` | authenticated | the live filter chain |
Regenerate its captured output with `./passkeys/scripts/run-all.sh` (no Docker; a few minutes).
---
## Documentation
One numbered trail across the first two projects, plus a separate set for the third. Start at
One numbered trail across the first two projects, plus a separate set for the third and the
fourth. Start at
[`docs/01-architecture.md`](docs/01-architecture.md).
| doc | covers |
@@ -293,6 +353,25 @@ A separate chapter set, indexed at
| [09 — Entry point and the Accept header](docs/authorization-server/09-entry-point.md) | why the token endpoint 302s to a login page |
| [10 — Should you run one at all](docs/authorization-server/10-should-you.md) | the honest answer |
### Project 4 — passkeys and WebAuthn
A separate chapter set, indexed at
[`docs/passkeys/`](docs/passkeys/README.md).
| doc | covers |
|---|---|
| [01 — Versions, artifacts and the 7.0 split](docs/passkeys/01-versions.md) | the dependency `spring-boot-starter-security` does not give you |
| [02 — The minimum configuration](docs/passkeys/02-minimum-configuration.md) | six endpoints from one DSL block, and the bean that silently disables it |
| [03 — The two ceremonies](docs/passkeys/03-the-two-ceremonies.md) | what is on the wire, and what every default in the options object means |
| [04 — A software authenticator](docs/passkeys/04-virtual-authenticator.md) | how to execute a passkey ceremony in CI, with no browser |
| [05 — The defaults](docs/passkeys/05-defaults.md) | user verification is optional, and asking for attestation is not checking it |
| [06 — The bootstrap problem](docs/passkeys/06-the-bootstrap-problem.md) | a passkey cannot be a user's first credential |
| [07 — Failure modes](docs/passkeys/07-failure-modes.md) | why registration failures are 500s and login failures are bare 401s |
| [08 — The one-time-token fallback](docs/passkeys/08-one-time-token-fallback.md) | the way in, the way back, and the rate limit that does not exist |
| [09 — Persistence](docs/passkeys/09-persistence.md) | the in-memory default, the missing DDL, and the column you must not drop |
| [10 — The signature counter](docs/passkeys/10-signature-counter.md) | stored on every login, compared against on none |
| [11 — Should you build this](docs/passkeys/11-should-you.md) | the honest answer, and what the afternoon actually costs |
---
## Captured output
@@ -345,6 +424,23 @@ opening first:
| [`as-client-credentials-opaque.txt`](docs/output/as-client-credentials-opaque.txt) | a reference token, and what introspection returns for it |
| [`as-test-run.txt`](docs/output/as-test-run.txt) | 7 contract tests |
### Project 4
Indexed in full at [`docs/passkeys/README.md`](docs/passkeys/README.md). The ones worth
opening first:
| file | what it shows |
|---|---|
| [`pk-ceremony.txt`](docs/output/pk-ceremony.txt) | both WebAuthn ceremonies, end to end, no browser |
| [`pk-counter.txt`](docs/output/pk-counter.txt) | a signature counter of 1 accepted after the server stored 3 |
| [`pk-user-verification.txt`](docs/output/pk-user-verification.txt) | `uvInitialized: false`, and a successful login anyway |
| [`pk-attestation.txt`](docs/output/pk-attestation.txt) | `"attestation":"direct"` requested, `fmt: "none"` accepted |
| [`pk-origin.txt`](docs/output/pk-origin.txt) | `BadOriginException`, and the 500 and 401 it produces |
| [`pk-step-up.txt`](docs/output/pk-step-up.txt) | `?factor.type=webauthn&factor.reason=missing` |
| [`pk-bootstrap.txt`](docs/output/pk-bootstrap.txt) | a 400 from `/webauthn/register/options` with no session |
| [`pk-filters.txt`](docs/output/pk-filters.txt) | all 25 filters, and where the WebAuthn four land |
| [`pk-test-run.txt`](docs/output/pk-test-run.txt) | 7 contract tests |
---
## Security note
@@ -366,6 +462,11 @@ signing key is generated fresh on every boot, and its users are hard-coded. Read
[docs/authorization-server/10-should-you.md](docs/authorization-server/10-should-you.md)
before taking any of it near production.
The passkeys project's `/diag/credentials` prints credential ids and user handles to any
authenticated caller, and `ConsoleOneTimeTokenHandler` writes live one-time tokens to a file
in `/tmp` so the scripts can read them. Both are demo affordances. Delete them, and read
[docs/passkeys/11-should-you.md](docs/passkeys/11-should-you.md) first.
## License
MIT.