skip to content

Your Laravel API must accept cookie-authenticated calls from a marketing site on another origin — what must change in config/cors.php, and why?

level: seniorimportance: should knowfreq 42%

answer

  1. credentials need a named origin
  2. supports_credentials => true
  3. Access-Control-Allow-Credentials: true
  4. cover sanctum/csrf-cookie and login
  5. client opts in with credentials: 'include'

basics

~10 s

Set supports_credentials to true so HandleCors sends Access-Control-Allow-Credentials: true. Replace '*' in allowed_origins with the marketing site's exact origin, and list every credentialed path in paths, including sanctum/csrf-cookie and login.

solid answer

~40 s

Three keys change. `supports_credentials` goes to `true`, which makes `HandleCors` add `Access-Control-Allow-Credentials: true`; without that header the browser refuses to hand a credentialed response to the page. `allowed_origins` must name the real origin, for example `[env('MARKETING_URL')]` resolving to `https://www.tallyroom.example`. A credentialed response cannot be granted to `*`, and letting every site make cookie-bearing calls would be wrong anyway. `paths` must cover every endpoint the page calls with cookies: `sanctum/csrf-cookie`, `api/*`, and routes outside it such as `login`, `logout` or `broadcasting/auth`. The front end has to opt in as well (`fetch` with `credentials: 'include'`, or Axios `withCredentials`), and the session cookie domain and Sanctum's stateful domains must agree. CORS only makes the response readable; it authenticates nobody.

code

php · 13 lines
php
<?php

// config/cors.php for cookie-authenticated calls from the marketing site
return [
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
    'allowed_methods' => ['*'],
    'allowed_origins' => [env('MARKETING_URL', 'https://www.tallyroom.example')],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 600,
    'supports_credentials' => true,
];

go deeper

for a junior

Remember the three keys that change for cookie calls: supports_credentials, allowed_origins and paths.

for a middle

Explain why a credentialed grant needs an exact origin, which Sanctum endpoints must be in paths, and what the client must set.

for a senior

Diagnose a failing cookie flow end to end: preflight headers, the credentials flag, uncovered web routes, then cookie domain and Sanctum stateful settings.

for a principal

Choose between cookie-based SPA auth and tokens for a separately hosted front end, weighing cookie scope, CSRF handling and origin management.

## The scenario A SaaS product runs its Laravel API at `https://api.tallyroom.example`. A separate marketing site at `https://www.tallyroom.example` has a trial-signup form, and a "continue where you left off" widget that should know whether the visitor is logged in. The widget uses Sanctum's cookie-based SPA authentication: the browser keeps a session cookie for the API's domain and sends it on each call. Those two origins differ (the host differs), so every call is **cross-origin**. And because the calls carry cookies, they are **credentialed**. The framework's CORS defaults do not allow that. ## The three keys that change | Key | Framework default | Credentialed value | Why | |---|---|---|---| | `supports_credentials` | `false` | `true` | Makes `HandleCors` send `Access-Control-Allow-Credentials: true`, which the browser requires before it exposes a credentialed response | | `allowed_origins` | `['*']` | `[env('MARKETING_URL')]` | A credentialed grant must name one concrete origin, and only trusted front ends should get one | | `paths` | `['api/*', 'sanctum/csrf-cookie']` | Add `login`, `logout` and any other credentialed route | `HandleCors` adds nothing to a path it does not match | A few details make `allowed_origins` fragile: - **Exact values.** The browser sends `https://www.tallyroom.example` with a scheme and without a trailing slash. With a single configured origin, `HandleCors` sends that configured string verbatim, so `www.tallyroom.example` or `https://www.tallyroom.example/` can never match. - **Per-environment values.** Read the origin with `env()` inside `config/cors.php`, so staging and production each name their own front end. - **Several front ends.** List each origin explicitly. Wildcard entries such as `*.tallyroom.example` also match, but they admit every subdomain, including any that users or third parties control. ## Paths for a cookie flow Sanctum's SPA flow touches several endpoints, and each one must be covered: 1. `GET /sanctum/csrf-cookie` sets the `XSRF-TOKEN` cookie. It is in the default list; keep it if you publish a narrower `paths`. 2. `POST /login` (and later `/logout`) is usually a web route, provided by Fortify or a starter kit. It is outside `api/*`, so add it. 3. `GET /api/user` and the other API calls are covered by `api/*`. 4. `POST /broadcasting/auth` needs an entry if the page subscribes to private channels through Echo. ## Keys that can usually stay - `allowed_methods` `['*']` approves whatever method a preflight asks for. - `allowed_headers` `['*']` echoes back the header names the preflight requests, which covers `X-XSRF-TOKEN` and `Content-Type`. If you narrow the list, keep every header the front end sends. - `exposed_headers` matters only if script must **read** a response header, such as `X-RateLimit-Remaining`. Cookies are not exposed through it. - `max_age` can rise from `0` to cut repeat preflights, once the configuration is stable. ## What config/cors.php cannot fix - **The client must opt in.** A `fetch` without `credentials: 'include'`, or Axios without `withCredentials`, sends no cookie at all. Sanctum's docs also set `withXSRFToken` on Axios. - **Cookie scope and Sanctum's stateful domains** are separate settings: the session cookie's domain must cover both hosts, and Sanctum must treat the marketing origin as stateful. Those live in the session and Sanctum configuration, not here. - **CSRF protection still applies.** Cross-origin credentialed writes still pass through the framework's request-forgery middleware, which is why the SPA fetches the CSRF cookie first. - **CORS is not authentication.** It lets the browser show the page a response. Whether the request is authorised is decided by the `auth` middleware and policies. ## Verifying the setup Check the flow in the browser's network panel, one request at a time: 1. The preflight for the login `POST` returns an `Access-Control-Allow-Origin` of exactly `https://www.tallyroom.example` and `Access-Control-Allow-Credentials: true`. 2. The `GET /sanctum/csrf-cookie` response sets the `XSRF-TOKEN` cookie, and later requests send it back as the `X-XSRF-TOKEN` header. 3. The actual responses repeat both CORS headers; a missing header on any one of them means its path is not covered. 4. `GET /api/user` returns the user rather than a `401`. If CORS passes but authentication fails, the problem has moved to cookie scope or Sanctum's stateful domains. Reading the steps in this order separates the three layers — CORS, CSRF and authentication — that a single "login does not work" report usually mixes together.

  • Why would a Laravel app need broadcasting/auth in its cors paths?
    When the marketing site subscribes to private channels through Laravel Echo, Echo posts to `broadcasting/auth` with the session cookie to get a channel signature. That route is outside `api/*`, so without a `paths` entry `HandleCors` adds no headers and the browser blocks the authorisation response.
  • Should you keep allowed_headers as ['*'] on a credentialed Laravel setup?
    It is workable: with `['*']`, `HandleCors` echoes back whichever header names the preflight requests, including `X-XSRF-TOKEN`. Narrowing it to the headers the front end really sends is tighter, but every omission turns into a failed preflight. Keep `Content-Type`, `Accept`, `X-XSRF-TOKEN` and `X-Requested-With` if you narrow it.

saying these in an interview costs you the question

  • Leaving allowed_origins at ['*'] is a safe choice once supports_credentials is true.
  • Setting supports_credentials to true makes the browser send cookies on every call.
  • Cookies must be listed in exposed_headers for the page to use its session.
  • The api/* entry already covers the login and logout routes.
  • CORS settings authenticate the marketing site to the API.