A small passkey-only application with an Angular client, Spring Boot API, Yubico's WebAuthn server library, jOOQ, Flyway, and PostgreSQL. Users can create an account, sign in with a discoverable passkey, and replace a lost passkey with a one-time recovery code.
The project originated as the companion code for this WebAuthn article.
This is demonstration code, not a complete identity product. In particular, the in-progress WebAuthn ceremonies are kept in one server process, so production deployments with multiple instances need a shared, one-time challenge store.
- Java 25
- Node.js and npm
- Docker with Docker Compose
- Optional: Task for the shortcuts in
Taskfile.yml
WebAuthn requires a secure context. Browsers treat localhost as secure for local
development; use HTTPS everywhere else.
Start PostgreSQL:
docker compose -f server/docker-compose.yml up -dIn one terminal, start the API:
cd server
./mvnw spring-boot:runOn Windows, use mvnw.cmd instead of ./mvnw.
In another terminal, start the client:
cd client
npm install
npm startOpen http://localhost:4200. The database is stored in the named Docker volume
server_wa_data, so docker compose down does not erase registered accounts.
Use docker compose -f server/docker-compose.yml down -v only when you explicitly
want to delete that data.
The recovery code is displayed once after registration. Saving it is the only way to replace a passkey when every registered authenticator is unavailable. Recovery invalidates the previous passkey and rotates the recovery code.
Run all configured checks:
task verifyOr run them directly:
cd client
npm run lint
npm run build
cd ../server
./mvnw testtask build builds the Angular client first and then packages it into
server/target/webauthn-demo.jar. Run the packaged application with:
java -jar server/target/webauthn-demo.jarThe local defaults live in server/src/main/resources/application.properties.
Override them through standard Spring Boot configuration (environment variables,
command-line arguments, or an external properties file). Production deployments
must set at least:
app.relying-party-idto the site's effective domainapp.relying-party-originsto the exact HTTPS origin or origins- the
spring.datasource.*settings for PostgreSQL - secure session-cookie settings appropriate for the TLS deployment
The relying-party ID and allowed origins must match the site where the browser executes the WebAuthn ceremony.
Registration and authentication both use two calls: a start call creates a
short-lived challenge and a finish call consumes it. Challenge identifiers are
single-use and expire after five minutes. Account creation and recovery updates
are transactional; a canceled new registration does not reserve its username.
Unsafe API calls are protected with Spring Security's SPA-oriented CSRF support;
Angular obtains the token during application startup and sends it automatically.
The application intentionally uses usernameless authentication, so passkeys can appear through browser autofill in the login username field.