pk-auth is a production-grade, passkeys-first authentication template for the JVM. It ships as a reusable library set that drops into a Spring Boot, Dropwizard, or Micronaut application. The core is framework-neutral, and the host's user and credential storage is a plug-in SPI.
All modules share one version and the com.codeheadsystems group id.
- WebAuthn registration and assertion ceremonies, including multi-passkey enrolment and conditional UI, backed by WebAuthn4J.
- A stateless JWT mint at the end of authentication (HS256, configurable TTL). With the 1.1.0
AccessTokenStoreSPI, the mint is a stateful access token that is revocable before itsexp. - Per-audience JWT TTL dispatch via
TokenTtlPolicy, so web, cli, and mobile clients can carry different access-token lifetimes from a single issuer. - Rotating refresh tokens with family-based replay defence (1.1.0).
POST /auth/refreshis one ceremony and one row per rotation, with motif-style atomic mark-and-insert and family scorch on detected replay. The browser SDK'sPkAuthClient.refresh()returns a typed result sum and never throws on401. See ADR 0013. - Account admin: list, rename, and delete passkeys; regenerate view-once backup codes; account summary.
- Alternative-flow modules: backup codes, magic-link email verification, and phone OTP verification, each behind a host-implementable SPI.
- A framework-neutral admin service that every adapter mounts at the same paths, so the same TypeScript SDK drives all three.
- Persistence options: in-memory (testkit), JDBI + Postgres with Flyway migrations, or DynamoDB
single-table. The JDBI migrations ship as a required core set (
classpath:db/migration/core) and an opt-in exampleusersset (classpath:db/migration/example-users) used only withJdbiUserLookup; the operator guide gives the Flyway configuration for each. - A zero-dependency browser SDK (
@pk-auth/passkeys-browser) covering both ceremony and admin operations, published on npm (npm install @pk-auth/passkeys-browser). Its version tracks the pk-auth server release it speaks to.
flowchart LR
Browser["Browser SPA<br/>(@pk-auth/passkeys-browser SDK)"] -- HTTPS / JSON --> Adapter["pk-auth-{spring-boot,dropwizard,micronaut}<br/>mounts /auth/**"]
Adapter --> Core["pk-auth-core<br/>(framework-neutral)"]
Core --> Persistence["pk-auth-persistence-{jdbi,dynamodb}<br/>or testkit (in-memory)"]
Core --> Alt["backup-codes / magic-link / otp"]
Related documents:
- Architecture:
DESIGN.md. - Decision records:
docs/adr/. - Production operations:
docs/operator-guide.mdanddocs/threat-model.md. - SPI versioning and stability guarantees:
docs/stability.md. - Transactional behaviour across SPIs:
docs/transactional-semantics.md.
The Spring Boot demo is the fastest path to a working server:
./gradlew :examples:spring-boot-demo:runThe demo serves at http://localhost:8080, to be opened in a passkey-capable browser (Chrome, Edge,
Safari, Firefox 130+). The single-page UI exercises every flow:
- Registration of an account; the platform authenticator (Touch ID, Windows Hello, security key) handles the ceremony.
- Sign-in, which returns a JWT; the page decodes its claims at the bottom.
- Listing, renaming, and deleting passkeys; the demo enforces the last-credential guard.
- Regeneration of backup codes (view-once) and a remaining-count check.
- Email verification via magic link and phone verification via OTP.
Magic-link tokens and OTP codes are written to the server console (the testkit ships
LoggingEmailSender and LoggingSmsSender). Copying them from the Gradle log back into the form
completes the verification flows.
Two other demos exist for the other adapters, with the same UI over a different framework:
./gradlew :examples:dropwizard-demo:run # Jersey + Dropwizard 5
./gradlew :examples:micronaut-demo:run # Netty + Micronaut 4Only one demo can run at a time, because all three bind to port 8080.
pk-auth-core/ # framework-neutral ceremony engine + SPIs
pk-auth-jwt/ # HS256 JWT mint + validate, AccessTokenStore, TokenTtlPolicy
pk-auth-backup-codes/ # alt flow: Argon2id-hashed backup codes
pk-auth-magic-link/ # alt flow: email magic-link verification
pk-auth-otp/ # alt flow: phone OTP verification
pk-auth-refresh-tokens/ # rotating refresh tokens with family-based replay defense
pk-auth-admin-api/ # framework-neutral admin operations
pk-auth-persistence-jdbi/ # SPI impls on JDBI + Postgres + Flyway
pk-auth-persistence-dynamodb/ # SPI impls on AWS DynamoDB Enhanced
pk-auth-testkit/ # FakeAuthenticator + in-memory SPIs
pk-auth-spring-boot-starter/ # adapter: Spring Boot 4 / Spring Security 7
pk-auth-dropwizard/ # adapter: Dropwizard 5 Bundle + Dagger 2
pk-auth-micronaut/ # adapter: Micronaut 4 controllers + filter
clients/passkeys-browser/ # TypeScript SDK (ESM + CJS)
examples/ # runnable demos for each adapter
docs/ # ADRs, operator guide, threat model
./gradlew check # full lint + test + coverage gate
./gradlew clean build testRequirements:
- JDK 21. Gradle's toolchain fetches one if none is present.
- Node 22.22.2 or later, and npm, for the browser SDK build, which Gradle invokes automatically
(
./gradlew :buildPasskeysBrowserSdk).
Optional, needed only for the JDBI and DynamoDB persistence integration tests and the Playwright end-to-end suites:
- Docker. Testcontainers starts Postgres and DynamoDB Local.
- Chrome. Playwright drives a CDP virtual WebAuthn authenticator.
The 1.0.0 release cut the stable baseline. The current development line is the 2.x -SNAPSHOT
named by version in gradle.properties, which is the authoritative source. The 1.1 line added
per-audience JWT TTLs, the AccessTokenStore (stateful access tokens), the UserDeletionService
fan-out, and the pk-auth-refresh-tokens module (rotating refresh tokens with family-based replay
defence). Releases 1.2 and 1.3 followed with per-framework admin toggles and supply-chain
hardening.
See CHANGELOG.md for the full delta and
docs/stability.md for the versioning policy and the list of SPI surfaces.
The full build and end-to-end suites pass. The test classpath includes the testkit's
FakeAuthenticator, so registration and assertion ceremonies exercise the real WebAuthn4J verifier
without a browser.
pk-auth does not require host SPIs to share a transactional context. At ceremony finish,
ChallengeStore.takeOnce is consumed before CredentialRepository.save. If the save fails, the
user restarts the ceremony from the beginning, and a new challenge is issued. Challenges are
short-lived (5-minute default TTL), so a forced restart is acceptable, and it avoids
distributed-transaction complexity across heterogeneous SPI implementations. See
docs/transactional-semantics.md for full details.
MIT. See LICENSE.
For this project development, I played the role of product manager and architect, but let Claude write all the code. If you do not like AI generated code, then you will want to ignore this project.
This project is open-source and is not based on any novel ideas. It's a way to provide a standardized approach to your project authentication model when you want to have a passkeys-first approach and not write it from scratch. I outlined the security limitations and trade-offs in the docs directory, and I encourage you to read through those.
That said, I used competing agents and industry best practices tm to try to find security issues and bugs outside my own reviews. It is still early in AI development efforts so judge those 'best practices' with a grain of salt given how nascent it is developing with AI tools.
I will be verifying this project by using it elsewhere in my work. The description of this git repository indicates if I feel it is production worthy or not. I will also try to get legitimate security folks to review it; but unless there is a note that they have reviewed the project, assume it is not.
The AI-as-supply-chain analysis lives in
docs/threat-model.md → AI-authored code (provenance).]