Authentication with Starter Kits
By the end of this lesson you will know how to add a complete login system to a Laravel app in minutes with a starter kit, and how login works underneath by building a small manual version for the coaching office. You will also learn how to protect pages, read the current user and stop password-guessing attacks.
Authentication in plain words
Authentication answers "who are you?". Authorisation, the next lesson, answers "what are you allowed to do?". At a school gate, showing your ID card is authentication. Being allowed into the staff room is authorisation.
Laravel stores users in the users table (created by the very first migration) and uses the App\Models\User model. After a successful login, it remembers the user's ID in the session, so every later request knows who is visiting.
Option 1: a starter kit (recommended for new apps)
A starter kit is a complete, ready-made app skeleton with login, registration, password reset, email verification, two-factor authentication and a settings page. The code is copied into your project, so you own it and can change anything.
Laravel 13 offers four official starter kits:
| Starter kit | Front end | Built with |
|---|---|---|
| React | React 19 pages through Inertia | TypeScript, Tailwind, shadcn/ui |
| Vue | Vue 3 Composition API through Inertia | TypeScript, Tailwind, shadcn-vue |
| Svelte | Svelte 5 through Inertia | TypeScript, Tailwind, shadcn-svelte |
| Livewire | Blade and Livewire 4 | Tailwind, Flux UI |
All four use Laravel Fortify, a back-end package that provides the login logic and routes. Each kit also has a WorkOS AuthKit variant that adds social login (Google, Microsoft, GitHub, Apple), passkeys and single sign-on through an external account.
Step 1. Create the app with the installer and choose a kit when asked:
laravel new coaching-portalStep 2. Install front-end packages and start everything:
cd coaching-portal
npm install && npm run build
composer run devStep 3. Open http://localhost:8000/register, create an account and you are logged in. Fortify registers routes like these, depending on the features switched on in config/fortify.php:
| Route | Method | Purpose |
|---|---|---|
/login | GET, POST | Show the form, log in |
/logout | POST | Log out |
/register | GET, POST | Sign-up form, create the user |
/forgot-password | GET, POST | Ask for a reset link |
/reset-password/{token} | GET | Reset form from the email link |
/email/verify | GET | "Please verify your email" notice |
/two-factor-challenge | GET, POST | Ask for the 2FA code |
To turn a feature off, remove it from the features array in config/fortify.php. For a coaching office where only the admin creates staff accounts, remove Features::registration() so nobody can sign up from outside.
To change what happens at registration, edit the action classes in app/Actions/Fortify, such as CreateNewUser.php.
Option 2: how login works, by hand
Starter kits hide the details. Writing a tiny login yourself shows what is really happening. This is the coaching office's staff login.
Step 1. The controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function store(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
return back()
->withErrors(['email' => 'These details do not match our records.'])
->onlyInput('email');
}
$request->session()->regenerate();
return redirect()->intended('/admin/dashboard');
}
public function destroy(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}What each piece does:
Auth::attempt()finds the user by email and checks the password against the stored hash withpassword_verify(). It returnstrueand logs the user in, orfalse.- The second argument turns on remember me, a long-lived cookie that keeps the user logged in after the session ends.
session()->regenerate()gives the browser a new session ID after login, to block session fixation.redirect()->intended()sends the user to the page they originally wanted, or to the default.- Logout clears the user, destroys the session and makes a new CSRF token.
Step 2. The routes, with middleware:
use App\Http\Controllers\LoginController;
Route::get('/login', [LoginController::class, 'create'])->middleware('guest')->name('login');
Route::post('/login', [LoginController::class, 'store'])->middleware(['guest', 'throttle:5,1']);
Route::post('/logout', [LoginController::class, 'destroy'])->middleware('auth')->name('logout');
Route::prefix('admin')->middleware('auth')->group(function () {
Route::get('/dashboard', fn () => 'Admin dashboard. Welcome, '.auth()->user()->name);
});The login route is named login, because the auth middleware redirects guests to the route with that name. throttle:5,1 allows only 5 login attempts per minute from one visitor, which slows down password guessing.
Step 3. Test the whole flow. These are the real responses from curl with a cookie jar, using the seeded admin user:
GET /admin/dashboard (not logged in) -> 302 to /login
POST /login with a wrong password -> 302 to /login
form shows: These details do not match our records.
email box still contains admin@example.com
POST /login with the right password -> 302 to /admin/dashboard
GET /admin/dashboard -> Admin dashboard. Welcome, Office Admin
GET /login (already logged in) -> 302 to /dashboardThe last line shows the guest middleware in action: logged-in users are sent away from the login page. Notice the error message does not say whether the email or the password was wrong; that would help an attacker find valid emails.
Working with the logged-in user
use Illuminate\Support\Facades\Auth;
$user = Auth::user(); // the User model, or null for guests
$id = Auth::id(); // just the ID
$isIn = Auth::check(); // true or false
$user = auth()->user(); // helper version
$user = $request->user(); // inside a controller with RequestIn Blade, show parts of a page only to logged-in visitors or only to guests:
@auth
<p>Logged in as {{ auth()->user()->name }}</p>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button>Log out</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}">Staff login</a>
@endguestPasswords
Never store plain passwords. The User model casts password as 'hashed', so User::create(['password' => 'secret123']) stores a bcrypt hash automatically. To hash or check by hand, use the Hash facade: Hash::make($plain) and Hash::check($plain, $hash). The BCRYPT_ROUNDS setting in .env controls how slow (and therefore how hard to crack) hashing is.
Common mistakes
- Not naming the login route
login. Theauthmiddleware cannot find where to send guests and throws an error. - Forgetting
session()->regenerate()in a custom login. It leaves the app open to session fixation. - Leaving public registration on. For internal office apps, disable
Features::registration()and create staff accounts yourself. - Writing "email not found" or "wrong password". Use one general message for both.
- No rate limit. Without
throttle, bots can try thousands of passwords.
Key points
- Laravel 13 starter kits (React, Vue, Svelte, Livewire) give full authentication through Fortify; the WorkOS variants add social login and passkeys.
- Fortify features such as registration and 2FA are switched on or off in
config/fortify.php. Auth::attempt()checks hashed passwords and logs in; then regenerate the session and redirect withintended().- Protect routes with the
authmiddleware, keep logged-in users off login pages withguest, and slow attackers withthrottle. - Read the user with
Auth::user(),auth()->user()or$request->user(); use@authand@guestin Blade.
Exercise
Extend the manual login so that only active staff can log in. Add a boolean is_active column to users (default true). Change the Auth::attempt() call so that inactive users get the same "These details do not match our records." error. Then show a "Log out" button in your layout only for logged-in users.
Show solution
// migration
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_active')->default(true);
});// LoginController::store()
if (! Auth::attempt([...$credentials, 'is_active' => true], $request->boolean('remember'))) {
return back()
->withErrors(['email' => 'These details do not match our records.'])
->onlyInput('email');
}@auth
<form method="POST" action="{{ route('logout') }}">
@csrf
<button>Log out</button>
</form>
@endauthExtra keys in the array passed to Auth::attempt() (other than the password) become extra where conditions, so an inactive user simply does not match.
Quick Quiz
1. Which package provides the login logic used by Laravel 13's starter kits?
All four starter kits use Laravel Fortify for authentication routes and actions.
2. What should you call right after a successful custom login?
Regenerating the session ID prevents session fixation attacks.
3. Where does the auth middleware send a guest by default?
It redirects unauthenticated web users to the route named login.
4. What does throttle:5,1 on the login route do?
The throttle middleware limits requests to slow down password guessing.
Learn Laravel with a mentor
Free lessons get you started. Our PHP & MySQL Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
