Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with PHP 8.5.11, Composer 2.10.3, Laravel 13.34.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

composer.json (part of the React kit's require block) JSON · react-kit-require.json
{
    "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

  1. Question 1 of 2 You ran laravel new --react three months ago. A new framework release comes out. What happens to the login page the kit generated?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing changes it; the generated code is yours to update yourself

    A starter kit is scaffolding. The generated files live in your project and you maintain them; only the backend packages behind them, such as Fortify, are Composer dependencies.

  2. Question 2 of 2 Which starter kit renders its pages on the server and needs no JavaScript framework?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The Livewire kit

    React, Vue and Svelte kits render through Inertia with a JavaScript framework; the Livewire kit renders on the server.

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:

Interactive · message by message A login the kit did not have to write Step through a sign-in, from the form the kit renders to the session guard that remembers the user.
A login the starter kit did not have to write, step 5 of 5. Messages so far: Browser to Fortify: POST /login; Fortify to Guard: attempt(); Fortify to Browser: 302 to dashboard.Browserkit's formFortifycode neededGuardsigned inPOST /loginattempt()302 to dashboard

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

config/fortify.php: features are switches PHP · fortify-config.php
<?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.

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.

Interactive · step by step Which user does a social sign-in belong to? Follow a provider sign-in through the three questions the action asks before it trusts it.
Which user does a social sign-in belong to?, step 5 of 5. Showing Seen this account?, Sign in that user, Email verified?, Refuse (takeover blocked), Email on file?, Create a user, Link and sign in. In focus: Create a user, Link and sign in.Seen this account?by provider idSign in that userits row existsEmail verified?provider's flagRefusetakeover blockedEmail on file?a user owns itCreate a useremail set verifiedLink and sign inadd the link row

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

php artisan test: the safe account-linking action Python · account_link_session.py
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:

The naive action fails the same check Python · naive_link_session.py
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

  1. Question 1 of 2 A GitHub sign-in reports asha@example.com, and a Pathshala user already has that email. The provider has NOT marked the email verified. What should the action do?
    Show the answer: Question 1 of 2

    Answer: Refuse to link, because an unverified email does not prove who controls the address

    Link to an existing account only when the provider verified the email. An unverified match is refused.

  2. Question 2 of 2 A social_accounts row already exists for this provider and provider id. What does the safe action do?
    Show the answer: Question 2 of 2

    Answer: Return that row's user and stop, without re-checking the email

    The first question the action asks is whether this exact provider account has been seen before. If so, it returns that user and the email checks never run.

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:

Interactive · answer and compare Built-in auth or a hosted provider? Answer the two questions. The table marks the option that fits, with the reason under the one that does not.
Do your customers' companies need to sign in with their own identity provider?
Do you want to own and keep the auth code and user data?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Built-in Fortify auth against WorkOS AuthKit, on four things that differ
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

  1. One question Your customers are companies that must sign in with their own corporate identity provider (SAML). Which option fits best?

    Choose one answer.

    Show the answer: One question

    Answer: WorkOS AuthKit, whose kit variant brings enterprise single sign-on

    Enterprise SSO is the case WorkOS AuthKit is built for; the built-in path fits individual sign-ins.

Practise

Work on your own Pathshala project, or the fake provider user from the example:

  1. Install a kit into a throwaway project with laravel new --livewire, and list which files under app/ and resources/ it added. Decide which three you would most likely edit.
  2. In config/fortify.php, comment out Features::registration() and confirm with php artisan route:list that the registration route is gone. Put it back.
  3. Add a second case to LinkSocialAccountTest: a returning social account (a social_accounts row 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.

  1. Question 1 of 9 Where does the code a starter kit generates end up?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: In your own project, under your version control

    A starter kit is scaffolding copied into your app, so the generated files are yours to keep and change.

  2. Question 2 of 9 You want to change the look of the generated dashboard. What is the honest consequence of a kit putting the code in your app?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: You can edit it freely, and you are responsible for maintaining it

    Because the files live in your project, you may change every screen, and you also own their upkeep.

  3. Question 3 of 9 What does it mean that Fortify is a headless authentication backend?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: It provides the routes and logic but renders no pages of its own

    Headless means no views. Fortify owns registration, login, reset, verification, confirmation and two-factor, and the kit draws the screens.

  4. Question 4 of 9 You are shipping a product that must not allow public self-registration. Using Fortify, how do you remove the registration route?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Remove Features::registration() from the features array in config/fortify.php

    Features are switches in config/fortify.php. Leave registration off and Fortify never registers its routes.

  5. Question 5 of 9 Registration must also set a centre_id and send a welcome email. Where does that logic belong?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: In the action class Fortify calls to create a user, which you bind

    Fortify calls a create-user action you provide, so extra columns, roles and a welcome email live in that one place.

  6. Question 6 of 9 What single fact does the safe account-linking action require before it links a social sign-in to an existing user?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: That the provider has verified the email it reported

    Linking is safe only on a provider-verified email; an unverified match is refused.

  7. Question 7 of 9 The lesson's naive action failed its test with "Failed asserting that 1 is identical to 0". What real-world event does that failing assertion stand for?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: A social account was linked to a user from an email the provider never verified

    One linked row where zero were allowed means the naive action joined a stranger's unverified email to an account, which is the takeover.

  8. Question 8 of 9 Which statement about WorkOS AuthKit is accurate?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: It is a hosted sign-in, so credentials and SSO live with WorkOS while your app keeps a user row

    WorkOS AuthKit hosts the sign-in, with social providers, passkeys and enterprise SSO, which suits customers that need SAML.

  9. Question 9 of 9 A single-user note-taking product is unsure whether to enable team support now "just in case". What is the sound advice?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Leave teams off, since a single-user product gains nothing and every query and policy would carry a team check

    Teams make every query and policy team-aware, so enable them when data is genuinely shared in groups, not pre-emptively.

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.

  1. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.