This PR hardens the native token + websocket path that shipped in the initial native API work.
- Store only SHA-256 token hashes in
native_tokens(raw bearer tokens are never persisted). - Add token metadata (
last_used_at,user_agent,created_ip) and updatelast_used_atatomically on successful auth. - Add migration
046_native_token_metadata.sqlto append metadata columns and invalidate pre-hash tokens (TRUNCATE native_tokens). - Add periodic cleanup task in
late-sshstartup to purge expired native tokens every hour. - Add per-IP rate limiting for native challenge, token issuance, and websocket connect paths.
- Add one-time short-lived websocket tickets (
GET /api/native/ws-ticket) and support ticket-first auth in/api/ws/native. - Add native logout endpoint (
DELETE /api/native/logout) to revoke current bearer token. - Enforce room membership checks for native history reads and websocket room subscription changes.
Native tokens are now write-only secrets from client perspective:
- Server generates raw token and returns it once.
- Server hashes token with SHA-256 and stores only hex digest.
- Auth lookups hash incoming token before DB query.
- Successful auth updates
last_used_at. - Expired tokens are removed by scheduled purge job.
This reduces impact of DB leaks and improves auditability for active token usage.
Native endpoints now use dedicated IP limiters:
- challenge issuance (
/api/native/challenge) - token minting (
/api/native/token) - websocket connect (
/api/ws/native)
Client IP derivation reuses existing trusted-proxy logic so limits apply to real client IP when requests pass through approved proxies.
/api/ws/native now prefers ephemeral one-time tickets over long-lived bearer token query params:
- Authenticated client requests ticket from
GET /api/native/ws-ticket. - Server mints ticket valid for 30 seconds, single-use.
- WS connect consumes ticket; replay fails.
- Bearer token auth remains as fallback via
Authorizationheader or query param for compatibility.
This limits token exposure in logs/URLs for clients that adopt ticket flow.
GET /api/native/rooms/{room}/historynow returns403unless caller is room member.- Native websocket
subscribenow switches rooms only if caller is member of requested room.
These checks close cross-room data access gaps.
- Existing rows in
native_tokensare intentionally invalidated by migration (TRUNCATE native_tokens) because old records contain unhashed raw token values that cannot match new lookup semantics. - Clients must re-authenticate and receive fresh tokens after deploy.
Initial native API implementation proved feature path. This follow-up focuses on production hardening: secret-at-rest protection, abuse throttling, revocation support, reduced token leakage during websocket auth, stronger room-level authorization, and better token observability.
- Ran
cargo check -p late-ssh.