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

aschorr's avatar

How should I store my access_token for my web API?

I'm making a traditional web app, using Laravel as the API backend and Next.js as my actual app - it can be considered 2 separate apps really. NextJS (the client) and Laravel (the server). For this I'm using passport to auth via the API routes. I can successfully auth and login with my API client, Postman, but I am curious where I should be storing my access_token in the browser?

Localstorage is not secure for this sort of a thing. Is there any sort of best practice for storing this as a cookie in the browser? I'm skeptical of rolling my own.

0 likes
14 replies
Nakov's avatar

Local storage is personal to the user, so there is nothing wrong for storing it there in my opinion, just make sure that when the user logs out, you clear the storage. But that's just my opinion of your point of not being secure.

Here is a good guide for where to store the token:

https://dev.to/jolvera/user-authentication-with-nextjs-4023

The backend in this case is NodeJS I think, but that should not make any difference as you are interested in the front-end.

2 likes
Nakov's avatar

@aschorr okay, that was my personal opinion. But this is a great article. The article I shared above stores the token in a cookie, you've checked it right?

rodrigo.pedra's avatar

Hi @nakov kind of unrelated to the thread but wanted to thank for the article you linked. It shows how to use localStorage in a very clever way to emit an event so all the app's instances (all opened tabs) are sync'ed on user logout. Never thought of using it that way, so thanks for sharing.

Regarding the security - and this thread's subject - that article does not use localStorage for token storage or retrieval, it actually shows how to store the token within a cookie. The localStorage part is just for syncing open tabs when the user logs out, which, in my opinion, it is a better approach than to wait for the next API request on a different tab and still allow sensitive data to be shown.

I am not a Next expert but it seems the cookie is set from a route middleware run by the Next process not by the browser's framework. If that is the case it is a similar approach I used in a project with Nuxt.js (similar to Next.js for Vue) and a Laravel API backend. As the token seems to be managed by the Nuxt/Next process, and if it is sent as a http-only same-site cookie, that should suffice any security requirement as the contents of the cookie would not be manipulated by the browser's JavaScript and can be sent with Ajax requests using withCredentials: true.

aschorr's avatar

That looks super useful, but that seems to be for the instance that you're opening up your API routes to the 1st party laravel app, ie - accessing /api/user from the JS on the same domain that the laravel app is running, as evidence by the CreateFreshApiToken class that needs to be added to 'web' key in Kernel.

I'm planning on accessing these API routes via a wholly different app and domain (via next.js). Let me know if I'm misunderstanding, because otherwise this looks exactly like something I'd need

rodrigo.pedra's avatar

If both apps are in the same domain (even with different subdomains) it should work by using the CreateFreshApiToken middleware.

The only detail is that the POST /login route should be in the web middleware stack, so the cookie is sent on the response. The other routes can be on the api middleware stack as they won't send a cookie in the response. But if you use the withCredentials in your ajax requests (axios, XHR or fetch accept that or a variation of that) the request will send the cookies and passport will extract the token from the cookie.

When using this approach (SPA + Laravel API backend) I also keep a GET /me route in the web middleware stack which return the current user's data. This allows me to fetch the current user from a route that also refreshes the session cookie.

1 like
aschorr's avatar

Ah that all seems suuuuuper helpful, although now that I'm hitting the plain /login route (not /api/login) it complains about a missing CSRF token. Curious how you handle that?

rodrigo.pedra's avatar

Had to open that project to check it out.

I have these routes related to authentication

GET /auth/me  - returns current user data
GET /auth/session  - returns the session token
POST /auth/login  - logs the user in
POST /auth/logout  - logs the user out (destroy session)

All these routes are on the web middleware stack, so responds with cookies.

I have an helper file with this axios config

// libs/http-client.js
import axios from 'axios';
let csrfToken = null;

const httpClient = axios.create();

axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

axios.interceptors.request.use(function (config) {
    if (csrfToken) {
        config.headers['X-CSRF-TOKEN'] = csrfToken;
    }

    return config;
});

axios.interceptors.response.use(function (response) {
    if (status === 419) {
        httpClient.get('/auth/session');
    }

    if (response.data && response.data.csrf_token) {
        csrfToken = response.data.csrf_token;
    }

    return response;
});

export default httpClient;

When the app is created I send a request to /auth/session so the token is retrieved and saved locally and then a request to /auth/me to retrieve the current user. (in Nuxt.js I do this on the 'store/index.jsinside thenuxtServerInit` action, I don't know where to do in Next, but I am sure there is somewhere to do initialization there).

In every component that I need to send a request ajax request I import the axios instance like this (using Vue here):

import httpClient from '@/libs/http-client.js'; // where you saved the file above

export default {
    // ...
    methods: {
        submit() {
            httpClient.post(...);
        },
    },
}

So I always use the axios instance created with those interceptors.

If you don't want to use axios you could export a factory function to always create a XHR or fetch instance with the required headers.

The trick is that after login and logout the CSRF token is regenerated by Laravel. The solution is to redirect to /auth/session after logging in or logging out with status code 303 so the redirect is done automatically by axios/XHR/fetch with the GET method.

As the interceptor will check if the response contains a csrf_token key it will update the token locally correctly.

For reference this are the details on the Laravel side

  1. SessionTokenController
namespace App\Http\Controllers\Auth;

use App\Http\Requests\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Response;

class SessionTokenController extends Controller
{
    public function show(Request $request)
    {
        $csrfToken = $request->session()->token();

        return Response::json(['csrf_token' => $csrfToken]);
    }
}

2.To redirect after login, override the authenticated method in the LoginController

protected function authenticated(Request $request, $user)
{
    return Redirect::to('/auth/session', 303);
}

3.To redirect after logout, override the loggedOut method in the LoginController

protected function loggedOut(Request $request)
{
    return Redirect::to('/auth/session', 303);
}

This way after logging in and logging out the SPA CSRF token will be synced with the refreshed token from the Laravel session.

Last tip: increase the session expiration. As most of the time your SPA will interact with Laravel through the API stack the session token will not be renewed when it expires. So it is a good idea to increase the default session expiration time to avoid dropped API calls.

2 likes
Lattespirit's avatar

You are right.

Beyond the CreateFreshApiToken, it just creates a JSON Web Token. I prefer to use a jwt package in backend service for the case separating client and server apps.

rodrigo.pedra's avatar

@lattespirit and how do you persist the JWT in the frontend to avoid logging out on a hard refresh? (user hits F5 or open a link in a new tab)

The whole argument before is not to store in localStorage for security reasons highlighted in the articles linked in the previous answers.

Lattespirit's avatar

Hi, i mean using encrypted cookie, not localStorage.

And as far as i know, localStorage will not be sweeped after hard refreshing.

Let me know if i have any mistakes.

rodrigo.pedra's avatar

Sorry, I was not clearly regarding localStorage. localStorage is indeed persistent across requests and even when user closes the browsers and opens it lately. But it also allows direct manipulation of its content by the local JavaScript process and its content can be seen directly in the browser's console. Those are some of the reasons some people do not recommend stroring auth data using localSotorage.

For example, Any JavaScript code can manipulate it, so a third party script could read all localStorage and upload to somewhere else without user knowledge. One of the articles linked in the previous responses lists this an other reasons for not using it.

For non-sensitive data, like the mechanism explained in the first linked article for syncing tabs when the user logs out, using localStorage is just fine.

The we get to the need to send an encrypted cookie and decrypting it server side on each request. That is what using Laravel Passport helps us. Passport will automatically extract and decrypt the auth token from a session cookie automatically.

Of course you can manage the JWT and the encrypt/decrypt manually by hand, but, in my opinion, using a community maintained package is a better approach as you don't have to keep track yourself of all security vulnerability and have more people finding and correcting bugs on an import part of the stack.

One other benefit is using the same API endpoints for other consumers. In your first party (same domain) app we leverage the security and DX by using the CreateFreshApiToken middleware and have Passport handle token exchange for us. For other parties they can use the regular OAuth authentication to get an access token and send thisa token as an Authorization header and Passport will make it work.

But again, this is my opinion, in the end use whichever fits better your project.

Please or to participate in this conversation.