Skip to content

From the team

Laravel Sanctum or Passport: choosing API authentication

Use Sanctum unless you are building an OAuth provider, and know exactly what that means before you decide you are. Cookies for SPAs, tokens for mobile, Passport for third parties.

5 min read

Laravel ships two first-party packages for API authentication, and the choice between them is made wrongly more often than any other early decision in a Laravel API. This article is for developers and technical leads starting an API that a web front end, a mobile app or a third party will call, and it gives a straight answer: use Sanctum unless you are building an OAuth provider, and know exactly what that means before you decide you are.

What is the difference between Sanctum and Passport?

Sanctum does two things. It lets a first-party single-page application authenticate with the ordinary session cookie, and it issues simple personal access tokens for mobile apps, scripts and integrations. It stores tokens as hashed strings in a table, has no concept of clients or grants, and adds one middleware.

Passport is a full OAuth2 server. It implements authorization codes, refresh tokens, client credentials, PKCE, scopes and the consent screen a user sees when a third-party application asks to act on their behalf. It is the right tool when other people's software needs to access your API on behalf of your users. It is a lot of machinery when only your own front ends do.

The difference is not security. Both are secure when configured correctly. The difference is which problem they solve.

How should a SPA authenticate with Laravel?

If your Next.js or Vue front end lives on the same top-level domain as the API, use Sanctum's cookie mode and no tokens at all. The browser sends the session cookie; Laravel checks the CSRF token; there is nothing to store in local storage and nothing for a script injected into the page to steal. This is how our own Next.js front ends talk to Laravel: same-site cookie, withCredentials on the client, SANCTUM_STATEFUL_DOMAINS listing the front-end hosts, and a /sanctum/csrf-cookie call before the first login.

Get the domains right first. The cookie is scoped to the API domain, so app.example.com and api.example.com work; example.com and example-api.com do not, and the fix is DNS, not code. Set SESSION_DOMAIN to the shared parent with a leading dot and make sure SESSION_SECURE_COOKIE is on in production.

What about mobile apps and tokens?

Mobile apps cannot use the cookie session, so they use Sanctum's personal access tokens. The app posts credentials to a login endpoint, receives a plain-text token once, stores it in the platform's secure storage (Keychain, Keystore), and sends it as a bearer header. One token per device, named after the device, so the user can see and revoke sessions.

Give tokens abilities and an expiry. createToken('ios', ['orders:read', 'orders:write']) limits what a token can do; expires_at combined with the sanctum:prune-expired command keeps the table clean and limits the blast radius of a leaked token. Revoke every token on password change. Sanctum's abilities are coarse and checked in your code with tokenCan, which is enough for a first-party app and far simpler than Passport scopes.

When do you actually need Passport and OAuth?

You need Passport when the answer to "who is calling the API?" is "an application we do not control, on behalf of a user who has to consent". Partners integrating with your platform, a public developer programme, a marketplace of apps, single sign-on where you are the identity provider. In those cases you need client registration, redirect URI validation, the authorization code grant with PKCE, scoped consent and refresh tokens, and Passport gives you all of it in a form external developers already understand.

Design scopes as nouns and verbs a partner can read: invoices:read, customers:write. Keep access tokens short-lived and refresh tokens revocable. Keep the client credentials grant for server-to-server integrations with no user in the loop, and never hand a client secret to a mobile or browser app; that is what PKCE exists for.

If you need to consume someone else's OAuth (sign in with Google, connect to an accounting platform), that is Socialite or a client library, not Passport. Passport is only for when you are the provider.

What are the common mistakes with Sanctum and Passport?

  • Running both because a tutorial said so. Pick one per API. Two auth guards on the same routes are a source of confusing 401s and a wider attack surface.
  • Storing a Sanctum token in local storage for a same-domain SPA. Use the cookie; that is the whole point.
  • Tokens that never expire. Set expiration in the Sanctum config or per token, and prune.
  • Forgetting EnsureFrontendRequestsAreStateful in the API middleware group, then wondering why the cookie is ignored.
  • Treating abilities as authorization. tokenCan('orders:write') says what the token may attempt; a policy says whether this user may touch this order. You need both.
  • Choosing Passport "in case we need OAuth later". Moving from Sanctum to Passport when you do is a contained job; carrying Passport for years for a single SPA is a permanent tax.

Authentication is decided in the first fortnight and lived with for the life of the product, which is why it is one of the things we settle in the discovery workshop before any scope is priced.