Getting Authentication
What the starter kits scaffold, the three tables authentication uses, and exactly what Auth::attempt checks.
Auth::attempt(array $credentials, bool $remember = false): boolAuthentication answers one question: who is making this request. Laravel's default answer is session based. A login form is posted, the credentials are checked against a database table, and the user's primary key is written into the session. Every later request reads the session cookie and loads that user again.
What the starter kits give you
Laravel 13 ships four starter kits: React, Svelte, Vue and Livewire. Running laravel new my-app prompts for one. All four use Laravel Fortify for the backend, which registers the routes for login, logout, registration, password reset, email verification, password confirmation and the two-factor challenge. php artisan route:list shows them, and config/fortify.php decides which of those features exist.
The tables
One migration, database/migrations/0001_01_01_000000_create_users_table.php, creates all three tables authentication touches: users, password_reset_tokens and sessions. Run php artisan migrate before your first login.
What attempt checks
Auth::attempt(['email' => $email, 'password' => $password]) hands the array to the guard's user provider, configured under providers in config/auth.php. The provider builds a query from every key except password and fetches one row. If a row comes back, the plain-text password is compared with the stored hash. You never hash the incoming value yourself. On a match the session is started and attempt returns true.
Reading the result has three spellings of the same call: Auth::user(), the auth()->user() helper and $request->user() all resolve the default guard named in config('auth.defaults.guard').
Example
<?php
// database/migrations/0001_01_01_000000_create_users_table.php
// This file ships with a new Laravel 13 application. All three tables
// that authentication reads live in it.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken(); // nullable string, 100 chars
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};When to use it
- Adding login to an application that has none, and deciding between a starter kit and writing it yourself
- Working out why Auth::attempt returns false when the email and password are definitely correct
- Checking that the users, sessions and password_reset_tokens tables exist before debugging a login
- Refusing suspended accounts by adding a column condition to the credentials array
More examples
Reaching the signed-in user
auth() with no argument returns the Illuminate\Contracts\Auth\Factory, so auth()->user() is Auth::user() with the facade spelled out; auth('admin')->user() is Auth::guard('admin')->user(). Reach for $request->user() inside a controller you already type-hint the request in, and the facade in code that has no request to hand, such as a model method.
<?php
// app/Http/Controllers/ProfileController.php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class ProfileController extends Controller
{
public function show(Request $request): View
{
// Three spellings, one guard. Pick one and stay with it.
$user = $request->user(); // the request's own user() method
$user = Auth::user(); // the Auth facade
$user = auth()->user(); // the auth() helper
// null when nobody is signed in; Auth::check() is the boolean form.
abort_unless(Auth::check(), 403);
return view('profile.show', [
'user' => $user,
'id' => Auth::id(),
]);
}
}What attempt does, with the database left out
This is the shape of Illuminate\Auth\EloquentUserProvider: retrieveByCredentials builds the query from the non-password keys, then validateCredentials hashes and compares. Seeing it written out explains two common surprises: an extra key in the array silently changes the query, and a password column that is not a hash can never match. Adding 'active' => 1 to the real credentials array is a supported way to refuse suspended accounts.
<?php
declare(strict_types=1);
use Illuminate\Support\Arr;
$credentials = [
'email' => '[email protected]',
'password' => 'secret',
'active' => 1,
];
// Step one: every key except password becomes a where clause.
$wheres = Arr::except($credentials, ['password']);
// Step two: the single row that came back from that query.
$row = [
'email' => '[email protected]',
'active' => 1,
'password' => '$2y$12$4dvdn0vgrer826CKinyOxOKEERBQG9Iik9yPq59y.2q8dzzcsulFG',
];
echo Arr::except($row, ['password']) === $wheres ? 'user found' : 'no user', PHP_EOL;
// Step three: compare the plain-text password with the stored hash.
// Hash::check($plain, $hashed) does this with the configured driver.
echo password_verify($credentials['password'], $row['password']) ? 'password ok' : 'password wrong', PHP_EOL;
// Output:
// user found
// password okKeeping guests out of a route
The redirect target is a route name, not a URL: if no route is named login the auth middleware throws a RouteNotFoundException instead of redirecting, which is the usual cause of a confusing error on a half-built login page. redirectGuestsTo in bootstrap/app.php overrides it, and redirectUsersTo does the same for the guest middleware, which defaults to the dashboard or home route.
<?php
// routes/web.php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// 'auth' is the alias for Illuminate\Auth\Middleware\Authenticate.
// An unauthenticated visitor is redirected to the route named 'login'.
Route::middleware('auth')->group(function () {
Route::get('/dashboard', fn () => view('dashboard'))->name('dashboard');
});
// 'guest' is the opposite: a signed-in user is sent away from the
// login and registration screens.
Route::middleware('guest')->group(function () {
Route::get('/login', fn () => view('auth.login'))->name('login');
});
// bootstrap/app.php, if you want somewhere other than route('login'):
// ->withMiddleware(function (Middleware $middleware): void {
// $middleware->redirectGuestsTo('/login');
// })
Discussion