Building Web Apps with Laravel Module 8 – Authentication and authorization
Starter kits, Fortify and social login
Choose a Laravel 13 starter kit, switch Fortify features on and off, and add social sign-in that links an account only when the provider verified the email.
What you will learn
- Choose a Laravel starter kit and describe what it generates in your app
- Configure Fortify features and how a new user is created
- Add social sign-in and link an account only when the provider verified the email
- Decide when WorkOS AuthKit or team support is worth turning on
Before you start
What you’ll learn · Before you start
What you will learn
- Choose a Laravel starter kit and describe what it generates in your app
- Configure Fortify features and how a new user is created
- Add social sign-in and link an account only when the provider verified the email
- Decide when WorkOS AuthKit or team support is worth turning on
Before you start
On this page
Pathshala’s team wants a React dashboard and a “Sign in with GitHub” button, and nobody wants to hand-build registration, password reset and sessions first. A starter kit scaffolds all of that in a minute. The catch is that the code lands in your own repository, so you own and maintain it, and one careless line in social sign-in can join a stranger to a parent’s account.
This lesson picks a kit and names what it generates, switches Fortify’s features on and off, adds social sign-in that refuses an unverified email (with a test that proves the refusal), and weighs WorkOS AuthKit and team support against the built-in path. The authentication the kit sets up is taken apart by hand in the next three lessons.
A starter kit is scaffolding that lives in your app
A starter kit: Scaffolding that laravel new copies into your project: ready-made authentication controllers, pages, routes and config that then become your own code to maintain. is not a dependency you install and forget. laravel new --react (or --vue, --svelte, --livewire) copies controllers, pages, routes and config straight into your project. From then on the auth screens are your code, in your version control, changed like anything else you wrote. The React, Vue and Svelte kits render through Inertia: A library that lets a server-side app such as Laravel keep its routes and controllers while React, Vue or Svelte draws each page; every response names a page component and the props it gets. More in “Single-page apps with Inertia 3”; the Livewire kit renders on the server with Livewire builds interactive interfaces from Blade components whose state lives on the server. and needs no JavaScript framework.
What arrives differs a little by kit, but each one brings the same backend. The React kit’s composer.json, for instance, pulls in Inertia, Fortify and a route helper:
{
"require": {
"php": "^8.3",
"inertiajs/inertia-laravel": "^3.0",
"laravel/fortify": "^1.37.2",
"laravel/framework": "^13.17",
"laravel/wayfinder": "^0.1.14"
}
} | Kit | Front end | Renders with | You also get |
|---|---|---|---|
| React / Vue / Svelte | a JS framework | Inertia | Fortify, a typed route helper, a dashboard |
| Livewire | Blade + Livewire | the server | Fortify, Flux UI components, a dashboard |
Because the files are yours, an upgrade to the framework does not rewrite them; you change them yourself. That is the trade for having every screen open to editing. Breeze and Jetstream, the older scaffolds, stopped receiving updates when these kits arrived with Laravel 12, so a new project starts from a kit.
Checkpoint
Check: What a starter kit is and generates
Fortify does the authentication; the kit draws the pages
Underneath every kit sits Laravel Fortify, a headless authentication backend. Headless means it registers the routes and logic for registration, login, password reset, email verification, password confirmation and two-factor authentication, but renders no pages of its own. The starter kit supplies the pages; Fortify does the work behind them. Follow a single sign-in through it:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The form posts. The page the starter kit generated collects the email and password and posts them to /login. Fortify registered that route; the kit wrote none of the logic behind it.
- Two-factor can step in first. Fortify runs its login steps in a fixed order. When two-factor is switched on and this user has set it up, it checks the password, keeps the user aside without signing them in, and sends the browser to the code page the kit supplies.
- Fortify checks the credentials. Without two-factor, Fortify hands the email and password to the session guard, which looks the user up through the configured provider and checks the password against the stored hash.
- The session is started. On a match the guard signs the user in, and Fortify regenerates the session id so a session fixed before login cannot be reused. The next lesson builds this step by hand.
- The answer goes back. Fortify redirects a browser to the home path in config/fortify.php, the dashboard in the kits, and answers an XHR request with 200. The kit's job was the page; Fortify's job was everything behind it.
Each feature is one line in config/fortify.php. Comment the line out and Fortify stops registering its routes, which is how you ship a product without public registration, or without password reset. In the React, Vue and Svelte kits, also delete the pages’ references to those routes: Wayfinder generates route definitions at build time, so a reference to a missing route fails the build. What a new user looks like is yours too: Fortify calls the action class you bind, so a centre_id, a role or a welcome email is added there rather than in framework code.
<?php
// config/fortify.php (the relevant part). Each entry here switches one feature on.
// Comment a line out and Fortify stops registering that feature's routes.
return [
// ... driver, guard, middleware and prefix settings above ...
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true, // make the user type a code before it is switched on
'confirmPassword' => true,
]),
],
];
// What a user looks like when they register is yours to decide. Fortify calls the
// class you bind, so you add a centre_id, a role or a welcome email here:
//
// app/Actions/Fortify/CreateNewUser.php
// public function create(array $input): User
// {
// Validator::make($input, [...])->validate();
// return User::create([... your columns ...]);
// } Key idea
The kit is the pages and the Fortify backend, copied into your app. Turn a feature off by removing its line in config/fortify.php; change what registration creates by editing the action Fortify calls.
Social sign-in links an account only when the email is proven
Socialite handles sign-in through an OAuth: An authorization protocol that lets a user agree to sign in at a provider (such as Google or GitHub), which then tells your app who they are without sharing their password. provider such as GitHub or Google: the visitor signs in at the provider, and Socialite hands your app a user object with an id, a name and an email. The hard part is not the redirect. It is deciding which Pathshala account that provider user belongs to, a step called account linking: Deciding which existing user a third-party (social) sign-in belongs to, and attaching the provider identity to that account only when it is safe to do so..
The rule that keeps it safe is one question: did the provider confirm the email it sent you? An email address on its own proves nothing, because anyone can type someone else’s address when they register at a provider. So the action links a social sign-in to an existing account, or creates a new one, only when the provider marks that email verified (providers that speak OpenID Connect send this as the standard email_verified claim); otherwise it refuses.
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A returning account. First the action looks for a social_accounts row with this provider and provider id. If it finds one, the person has signed in this way before, so it returns that user and stops. Nothing else is checked.
- The question that matters. For a first-time social sign-in, the email is about to decide the account, so the action first asks whether the provider confirmed the email it sent. An address on its own is not proof of who controls it.
- Unverified is refused. An unverified email is refused before any lookup. It may not join an existing account, which would be a takeover, and it may not claim a new one, which would let a stranger hold the address before its owner signs up.
- A verified email on file. With a verified email, the action looks for a user who owns that address. When there is one, it adds a social_accounts row for that user and returns them, so their next sign-in takes the first path.
- No user yet: create, then link. When nobody owns the address, the action creates the user, marks the email verified because the provider proved it, gives the account an unguessable password, and then adds the same link row.
The decision is a small action with three questions, and a feature test drives it with a fake provider user so it runs with no network and no browser. A verified email signs in a new visitor or links to the existing user who owns it; an unverified one is refused in both cases:
from session import run
# The four cases a safe account-linking action must get right: a new visitor and an
# existing user whose email the provider verified, and the refusal of both when it did not.
run(
"""
php artisan test --filter=LinkSocialAccountTest
""",
app=True,
overlay=("link-probe",),
) Output
$ php artisan test --filter=LinkSocialAccountTest PASS Tests\Feature\LinkSocialAccountTest ✓ a new visitor with a verified email gets an account …s ✓ a verified email links to the existing account …s ✓ an unverified email is refused and nothing is linked …s ✓ an unverified email from a new visitor creates nothing …s Tests: 4 passed (10 assertions) Duration: …s
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 account_link_session.py
All four pass. Now the tempting shortcut: match on email and link, whatever the provider says. Run the same safety check against that version:
from session import run
# The same safety check, run against the naive action. It is meant to fail: the
# naive action linked an unverified email to an existing account.
run(
"""
!php artisan test --filter=NaiveLinkTest
""",
app=True,
overlay=("link-probe",),
) Output
$ php artisan test --filter=NaiveLinkTest
FAIL Tests\Feature\NaiveLinkTest
⨯ an unverified email is refused and nothing is linked …s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\NaiveLinkTest > an unverified email is refused and…
Failed asserting that 1 is identical to 0.
at tests/Feature/NaiveLinkTest.php:30
26▕
27▕ (new NaiveLinkSocialAccount)->resolve($incoming);
28▕
29▕ // We want nothing linked to Asha's account from an unverified email.
➜ 30▕ $this->assertSame(0, SocialAccount::count());
31▕ }
32▕ }
33▕
1 tests/Feature/NaiveLinkTest.php:30
Tests: 1 failed (1 assertions)
Duration: …s
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 naive_link_session.py
It fails on Failed asserting that 1 is identical to 0: the naive action linked a social account to Asha’s record from an email the provider never verified. In a real product, the attacker who did that would then sign in as Asha. The failing check is the proof that the verified-email guard is doing something; the passing check next to it is the proof the fix works.
Trusting the email, not the verification
User::where('email', $providerUser->getEmail())->first() and then linking looks reasonable and is the takeover. Link only when the provider says the email is verified, and remember the check is provider-specific: read each provider’s field rather than assuming one exists.
Checkpoint
Check: Safe social account linking
When WorkOS AuthKit or team support is worth turning on
Two options sit beside the built-in path. WorkOS AuthKit is a hosted login. Each kit has a WorkOS variant that hands sign-in to WorkOS, which brings social providers, passkeys, email-based Magic Auth and enterprise single sign-on; switch its email-and-password method off and your app never handles a password. Team support (added with the installer’s --teams option) gives every user a personal team and a current team, puts the team in the URL as /{current_team}/dashboard, and reserves names such as settings, login and dashboard so a team slug cannot collide with a route.
Neither is without cost, so choose by the product rather than the fashion. The question that decides WorkOS is usually whether your customers are companies that must sign in with their own identity provider:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Option | Where identities live | SSO and SAML for companies | Setup and running cost | Control over the pages |
|---|---|---|---|---|
| Built-in (Fortify) What you run: a starter kit, auth code in your app | Good in your own database, under your control | Poor you build any SAML single sign-on yourself | Good nothing extra to run or pay for | Good every page and field is yours to change |
| WorkOS AuthKit What you run: a hosted sign-in, no passwords in your app | Mixed sign-in at WorkOS; your users table keeps a workos_id | Good enterprise SSO comes with the WorkOS variant | Mixed a WorkOS account and API keys in .env, less code | Mixed the sign-in screens are hosted by WorkOS |
How the answers decide
- “Do your customers' companies need to sign in with their own identity provider?” Yes, enterprise SSO matters: adds a caveat to Built-in (Fortify). Enterprise SSO and SAML are large to build and keep correct, which is what a hosted provider such as WorkOS does for you.
- “Do your customers' companies need to sign in with their own identity provider?” No, individual sign-ins and “Do you want to own and keep the auth code and user data?” Own it in the app: rules out WorkOS AuthKit. For individual sign-ins, the built-in kit keeps every identity and page in your app with nothing extra to run.
Teams add a dimension to every query
Once a row belongs to a team, every read and every policy has to check the team as well as the user. Turn teams on when real customers share data inside a group, and decide before you run the installer, since it is the installer that generates the team code. Leave it off for a single-user product, where every team check would guard nothing.
Checkpoint
Check: When WorkOS AuthKit or teams is worth it
Practise
Work on your own Pathshala project, or the fake provider user from the example:
- Install a kit into a throwaway project with
laravel new --livewire, and list which files underapp/andresources/it added. Decide which three you would most likely edit. - In
config/fortify.php, comment outFeatures::registration()and confirm withphp artisan route:listthat the registration route is gone. Put it back. - Add a second case to
LinkSocialAccountTest: a returning social account (asocial_accountsrow already exists) signs in as its own user even if a different email is now reported. Make it pass.
Then work through the self-check.
Practice that adapts to you
Practise this lesson
9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: a starter kit is a dependency you can upgrade with Composer, true or false?
False, and the difference matters. A starter kit is scaffolding: laravel new --react copies controllers, pages, routes and config into your project, where they become your code under your version control. A framework upgrade does not rewrite those files, and there is no “kit version” to bump; you maintain the generated code yourself, which is the price of being able to edit every screen. What the kit installs as real Composer dependencies is the backend it leans on, such as Fortify and Inertia, and those you do upgrade. So treat the generated code as yours and the packages behind it as dependencies.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
You are adding "Sign in with GitHub" to an app that already has email-and-password accounts. Describe the rule that decides when a social sign-in may be joined to an existing account, and the bug if you get it wrong.
-
Fresher to mid
A product must not allow public self-registration, and new users must be created with a centre_id and a welcome email. Using a starter kit and Fortify, say how you would achieve both.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Is a Laravel starter kit a dependency that Composer upgrades for you?
No. laravel new --react (or --vue, --svelte, --livewire) copies controllers, pages, routes and config into your project, where they are your code to maintain. Only the backend packages it leans on, such as Fortify, stay Composer dependencies.
-
How do the React, Vue and Svelte kits render pages, compared with the Livewire kit?
The React, Vue and Svelte kits render through Inertia. The Livewire kit renders on the server with Livewire and needs no JavaScript framework.
-
What is Fortify, and how do you turn one of its features off?
The headless authentication backend behind the kits. Each feature is one line in config/fortify.php: comment out Features::registration() and Fortify stops registering those routes. What registration creates is the action class Fortify calls, which you edit.
-
When may a social sign-in be linked to an existing account with the same email?
Only when the provider says it verified that email. Linking on an email match alone is an account-takeover path, because anyone can register at a provider with someone else's address; the check is provider-specific, so read each provider's own field.
-
When are WorkOS AuthKit and team support worth turning on?
WorkOS AuthKit, a hosted login, when customers are companies that must sign in through their own identity provider (enterprise single sign-on). Team support when real customers share data inside groups. Both cost more than the built-in path.
-
Why decide on team support before running the installer?
The installer generates the team code, and once a row belongs to a team every read and every policy must check the team as well as the user. In a single-user product those checks would be work with nothing to protect.
Key takeaways
- A starter kit copies auth controllers, pages and config into your app, so the generated code is yours to maintain, while the backend packages behind it stay Composer dependencies.
- Fortify is the headless authentication backend; each feature is a switch in
config/fortify.php, and what registration creates is the action class you bind. - Social sign-in is safe only when an existing account is linked after the provider has verified the email; an email match alone is an account-takeover path, and a test that fails on the weak version proves the guard works.
- WorkOS AuthKit is worth it when customers need enterprise single sign-on; team support is worth it when customers share data in groups, and both cost more than the built-in path.
Terms in this lesson
- account linking
- Deciding which existing user a third-party (social) sign-in belongs to, and attaching the provider identity to that account only when it is safe to do so.
- OAuth
- An authorization protocol that lets a user agree to sign in at a provider (such as Google or GitHub), which then tells your app who they are without sharing their password.
- starter kit
- Scaffolding that laravel new copies into your project: ready-made authentication controllers, pages, routes and config that then become your own code to maintain.
References
- Starter kits (Laravel (laravel.com))
- Laravel Fortify (Laravel (laravel.com))
- Laravel Socialite (Laravel (laravel.com))
- Authentication (Laravel (laravel.com))
- Release notes (Laravel 12) (Laravel (laravel.com))
- OpenID Connect Core 1.0, Standard Claims (OpenID Foundation)
- laravel/installer, NewCommand.php (Laravel (GitHub))
- laravel/react-starter-kit, composer.json (Laravel (GitHub))
- laravel/react-starter-kit (WorkOS variant), users table migration (Laravel (GitHub))
Report a problem with this lesson
Kept only in this browser. Your Learn progress