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

iamjaredsimpson's avatar

Unauthenticated when using Sanctum Token

I am working on an SPA that will need to communicate with a browser extension. Sanctum is working correctly when using the SPA authentication to access the api, but I can't seem to get it to authenticate when using the token.

I have created a new route to check the login details from the extension and either generate a token or pass back the previously generated token for a specific browser. This appears to be working correctly.

public function token(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
            'device_name' => 'required'
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        if(!$user->tokens()->where('name', $request->device_name)->first()) {
            return $user->createToken($request->device_name)->plainTextToken;
        }

        return $user->plainTextToken($request->device_name);
    }

I'm then immediately sending an axios request with the returned token. Here is a simplified version of my Axios request in the extension:

axios.defaults.headers.common['Authorization'] = `bearer ${token}`;
axios.defaults.headers.common['Accept'] = 'application/json';

axios.get('http://site.localhost/api/files/index')
	.then(response => {
            this.files = response.data;
        });

Route in routes/api.php:

Route::middleware('auth:sanctum')->get('/files/index', 'Api\FileController@index');

But for whatever reason, I can't seem to get this to authenticate correctly, despite the SPA version of sanctum working fine. Does anyone have any suggestions?

0 likes
25 replies
iamjaredsimpson's avatar

@fylzero Thanks for your reply. I went ahead and added this, but it didn't seem to help. I was also under the impression that this was only needed when using the SPA authentication. I do have SPA auth in my Laravel app and it is working correctly. The requests giving me problems are being made from my chrome extension though. It is attempting to authenticate using Sanctums token authentication.

fylzero's avatar

@iamjaredsimpson Does your User model have HasApiTokens?

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

This might be a stupid suggestion but usually when I've seen bearer tokens entered, they have a capital B in Bearer. Maybe try that?

iamjaredsimpson's avatar

@fylzero Right, so I have the HasApiToken on my user model. I'd post the code, but it looks pretty much the same as your snippet. I also tried changing bearer to uppercase, but that didn't seem to change anything. I should note that I've also tried the api url in Postman with their authentication header set to Bearer and my token and I got the same response there.

I was wondering if it was a problem with my Cors settings, but I'm not receiving a No 'Access-Control-Allow-Origin' error, so I don't think it's that. Just in case, here are my settings in my config/cors.php file.

'paths' => [
        'api/*',
        '/login',
        '/logout',
        '/token',
        '/sanctum/csrf-cookie'
    ],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

And my .env file has the following

SANCTUM_STATEFUL_DOMAINS=site.localhost,localhost,127.0.0.1
SESSION_DOMAIN=site.localhost

I really have no idea what is going wrong. Every other help thread I've seen makes it look like it's set up correctly.

iamjaredsimpson's avatar

@fylzero So I made a breakthrough. It's not quite fixed yet but I think I should be able to figure it out from here. I noticed that if I delete the key from the database and try a fresh login from the extension, it will generate a token and then move on to the request that is failing. In this instance, it has started working (!!!!!). It also works in postman with this new token. However, I'm also storing the token in the extensions storage for future use. When I try to open the extension when this save token is used, it doesn't seem to work. So I'm thinking there is something happening there that is causing it to fail.

I appreciate your help and I should be able to move forward from here. Just knowing that it works in one specific use case should be enough for me to hunt down why it's failing in other situations.

iamjaredsimpson's avatar

For anyone who finds this thread, the problem was that the line below in my original post was returning the wrong value.

$user->plainTextToken($request->device_name);

This was a method that I added to my user model that returned (what I thought) was the plain text token. It turns out that the plain text token is the id, a separator, and a hashed value of the token stored in the db. Since I don't have access to the hashed value, you cannot resend it. So the solution was update the last few lines of my token method to always create a new token when they sign in.

public function token(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
            'device_name' => 'required'
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $user->tokens()->where('name', $request->device_name)->delete();

        return $user->createToken($request->device_name)->plainTextToken;
    }

4 likes
AccessTruck's avatar

@iamjaredsimpson You are a life saver. Thank you so much for documenting the solution. I spent all day on the same exact problem and this fixed it for me 2 years later.

1 like
nomikz's avatar

I am not sure what is the problem. I hit /sanctum-csrf, and /api/login successfully but when I make /api/user, I get "Unauthenticated.".

1 like
nomikz's avatar

No. I decided not to use nuxt-auth. I failed to understand how to use it properly.

saidu's avatar

im having the same problem, although mine happens when i try to auth private channels

meshack007's avatar

Having the same problem, postman works but when i make a request from react-native it returns unauthenticated.

kossa's avatar

Any solution for that ? I'm using Http facade, got the same error

Clement_Te's avatar

Don't know if my experience might help some of you.. but I had the same issue where the token from the database didn't work and through me a "Unauthenticated" error message.

Please note that the value stored in the database is hashed with SHA 256. Check out the doc : https://laravel.com/docs/8.x/sanctum#issuing-api-tokens

The actual token can only be retrieved right after creating it, thanks to the NewAccessToken class of Laravel Sanctum.

1 like
Hadayat's avatar

Great dude, it also solved my problem.

Hadayat's avatar

Change your env file as below.

Add your API domain and client domain (Please avoid the white spaces in the env file). My API is running on the localhost:8000 and the client is running the localhost:3000 port.

SANCTUM_STATEFUL_DOMAINS=localhost:8000,localhost:3000

If you are running your apps on the localhost add only the localhost in the session domain, like below.

SESSION_DOMAIN=localhost
4 likes
mmasomi73's avatar

i Have Same Problem Test this Solution, It Work...

Zanafar's avatar

tried to fix this error for 5 days on my laptop))) Fixed when added ports for frontend 8080 on stateful domain string. And now is second day I m trying to fix this when uploaded to production. Since there is no port anymore it not helps. without port not working. with :80 port either... I believe there would be much less problems if in laravel sanctum documentation would be more info about how exactly fill statefull strings; when need domain session string and what to type there.. And finally would be easier if we have more error messages. Not only "message: "unauthenticated", but "message": wrong token" "message": "csrf problem" "message": "wrong session" "message": "domain not in stateful domains" etc

how can we understand where is the problem...

2 likes
jrran90's avatar

Hi,

Have you checked your Kernel.php? app/Http/Kernel.php

Make sure you uncomment \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, coz by default it is commented out

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

This is the only thing that worked for me.

1 like
bons's avatar

I'm having the same issue with Laravel 8 and Vue 3. While "/sanctum/csrf-cookie" and "/api/token" works OK the next request to "/api/me" returns "Unauthenticated.". The "EnsureFrontendRequestsAreStateful" is in http/Kernel and User model has "HasApiTokens".

app.js

axios.defaults.baseURL = process.env.MIX_APP_URL + "/api";
axios.defaults.withCredentials = true;

bootstrap.js

window.axios = require('axios');
# Even if I add withCredentials = true here it ends up the same
# window.axios.defaults.withCredentials = true;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

I also checked "public/.htaccess" as many point out. File was also moved to root folder and to the root folder of project

RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

# I also tried replacing above line with
# RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
SANCTUM_STATEFUL_DOMAINS=domain.com
SESSION_DOMAIN=.domain.com

for SESSION_DOMAIN it didn't/doesn't matter if I had/have leading "." or not. I also can't add a port since it is not running locally but on shared hosting (cPanel).

I save the Bearer token to header as follows

axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;

api.php

Route::post('/token', [LoginController::class, 'token']);
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/me', [LoginController::class, 'me']);
});

Clearing cache didn't solve it either

php artisan optimize:clear
php artisan config:cache
...

cors.php

'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,

Requests to "/sanctum/csrf-cookie", "/api/token" and to "/api/me" are all async await.

Inspecting the "/api/me" request in network tab I can see that the Bearer token is attached to authorization header.

What I really don't understand is that locally and on cPanel "B" (dev) works OK while on cPanel "A" (prod) it returns "Unauthenticated.". At this point I don't know what to check or change anymore. Any tips or points to some directions would be greatly appreciated.

mrojas18's avatar

the problem is related to apache or .htaccess

you have to add to the config this lines otherwise, the bearer token never get the controller

RewriteCond %{HTTP:Authorization} ^(.+)$ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

https:// laravel.com/ docs/8.x/ authentication#a-note-on-fastcgi

KreisonReis's avatar

The issue I faced was that the tokenable_id column in the personal_access_tokens table was created as a bigInt, while in my User model the primary key is a string. My user IDs start with a leading zero, for example: "0123456789". However, when stored in the personal_access_tokens table, the value was saved as "123456789" — dropping the leading zero. As a result, authenticated routes would always fail because the token couldn’t be matched to the correct user.

The solution was to modify the migration to use:

$table->string('tokenable_type');
$table->string('tokenable_id');
1 like
TitanDvd's avatar

Do not move or edit the .htaccess in ./public directory. Create a virtual host with xampp or wampp or any other and point the root dir to /public. In case of shared host, you need to make (if I well remember) hot-links from domain root public_html to laravel public dir. And from inside public dir, another hot-link to storage. That way I managged to run laravel in hostinger with livewire.

Please or to participate in this conversation.