[← 08 — The one-time-token fallback](08-one-time-token-fallback.md) · [index](README.md) · next: [10 — The signature counter](10-signature-counter.md) # Persistence Two repositories, and the default for both is a `HashMap`. | interface | default | JDBC implementation | |---|---|---| | `PublicKeyCredentialUserEntityRepository` | `MapPublicKeyCredentialUserEntityRepository` | `JdbcPublicKeyCredentialUserEntityRepository` | | `UserCredentialRepository` | `MapUserCredentialRepository` | `JdbcUserCredentialRepository` | `WebAuthnConfigurer.configure` constructs the map-backed ones if no beans exist, without logging anything. In a passwordless application that is a trap with teeth: restart the process and every user's only credential is gone, and by design they have no password to fall back on. ## Switching to JDBC ```java @Bean PublicKeyCredentialUserEntityRepository userEntityRepository(JdbcOperations jdbc) { return new JdbcPublicKeyCredentialUserEntityRepository(jdbc); } @Bean UserCredentialRepository userCredentialRepository(JdbcOperations jdbc) { return new JdbcUserCredentialRepository(jdbc); } ``` Nothing creates the tables. The Javadoc points at `classpath:org/springframework/security/user-credentials-schema.sql`, and searching `spring-security-webauthn-7.1.1.jar` for it finds nothing — the two DDL files stayed in `spring-security-web` when the classes moved out in 7.0 ([01](01-versions.md)). They are still on the classpath transitively, so this works: ```yaml spring: sql: init: mode: always schema-locations: - classpath:org/springframework/security/user-entities-schema.sql - classpath:org/springframework/security/user-credentials-schema.sql ``` There is a Postgres variant, `user-credentials-schema-postgres.sql`, which differs only in `bytea` versus `blob`. There is no Postgres variant of the user entities schema, because it does not need one. Run it with `./scripts/run.sh jdbc`; the ceremony transcript is [`docs/output/pk-jdbc.txt`](../output/pk-jdbc.txt) and is identical to the in-memory one, which is the point. ## What a credential record holds ``` credential_id the raw id, base64url user_entity_user_id the random user handle, not the username public_key the COSE key, as CBOR bytes signature_count updated on every assertion - see chapter 10 uv_initialized whether UV was set at registration backup_eligible / backup_state whether this is a syncable passkey, and whether it is synced authenticator_transports internal, hybrid, usb, nfc, ble attestation_object the full attestation object from registration attestation_client_data_json the client data from registration created / last_used timestamps label whatever the user typed ``` `attestation_object` is nullable in the DDL and mandatory in practice. `Webauthn4JRelyingPartyOperations.authenticate` re-parses it on every login to recover the COSE key: ```java Bytes attestationObject = credentialRecord.getAttestationObject(); Assert.notNull(attestationObject, "attestationObject cannot be null"); AttestationObject wa4jAttestationObject = cborConverter.readValue(attestationObject.getBytes(), ...); ``` So a custom `UserCredentialRepository` that stores the public key but drops the attestation object — a reasonable-looking optimisation, since the public key is right there in its own column — produces credentials that register fine and can never log in. The symptom is a bare 401. That re-parse is also the mechanism behind [chapter 10](10-signature-counter.md). ## Things to decide before you ship - **Deleting a credential.** `DELETE /webauthn/register/{id}` exists and is guarded by `CredentialRecordOwnerAuthorizationManager`, so users cannot delete each other's passkeys. Make sure your UI exposes it, and make sure a user cannot delete their last one. - **Labels.** The label is user-supplied and displayed back; treat it as untrusted, and cap its length. The column is `varchar(1000)`. - **Session storage.** The challenge lives in the `HttpSession` between the options call and the ceremony call. Behind a load balancer without sticky sessions, registration fails intermittently and nothing else does. Spring Session with Redis has its own open issue — [spring-security#16328](https://github.com/spring-projects/spring-security/issues/16328). - **Backups.** In a passwordless application the credential table *is* the account. Losing it is not a data-loss incident, it is a lockout incident for every user at once. [← 08 — The one-time-token fallback](08-one-time-token-fallback.md) · [index](README.md) · next: [10 — The signature counter](10-signature-counter.md)