Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ms1987's avatar

Laravel Stateless API authentication done right

Dear all,

I have spent quite some time to try and figure out a correct and secure implementation of a Laravel stateless API. The purpose of this topic is two-fold. I find that there is so much incorrect and even downright dangerous blogposts out there, that I wanted to share my findings here. Secondly I do have some questions left, where I am looking for feedback/peer review.

Lets start off with my prerequisites when starting this journey.

  • I want my API to be fully stateless (no sessions!), so scaling it becomes a breeze. Just deploy it to another instance, add a loadbalancer to the mix (make sure you have a share database of course, and possibly a redis instance).
  • I did not want to use an Oauth approach, as my API will only couple with a SPA (Vue frontend) and it feels like an overkill to have a full Oauth flow for just one consumer and not machine-to-machine communication
  • I did not use Laravel Sanctum, as that is a session based implementation.

As an Implementation it goes like this:

  • Upon successful registration, I generate a JWT token for the user that is set in an encrypted cookie. The cookies has Same-Site: Strict , HttpOnly: true, Path: my-domain.example, Secure: true.
  • That JWT token contains only one thing: the UUID of the user (I choose to use UUID's everywhere instead of regular ID's)
  • When a request is made from the frontend to the API, the withCredentials: true flag makes sure that axios (or whatever http client you use), sends the cookie along with the request).
  • My Laravel API had a middleware that takes care of looking at the JWT token, making sure that it is valid, getting the user UUID from the JWT Token and performing the user lookup. (upon lookup, I have some checks in place to make sure that the user account is in the correct status etc). If all of this is successfull, I set the retrieved user via Auth::setUser()
  • I have a cors implementation in place that performs the necessary checks. Being a check on allowed methods (in my case, I allow for GET, PUT,POST, DELETE, PATCH and OPTIONS), a check that the request originated from a trusted origin, that it contains the right headers that I am expecting.

Security thoughts, opinions around this

  • I choose for a cookie based authentication flow, to make sure the frontend becomes immune to XSS, as my Javascript cannot access the cookie that contains my auth information. Is is really disturbing to see how many tutorials you find out there that actually advise you to store you JWT in localStorage. (I will share a link at the bottom of this post, that provides insights to why that is a horrible idea).
  • By making sure that my cookie is set tot httpOnly and SameSite strict, it helps protect me from CSRF attacks (more on that later).
  • By leveraging a CORS implementation, I can provide an additional layer of security by making sure that the requests are only made using methods I allow, as well as originate from origins I allow. Setting the SameSite to strict forces you to be specific about your origins. You can not use wildcard origins when using samesite strict.

Questions

  • Does this suffice in terms of CSRF protection? I find mixing information regarding the CSRF protection in this workflow.
  • The SameSite:strict behaviour confuses me a bit. When reading up on it, I find articles like this: https://blog.heroku.com/chrome-changes-samesite-cookie . Stating that when you are authenticated on my API (meaning you have a valid cookie), SameSite:strict should make sure that the cookie does not get sent along when I send a user from one domain to my domain. So as an example: somesite.com contains an < a > tag that has a link to mysite.com. When i try this, I do see that the cookie is being sent along. Which makes me wonder how secure it actually is. Because that would mean that you could potentially fabricate a url on somesite.com to perform a malicious action on mysite.com. I do understand that this is not CSRF in the pure sense (as you are not really submitting a form to mysite.com from somesite.com). But an AJAX request or < a > seems possible. Could someone shed some light on this for me?
  • Right now, my implementation sets a cookie with a lifetime that is in perfect sync with the TTL of my JWT token. Upon expiry of this lifetime, the user is being forced to re-login. Does someone have a propose how you could/would handle token refresh in a safe way? (by safe, i mean both safe in ajax usage (multiple calls might come in at once).

I notice that there is sooooo little (next to none that i could find) information about this kind of setup, that I would consider making a proper writeup/blog post somewhere once we can refine the approach a bit more here. Or Maybe even create a Laravel package for it (though I never really did that before).

I will close up here :-) Overall I am looking for (informed) opinions on this workflow and possible answers to my questions.

As promised, the link to the blogpost: https://dev.to/rdegges/please-stop-using-local-storage-1i04 (he is a bit fanatic maybe, but does make some very valid points).

Looking forward to opinions and answers about similar implementations by peers.

1 like
3 replies
bugsysha's avatar

I think that Taylor Otwell had similar concerns and cause of those he created Laravel Sanctum. I haven't used Sanctum, but I know the problems you are facing. I guess that the situation hasn't changed on the topic and that there is no perfect solution yet.

As for scaling, I've sooner reached the issues with the database than code.

fadhilinjagi's avatar

I'm in the process of building a stateless API. To answer your concerns:

  1. CSRF - I think you have to actually implement a CSRF defense as detailed here (owasp.com). By default, Laravel uses the Synchronizer token pattern with sessions. If you want to go stateless, you will need to use Double Submit Cookie technique. To summarize it: Have a GET /csrf-cookie endpoint that generates the CSRF token and sets 2 cookies. One: X-XSRF-TOKEN - the plaintext token (unencrypted). This is passed for convenience so libraries like Axios can easily set it as X-XSRF header. Two: XSRF-VERIFY - a HMAC of the plaintext token. You should encrypt this for security. This will be used to verify the CSRF token without storing anything on the server side. A middleware should do the X-CSRF checking on state-altering requests (POST, PUT, PATCH, DELETE).

  2. Refreshing login/access tokens. You can set a remember_me token in users table. When a user logs in and specifies remember_me, the application sets the remember_me cookie to the token. On authorization, the application should refresh the token (generate new token and extend expiry) if the token is near expiry and the remember me token is valid. This ensures that you refresh the access token for regular app users only, while their access tokens are still valid so its seamless while still preventing replay attacks.

fadhilinjagi's avatar

I think using SameSite: strict is secure yes, but bad for user experience. When they click a link from another site, or enter the application URL on the browser tab, they won't be logged in till they make another request. I'm not even sure how this will affect CORS.

Please or to participate in this conversation.