Phase 14: Full WebAuthn support for passwordless passkey login and
hardware security key 2FA.
- go-webauthn/webauthn v0.16.1 dependency
- WebAuthnConfig with RPID/RPOrigin/DisplayName validation
- Migration 000009: webauthn_credentials table
- DB CRUD with ownership checks and admin operations
- internal/webauthn adapter: encrypt/decrypt at rest with AES-256-GCM
- REST: register begin/finish, login begin/finish, list, delete
- Web UI: profile enrollment, login passkey button, admin management
- gRPC: ListWebAuthnCredentials, RemoveWebAuthnCredential RPCs
- mciasdb: webauthn list/delete/reset subcommands
- OpenAPI: 6 new endpoints, WebAuthnCredentialInfo schema
- Policy: self-service enrollment rule, admin remove via wildcard
- Tests: DB CRUD, adapter round-trip, interface compliance
- Docs: ARCHITECTURE.md §22, PROJECT_PLAN.md Phase 14
Security: Credential IDs and public keys encrypted at rest with
AES-256-GCM via vault master key. Challenge ceremonies use 128-bit
nonces with 120s TTL in sync.Map. Sign counter validated on each
assertion to detect cloned authenticators. Password re-auth required
for registration (SEC-01 pattern). No credential material in API
responses or logs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Web UI admin password reset now enforces admin role
server-side (was cookie-auth + CSRF only; any logged-in
user could previously reset any account's password)
- Added self-service password change UI at GET/PUT /profile:
current_password + new_password + confirm_password;
server-side equality check; lockout + Argon2id verification;
revokes all other sessions on success
- password_change_form.html fragment and profile.html page
- Nav bar actor name now links to /profile
- policy: ActionChangePassword + default rule -7 allowing
human accounts to change their own password
- openapi.yaml: built-in rules count updated to -7
Migration recovery:
- mciasdb schema force --version N: new subcommand to clear
dirty migration state without running SQL (break-glass)
- schema subcommands bypass auto-migration on open so the
tool stays usable when the database is dirty
- Migrate(): shim no longer overrides schema_migrations
when it already has an entry; duplicate-column error on
the latest migration is force-cleaned and treated as
success (handles columns added outside the runner)
Security:
- Admin role is now validated in handleAdminResetPassword
before any DB access; non-admin receives 403
- handleSelfChangePassword follows identical lockout +
constant-time Argon2id path as the REST self-service
handler; current password required to prevent
token-theft account takeover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>