Laravel ships with four different ways to handle authentication - session-based auth, Sanctum, Passport, and Fortify - and picking the wrong one, or misconfiguring the right one, is one of the most common sources of real Laravel security incidents. This guide covers what each one actually protects against, the session cookie settings almost every project gets wrong, and which package fits which architecture.

Session Cookie Security (Traditional Web Auth)

If your Laravel app renders Blade views and uses the default web guard, you're using session-based authentication - and its security lives almost entirely in three environment variables that are easy to leave at their insecure defaults during a rushed deployment.

# .env
SESSION_SECURE_COOKIE=true    # Cookie only sent over HTTPS - without this,
                               # the session cookie is readable over plain HTTP
SESSION_HTTP_ONLY=true        # Blocks JavaScript access via document.cookie
                               # (default true, but worth confirming explicitly)
SESSION_SAME_SITE=strict      # Prevents the cookie being sent on cross-site
                               # requests - your strongest built-in CSRF defense

SESSION_SECURE_COOKIE is the one that gets missed most often, because a local development environment runs over HTTP and the app works fine either way - the gap only becomes exploitable once the app is live on HTTPS with this still unset. Without it, a session cookie transmitted over an accidental plain-HTTP request (a stray internal link, a misconfigured load balancer health check) is readable by anyone on the network path.

SESSION_SAME_SITE=strict is the strictest setting and breaks legitimate cross-site navigation into authenticated pages (clicking a link from an email client, for example, won't carry the session). lax is Laravel's actual default and is a reasonable middle ground for most apps; use strict only if you don't need any cross-site entry points to authenticated pages.

Sanctum: SPA Cookie Auth and API Tokens

Sanctum does two unrelated things, and conflating them is the most common Sanctum mistake. It provides cookie-based authentication for a first-party SPA on the same top-level domain, and it provides bearer-token authentication for mobile apps and third-party API consumers. They have different security models.

SPA mode relies on Laravel's session cookie, exactly like traditional web auth, plus CSRF protection via the /sanctum/csrf-cookie endpoint. It only works when the SPA and API share the same top-level domain (configured in SANCTUM_STATEFUL_DOMAINS) - if you're serving an API to a frontend on a different domain, this mode does not apply and falls through to token auth, which has no CSRF protection because bearer tokens aren't automatically sent by the browser the way cookies are.

# config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort(),
))),

Token mode issues personal access tokens that, as covered in our main Laravel security guide, don't expire by default. Scope every token with abilities rather than issuing one all-access token per user.

Passport: Full OAuth2

Passport implements the complete OAuth2 specification - authorization codes, client credentials, password grants, refresh tokens. It's the right choice when you're building a platform that issues access to genuinely third-party applications (a public API with external developers registering their own OAuth clients), not when you just need "an API with login."

The password grant type deserves specific caution: it accepts a username and password directly from a client application to exchange for a token, which means that client must be fully trusted with raw credentials. It exists mainly for first-party mobile apps migrating off legacy auth - for anything else, prefer the authorization code grant, where the user authenticates on your own login page and the client never sees their password.

Passport's encryption keys (storage/oauth-private.key, oauth-public.key) must never end up in version control or a public build artifact - anyone with the private key can mint valid tokens for any user.

Fortify: Headless Auth Scaffolding

Fortify implements the backend logic for registration, login, password reset, email verification, and two-factor authentication without shipping any views - useful when Breeze or Jetstream's frontend doesn't fit your stack but you don't want to hand-roll auth flows yourself. Its main security value is that it implements rate limiting and account lockout correctly out of the box for every one of these routes, which is exactly the kind of thing that's easy to get subtly wrong writing it from scratch.

// config/fortify.php
'limiters' => [
    'login' => 'login',       // 5 attempts/minute by IP+email, built in
    'two-factor' => 'two-factor',
],

Which One Do You Actually Need?

SituationUse
Blade-rendered web app, no separate APISession auth (default)
First-party SPA + API on the same domainSanctum, SPA mode
Mobile app or third-party API consumersSanctum, token mode
Public API with external OAuth clientsPassport
Need 2FA/rate-limited auth without Breeze/Jetstream viewsFortify

Most Laravel apps need exactly one of these, not several layered together. Running Sanctum and Passport side by side because a tutorial mentioned both is a common source of confusing, inconsistently-enforced auth rules across routes.

Common Mistakes Across All Four

Verifying It's Actually Configured Correctly

Every setting above lives in .env or a config file - none of it is visible by reading your deployed site from the outside unless you check what the server actually sends. Shieldome's Laravel security scanner checks session cookie flags, exposed configuration, and authentication endpoint behavior from the outside, the same way an attacker would. Create a free account to run your first scan.