argument('id'); if (ctype_digit((string) $id)) { $user = User::withTrashed()->find($id); } else { $user = User::withTrashed()->whereUsername($id)->first(); } if (! $user) { $this->error('Could not find any user with that username or id.'); return 1; } $this->info('User: '.$user->username.' (id '.$user->id.')'); $this->info('Email on record: '.$user->email); $this->newLine(); // --- Login-path checks (login authenticates by EMAIL + password) --- $this->line('Login authenticates by EMAIL + password (LoginController defaults to the "email" field).'); $this->newLine(); // 1. Duplicate email — the guard matches the FIRST row with this email. $sameEmail = User::withTrashed()->where('email', $user->email)->get(); if ($sameEmail->count() > 1) { $this->error('DUPLICATE EMAIL: '.$sameEmail->count().' rows share "'.$user->email.'":'); foreach ($sameEmail as $row) { $this->line(' - id '.$row->id.' username='.$row->username.' deleted_at='.($row->deleted_at ?? 'null')); } $this->warn('The web login may match a different row than the one you expect.'); $this->newLine(); } else { $this->line('Email is unique in the users table. ✓'); } // 2. Which row would the web guard actually pick? (non-trashed, matching email) $guardRow = User::where('email', $user->email)->first(); if (! $guardRow) { $this->error('The web guard would find NO active (non-trashed) row for this email — login will always fail.'); } elseif ($guardRow->id !== $user->id) { $this->warn('The web guard would authenticate row id '.$guardRow->id.', NOT the row you looked up (id '.$user->id.').'); } else { $this->line('The web guard resolves to this same row. ✓'); } $this->newLine(); // 3. Stored hash sanity + algorithm/cost. $hash = $user->getAttributes()['password'] ?? null; if (empty($hash)) { $this->error('Stored password hash is EMPTY — cannot authenticate.'); return 1; } $info = password_get_info($hash); $algo = $info['algoName'] ?? 'unknown'; $this->line('Stored hash algorithm: '.$algo); if (isset($info['options']['cost'])) { $this->line('Stored hash cost: '.$info['options']['cost']); } if ($algo === 'unknown') { $this->error('Stored hash is not a recognizable bcrypt/argon hash — it may be corrupted or double-hashed.'); } $this->newLine(); // 4. Verify a candidate password against the stored hash. $candidate = $this->secret('Enter the password you are trying to log in with (hidden, not stored)'); if ($candidate === null || $candidate === '') { $this->comment('No password entered; skipping verification.'); return 0; } $matches = Hash::check($candidate, $hash); $this->newLine(); if ($matches) { $this->info('✓ PASSWORD MATCHES the stored hash for this row.'); $this->comment('If web login still fails with this exact password + email "'.$user->email.'", the problem is NOT the password:'); $this->line(' - check for a duplicate email row above,'); $this->line(' - confirm you are typing the email exactly (no trailing spaces),'); $this->line(' - confirm the app server and CLI share the same database & APP_KEY,'); $this->line(' - check for login throttling (5 failed attempts / 60 min lockout).'); } else { $this->error('✗ PASSWORD DOES NOT MATCH the stored hash for this row.'); $this->comment('Re-run: php artisan user:setpassword '.$user->username.' and set it again, then re-check here.'); $this->comment('If it still will not match after setting, the CLI and web app likely point at different databases.'); } // 5. Would Laravel want to rehash? (cost/driver mismatch signal) if ($matches && Hash::needsRehash($hash)) { $this->newLine(); $this->warn('Note: stored hash needsRehash() = true (cost/driver differs from current config). Login still works, but worth noting.'); } return $matches ? 0 : 1; } }