Authentication Flow
Flow Diagram
graph TB
subgraph Entry
A["HTTP Request"] --> B["CustomSession MW\n(cookie namespace)"]
end
B --> C{Surface}
C -->|"/admin/*"| D["admin guard\nAdmin model\nbcrypt only"]
C -->|"web routes"| E["web guard\nUser model\nbcrypt + SHA256 fallback"]
C -->|"unauthenticated"| F["EnsureGuestUser\nJWT cookie\nutaten_store_guest_user"]
D --> D1["AdminLoginController\nAuth::guard('admin')->attempt()"]
D1 -->|success| D2["redirect admin.dashboard"]
D1 -->|fail| D3["back with errors"]
E --> E1["WebLoginController\nMembershipManager::verifyPassword()"]
E1 --> E2["PasswordService\nbcrypt Hash::check()"]
E2 -->|fail| E3["SHA256 fallback\n3000 iterations\nfixed salt"]
E3 -->|match| E4["live upgrade\nrehash to bcrypt\nCopyUserCart"]
E2 -->|match| E5["authenticated()\nCopyUserCart\nredirect"]
E4 --> E5
E5 --> G["verified middleware\nEnsureEmailIsVerified"]
G --> H["address_required\nchecks address->isComplete()"]
H --> I["Protected routes\nmypage, order, lottery_winner"]
F --> F1["JWT decode\nfirebase/php-jwt HS256\nsession('_guest_user')"]
F1 --> F2["CartItemPolicy\n?User nullable\nguest cart access"]
Purpose
Provides three separate identity contexts: authenticated web users, authenticated admins, and guest users. Keeps admin and user sessions isolated at the cookie level to prevent cross-contamination under Octane's persistent-process model.
Trigger Points
POST /login — web user login
POST /admin/login — admin login
- Any request to web routes — EnsureGuestUser issues/validates guest JWT
POST /register — registration + email verification trigger
Actors
User model (web guard, session-based)
Admin model (admin guard, session-based)
MembershipManager / PasswordService (credential verification)
CartManager (guest cart migration on login)
Lifecycle Steps
Web User Login
POST /login → Web\Membership\Auth\LoginController::login()
LoginController::attemptLogin() queries User where email + status = active
MembershipManager::verifyPassword() → PasswordService:
- Try
Hash::check() (bcrypt)
- On failure: SHA256 fallback with fixed salt
975061db6fe25eacee72ba2743fc9e7634d4, 3000 stretch iterations
- On SHA256 success: rehash with bcrypt and save (live upgrade)
authenticated(): CartManager::copyUserCart() (guest cart → user cart)
- Redirect to
session('last_product_url') or RouteServiceProvider::HOME
Admin Login
POST /admin/login → Admin\Auth\LoginController::login()
Auth::guard('admin')->attempt() — bcrypt only, no legacy fallback
- On success:
to_route('admin.dashboard'); sets auth.password_confirmed_at in session
Guest Identity
- Every request on
user middleware group → EnsureGuestUser
- If no cookie: generate
firebase/php-jwt HS256 token, store as utaten_store_guest_user cookie (HttpOnly, Secure, SameSite=Strict, 1-day TTL)
- Decoded guest ID stored in
session('_guest_user')
CartItemPolicy accepts ?User $user (nullable) — guest operations validated via session
Post-Login Guards
verified (custom EnsureEmailIsVerified): blocks if !hasVerifiedEmail() AND !isActive()
address_required: lazy-loads $user->address, redirects to address form if incomplete
Laravel Components Involved
| Component |
Path |
| Web LoginController |
app/Http/Controllers/Web/Membership/Auth/LoginController.php |
| Admin LoginController |
app/Http/Controllers/Admin/Auth/LoginController.php |
| MembershipManager |
app/Modules/Membership/MembershipManager.php |
| PasswordService |
app/Modules/Membership/Providers/PasswordService.php |
| CustomSession |
app/Http/Middleware/CustomSession.php |
| EnsureGuestUser |
app/Http/Middleware/EnsureGuestUser.php |
| AuthServiceProvider |
app/Providers/AuthServiceProvider.php |
| Auth config |
config/auth.php |
| Middleware Kernel |
app/Http/Kernel.php |
Data Mutations
- On SHA256 login:
users.password updated to bcrypt hash
- On login:
sessions table updated (Laravel session driver)
- On guest: cookie set (no DB write)
- On cart copy:
cart_items.user_id populated from cart_items.guest_id
State Transitions
guest (JWT only) → authenticated (session, verified, address complete) → protected routes
SHA256 password → bcrypt password (one-time upgrade on successful login)
Failure Paths
| Scenario |
Behaviour |
| Wrong password |
Back with validation errors, no account enumeration |
| Inactive user |
status != active query filter — login silently fails same as wrong password |
| JWT decode failure |
Silently swallowed (no logging) — new guest JWT issued |
| Email unverified |
Redirect to verification notice |
| Address incomplete |
Redirect to address form (address_required middleware) |
| Admin tries web login |
Different guard — no cross-contamination |
Security Considerations
- Legacy SHA256 path: fixed salt derived from config + plaintext password prefix. Risk: if
config/auth.php is exposed, all legacy hashes are crackable without per-user salt. Upgrade path active but relies on user logging in.
- Guest JWT secret: HS256 shared key — if
APP_KEY or JWT secret leaks, guest IDs can be forged for cart manipulation.
- JWT decode failures: silently ignored with no logging — failed attempts are invisible.
- LotteryCartItemPolicy: requires authenticated
User (not nullable) despite Cart having guest support — guest access to lottery cart will throw.
AddressRequired: lazy-loads $user->address on every authenticated route — extra query per request (N+1 if batched).
- Sanctum installed but
EnsureFrontendRequestsAreStateful is commented out — API routes unprotected by Sanctum middleware.
- SHA256 fallback: 3000 stretch iterations add ~5–10ms per legacy login
AddressRequired middleware: one extra SELECT on every authenticated web request
CartManager::copyUserCart() on login: batch update query, generally fast
Extension Points
PasswordService is bound via interface — swap legacy hash logic without touching controllers
EnsureGuestUser middleware is applied per-group — can be removed from specific route groups