Skip to content

M19.2: Auth hardening — graceful 419/session-expiry → login with return-to-page - #97

Merged
n8bar merged 9 commits into
mainfrom
claude/m19.2-auth-hardening
Jun 9, 2026
Merged

n8bar merged 9 commits into
mainfrom
claude/m19.2-auth-hardening

Conversation

@n8bar

@n8bar n8bar commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

MS19 Phase 2 (Auth/Password Policy Hardening). Replaces Laravel's raw 419 page with a graceful session-expiry → login flow that returns the user to the page they were on after re-auth. Spec: x19.2_AUTH_HARDENING.md.

What changed

  • Every 419 → session-expiry handler ($renderSessionExpired in bootstrap/app.php): full-page requests redirect to /login?expired; AJAX requests get a JSON redirect hint that a shared axios interceptor + fetch wrapper act on — one recovery story for both modalities.
  • Return-to-page: App\Support\ReturnToResolver resolves the target as _return_to (POST body) → Referer → dashboard, validated as a same-origin GET route, so a POST/PATCH/DELETE is never the target and the mutation is never replayed. A global submit-listener stamps _return_to onto every POST form. url.intended wins over first-login persona routing so users land back where they were.
  • GET-idle expiry: redirectGuestsTo sends returning users (those carrying the cz_returning marker cookie, dropped at login / cleared at logout) to /login?expired for the banner, while never-authenticated first visits get a plain /login.
  • Unsaved-input preservation: the two input-bearing AJAX spots (invoice delivery draft, payment note) stash their field value to localStorage across the bounce and restore it on return.
  • Expired banner rendered on /login only when ?expired.

Testing

  • SessionExpiryRedirectTest — 10 cases: form + AJAX 419, off-origin / POST-only fallbacks, guest-form, GET-idle marker behavior, login/logout cookie lifecycle, banner gate, return-to-page landing.
  • Full suite: 361 passing.
  • localStorage restore eyeball-verified in-browser (outside feature-test reach).

Also in this branch

🤖 Generated with Claude Code

n8bar and others added 8 commits June 6, 2026 14:56
Wait for mysqladmin ping before launching schedule:work and queue:work,
and switch their depends_on to condition: service_healthy, so a cold
`sail up` no longer crash-loops the workers before the DB is ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cope

Only errors/403.blade.php is branded today; 404/500/503/429 fall through
to Laravel defaults. Flag as candidate addition to the Visual Identity
Polish phase, to confirm at flesh-out. 419 excluded (Phase 2 handles it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ders

§1.4 = return-to-page (asymmetric recovery cost). Survey captured:
Blade+Alpine stack, form-POST vs AJAX modality split, existing partial
419 handler baseline. §2.2 return target via hidden _return_to field
(survives POST body when session dies) → Referer → dashboard, all
same-origin GET validated. §2.3 AJAX unifies to /login?expired with
scoped localStorage preserve for draft + payment-note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every 419 now routes through a session-expiry handler instead of falling
through to Laravel's default page. Full-page posts redirect to
/login?expired; AJAX gets a JSON redirect hint that a shared axios
interceptor + fetch wrapper act on — one recovery story for both.

Return target resolves _return_to (POST body, survives a dead session) →
Referer → dashboard, validated same-origin GET via ReturnToResolver, so
the mutation is never replayed. A global submit-listener stamps
_return_to onto every POST form. url.intended wins over first-login
persona routing so users land back where they were. Unsaved text in the
two input-bearing AJAX spots (invoice draft, payment note) is preserved
across the bounce via localStorage.

Open: GET-idle-expiry banner (§3.2 decision), localStorage eyeball (§4.6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET-idle expiry stays on Laravel's auth-middleware redirect (returns to
intended page, no expired banner); banner reserved for the 419 path.
Only §4.6 localStorage eyeball QA remains open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A GET to a protected page after idle expiry was redirecting to a plain
/login with no signal — an invisible logout. Now redirectGuestsTo sends
returning users (those carrying the cz_returning marker dropped at login,
forgotten at logout) to /login?expired so they get the banner, while
never-authenticated first visits still land on a plain /login. The marker
is a non-sensitive boolean, exempted from cookie encryption so it reads
as plain text. Return-to-page already worked here via Laravel's stashed
url.intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§4.7 localStorage restore eyeball-verified. Rename strategy doc to
x19.2_, mark Phase 2 done in the milestone rollup + exit criteria.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§4.7 localStorage restore eyeball-verified; doc Status → Complete; Phase 2
checked off in the milestone rollup, Current Focus, and exit criteria.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 113e69cc5b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compose.yaml
image: 'sail-8.4/app'
command: >
sh -c "php artisan schedule:work"
sh -c "until mysqladmin ping -hmysql -usail -ppassword --silent; do echo 'Waiting for MySQL for scheduler...'; sleep 2; done; echo 'MySQL is ready for scheduler.'; exec php artisan schedule:work"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use configured DB credentials in wait loops

When DB_PASSWORD is anything other than the literal password—including the checked-in .env.example, where it is empty—this new wait loop never exits even after depends_on: condition: service_healthy passes, so the scheduler stays stuck instead of starting; the queue worker has the same hard-coded -ppassword on line 55. The mysqladmin docs define -p[password] as the password value sent to the server, so this needs to use the Compose-provided DB password (or skip-password for the empty-password case) rather than a fixed value.

Useful? React with 👍 / 👎.

CyberCreek LLC (parent) Articles filed 2026-06-08. Rework Phase 3 to form
the parent first, then CryptoZing LLC with CyberCreek as sole member —
building the parent-subsidiary structure from the start to skip a later
membership transfer. Two formations / EINs / publications; operating
account stays in CryptoZing LLC's name. Timing watch-item: CryptoZing
name-lock vs MS21 (2026-07-15) — name-lock is Articles-approved, not full
publication, and parallel tracks insulate MS19, so not pre-slipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@n8bar
n8bar merged commit ab34274 into main Jun 9, 2026
1 check passed
@n8bar
n8bar deleted the claude/m19.2-auth-hardening branch August 20, 2026 03:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant