Commit Graph

5 Commits (e02f4d036afb00e435876e475fea450956eb4c3b)

Author SHA1 Message Date
boban f82555469a style: pint --test fixes (CI pint step now reachable after the test fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:29:59 +02:00
boban 57a3dd51b6 feat(security): signed confirm-action tokens + airtight backup-code reveal
Two hardening features completed in parallel (each with its own spec + tests),
sharing call sites (TwoFactorSetup, WebauthnKeys, routes) so committed together:

- ConfirmAction token hardening: destructive confirm flows now carry a server-issued,
  single-use, signed token (App\Support\Confirm\ConfirmToken) instead of trusting a
  client-mutable event/params/auditTarget. Every #[On] apply handler consumes + validates
  the token (forged/replayed/direct-bypass calls no-op). Server-scoped per action; closes
  the codebase-wide confirm-bypass + audit-forgery vector across Security, Sessions, Users,
  WebauthnKeys, Servers\Show, Services, System, Files.
- Backup-code airtight reveal: codes are revealed via a transient channel, not a persisted
  Livewire property, so a captured/replayed snapshot can't re-render them; the recovery
  download is grant-gated.

Full suite green (162).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:35:33 +02:00
boban 9a6c09e488 fix(2fa): close two re-view holes in backup-codes show-once
Stored recovery codes were re-viewable two ways after their one-time
reveal. Both paths are now closed so codes are only viewable/downloadable
right after a fresh generation (enrollment or regenerate).

HOLE 1 — RecoveryCodes::$revealed was a mutable public Livewire prop, so
a crafted /livewire/update could flip it to true and make render() emit
stored codes. Annotated with #[Locked]; Livewire now rejects any
client-side write (set still happens server-side in mount()/regenerate()).

HOLE 2 — two-factor.recovery.download was auth-gated only, so any authed
user could GET it anytime to re-download stored codes. Added a one-time
session grant (2fa.download_grant) put alongside the existing
2fa.codes_fresh wherever codes are freshly generated (TwoFactorSetup,
WebauthnKeys, RecoveryCodes::regenerate); the route now
abort_unless(pull(grant), 403) before streaming — one download per fresh
generation, later direct hits 403.

Tests: lock rejection + download-grant lifecycle (forbidden → granted ok
→ consumed forbidden). Full suite 137 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:21:45 +02:00
boban dd891cd11e Reveal backup codes only when freshly generated
Backup (recovery) codes must be viewable ONLY at the moment they are
generated — at first-factor enrollment or on regenerate — and NEVER
re-viewable afterward from "Backup-Codes verwalten".

- Modals\RecoveryCodes: add server-gated $revealed. mount() pulls a
  one-time session flag (2fa.codes_fresh); regenerate() sets it. render()
  emits codes ONLY when revealed, so a client cannot tamper a flag to
  re-view stored codes.
- View: revealed state shows codes grid + download + warning + close;
  hidden state shows a security notice + Neu erzeugen + close (no codes,
  no download).
- TwoFactorSetup::confirm() and WebauthnKeys::register() put
  2fa.codes_fresh on first-factor code generation (put, survives the
  redirect + later openModal request).
- Add recovery_hidden_notice (DE/EN parity).
- Tests: manage view hides codes; fresh flag reveals once; regenerate
  reveals the new set; first-factor paths set the flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:14:59 +02:00
boban e9a6a6c083 feat(2fa): recovery codes become a modal; drop the dedicated page + route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 20:50:11 +02:00