validated('email'))->first(); if (! $user || ! Hash::check($request->validated('password'), $user->password)) { Log::warning('Failed login attempt', [ 'email' => $request->validated('email'), 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), ]); return $this->unauthorized('Invalid credentials'); } // MFA enabled and confirmed — check trusted device or require MFA if ($user->mfa_enabled && $user->mfa_confirmed_at) { $fingerprint = $request->header('X-Device-Fingerprint'); if ($fingerprint && $this->mfaService->isDeviceTrusted($user, $fingerprint)) { return $this->issueToken($user, $request); } // Revoke ALL existing tokens so old sessions cannot bypass MFA. // The only way to get a new token is through MfaVerifyController // after a successful code verification. $user->tokens()->delete(); $mfaSession = $this->mfaService->createMfaSession($user, $request->ip()); // Auto-send email code if email is the preferred method if ($user->mfa_method === MfaMethod::EMAIL->value) { try { $this->mfaService->sendEmailCode($user); } catch (\DomainException) { // Rate limited — code was already sent recently } } // Return MFA challenge — NO auth token, NO auth cookie. // Expire the auth cookie to invalidate any stale browser session. return response()->json([ 'success' => true, 'mfa_required' => true, ...$mfaSession, ])->withCookie($this->forgetAuthCookie()); } // MFA required by policy but not yet set up — issue token with flag if ($this->mfaService->isMfaRequired($user) && ! $user->mfa_enabled) { $response = $this->issueToken($user, $request); $data = $response->getData(true); $data['mfa_setup_required'] = true; $token = $user->createToken('auth-token')->plainTextToken; return response()->json($data) ->withCookie($this->makeAuthCookie($token)); } // No MFA — issue token as normal return $this->issueToken($user, $request); } private function issueToken(User $user, LoginRequest $request): JsonResponse { $user->load([ 'organisations', 'roles', 'permissions', 'persons' => fn ($q) => $q->with(['event:id,name,slug,start_date,end_date,organisation_id', 'event.organisation:id,name']), ]); $token = $user->createToken('auth-token')->plainTextToken; return $this->success([ 'user' => new MeResource($user), ], 'Login successful') ->withCookie($this->makeAuthCookie($token)); } }