isset($parts['host']) ? ($parts['scheme'] ?? 'http').'://'.$parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : '') : null, 'path' => $parts['path'] ?? '/']; // Query strings may contain tokens: excluded. } public static function state(Request $request): ?array { if (! $request->hasSession()) { return null; } $session = $request->session(); $guard = Auth::guard(config('fortify.guard')); return ['session' => self::pseudonym($session->getId()), 'accepted_cookie_session' => self::pseudonym($request->cookies->get(config('session.cookie'))), 'pending_mfa_user' => $session->get('login.id'), 'stored_web_user' => $session->get($guard->getName()), 'authenticated_user' => $guard->id(), 'intended' => self::destination($session->get('url.intended')), 'handler' => get_class($session->getHandler())]; } public function handle(Request $request, Closure $next): Response { if (! app()->environment('testing') || (int) $request->getPort() !== 18097) { return $next($request); } $trace = bin2hex(random_bytes(8)); $request->attributes->set('qa_auth_trace', $trace); $names = []; foreach (explode(';', $request->headers->get('Cookie', '')) as $cookie) { $name = trim(explode('=', $cookie, 2)[0]); if ($name === 'XSRF-TOKEN' || str_ends_with($name, '_qa') || str_starts_with($name, 'investmoz_qa_')) { $names[$name] = ($names[$name] ?? 0) + 1; } } $response = $next($request); $state = self::state($request); $loginInputCheck = null; if ($request->isMethod('POST') && $request->is('login')) { $email = $request->input('email'); $password = $request->input('password'); $actor = User::find(18); $emailMatches = is_string($email) && $actor && $email === $actor->email; $loginInputCheck = ['expected_qa_account' => 18, 'email_exact_matches_qa18' => (bool) $emailMatches, 'password_matches_qa18' => $emailMatches && is_string($password) ? Hash::check($password, $actor->password) : null]; unset($email, $password, $actor); } $persisted = null; if ($request->hasSession()) { $raw = $request->session()->getHandler()->read($request->session()->getId()); $data = $raw ? json_decode($raw, true) : null; $guardKey = Auth::guard(config('fortify.guard'))->getName(); $persisted = ['readable' => is_array($data), 'stored_web_user' => is_array($data) ? ($data[$guardKey] ?? null) : null]; } $cookies = []; foreach ($response->headers->getCookies() as $cookie) { $cookies[] = ['name' => $cookie->getName(), 'domain' => $cookie->getDomain(), 'path' => $cookie->getPath(), 'secure' => $cookie->isSecure(), 'http_only' => $cookie->isHttpOnly(), 'same_site' => $cookie->getSameSite()]; } $middleware = $request->route() ? app('router')->gatherRouteMiddleware($request->route()) : []; $handlerDirectory = null; if ($request->hasSession() && $request->session()->getHandler() instanceof FileSessionHandler) { $property = new ReflectionProperty(FileSessionHandler::class, 'path'); $handlerDirectory = $property->getValue($request->session()->getHandler()); } $entry = ['at_utc' => gmdate('c'), 'trace' => $trace, 'method' => $request->method(), 'path' => '/'.$request->path(), 'origin' => $request->getSchemeAndHttpHost(), 'cookie_name_counts' => $names, 'inertia' => $request->headers->has('X-Inertia'), 'json_expected' => $request->expectsJson(), 'inertia_version_matches' => $request->headers->has('X-Inertia-Version') ? hash_equals((string) app(HandleInertiaRequests::class)->version($request), (string) $request->headers->get('X-Inertia-Version')) : null, 'qa_restored_account_rejected' => $request->attributes->get('qa_restored_account_rejected', false), 'login_input_check' => $loginInputCheck, // Booleans only: no submitted values or password fingerprints. 'mfa_input_check' => $request->attributes->get('qa_mfa_input_check'), 'loaded' => $request->attributes->get('qa_auth_loaded'), 'after' => $state, 'persisted' => $persisted, 'status' => $response->getStatusCode(), 'redirect' => self::destination($response->headers->get('Location') ?? $response->headers->get('X-Inertia-Location')), 'set_cookie_attributes' => $cookies, 'default_guard' => config('auth.defaults.guard'), 'fortify_guard' => config('fortify.guard'), 'session_driver' => config('session.driver'), 'lifetime_minutes' => config('session.lifetime'), 'handler_directory_matches_config' => $handlerDirectory === config('session.files'), 'middleware' => $middleware, 'key_fingerprint' => hash('sha256', app('encrypter')->getKey()), 'qa_target' => config('database.connections.pgsql.database').':'.config('database.connections.pgsql.port'), 'config_cached' => app()->configurationIsCached()]; $file = storage_path('app/private/investmoz-v2/portal-audit/auth-session-trace-qa.jsonl'); // Preserve the original trace. A full file must not silently stop QA diagnostics. $segment = 0; while (is_file($file) && filesize($file) >= 5 * 1024 * 1024) { $segment++; $file = storage_path('app/private/investmoz-v2/portal-audit/auth-session-trace-qa-'.gmdate('Ymd').'-'.$segment.'.jsonl'); } file_put_contents($file, json_encode($entry, JSON_THROW_ON_ERROR).PHP_EOL, FILE_APPEND | LOCK_EX); $response->headers->set('X-Investmoz-QA-Auth-Trace', $trace); return $response; } } class PortalQaSessionTrace { public function handle(Request $request, Closure $next): Response { if ($request->attributes->has('qa_auth_trace')) { $request->attributes->set('qa_auth_loaded', PortalQaAuthTrace::state($request)); if ($request->isMethod('POST') && $request->is('two-factor-challenge') && (int) $request->session()->get('login.id') === 18) { // Diagnostic only: never consume a code, extend acceptance windows or authenticate. $code = $request->input('code'); $check = ['account_id' => 18, 'six_digits' => is_string($code) && preg_match('/^[0-9]{6}$/D', $code) === 1]; try { $actor = User::findOrFail(18); $secret = app('encrypter')->decrypt($actor->two_factor_secret); $check['secret_decrypts'] = is_string($secret); if ($check['six_digits'] && $check['secret_decrypts']) { $engine = new Google2FA; $window = config('fortify-options.two-factor-authentication.window'); if (is_int($window)) { $engine->setWindow($window); } $previous = app('cache.store')->get('fortify.2fa_codes.'.md5($code)); $check['matches_configured_time_window'] = $engine->verifyKey($secret, $code); $check['previously_used_in_cache'] = $previous !== null; $check['accepted_by_replay_check'] = $engine->verifyKeyNewer($secret, $code, $previous) !== false; $engine->setWindow(12); $check['matches_wider_diagnostic_window_only'] = ! $check['matches_configured_time_window'] && $engine->verifyKey($secret, $code); } } catch (DecryptException) { $check['secret_decrypts'] = false; } catch (Throwable) { $check['diagnostic_unavailable'] = true; } finally { unset($code, $actor, $secret, $engine, $previous); } $request->attributes->set('qa_mfa_input_check', $check); } } return $next($request); } }