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

User1980's avatar

Laravel Sanctum problems - Cannot authenticate

I have successfully set up Sanctum on Laravel 8. However, when I try to use Postman with the bearer token obtained from the personal_access_tokens table, I encounter the following error message. Could it be the stateful setting? As I am not communicating to the API form the same domain but from Postman and then my real project will be sending data from another window computer to my website. I have been for 4 days on this, I cannot find an answer to the issue.

"I would like to reassure you that the page is not undergoing maintenance(php artisan up)."

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>

<head>
	<title>503 Service Unavailable</title>
</head>

<body>
	<h1>Service Unavailable</h1>
	<p>The server is temporarily unable to service your
		request due to maintenance downtime or capacity
		problems. Please try again later.</p>
</body>

</html>

api.php:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\V2\Api\KeywordsController;


Route::middleware(['auth:sanctum', 'abilities:db:view,db:update,db:store'])->group(function () {
        Route::get('fetch-keyword', [KeywordsController::class, 'fetchKeyword']);
        Route::put('update-keyword/{keywordName}', [KeywordsController::class, 'updateKeyword']);
});

auth.php:

<?php

return [

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'sanctum',
            'provider' => 'users',
            'hash' => false,
        ],
    ],


    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

    ],

    'verification' => [
        'expire' => 1440,
    ],


    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 180,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => 10800,

];

Sanctum.php:

<?php

use Laravel\Sanctum\Sanctum;

return [

    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),


 'guard' => ['api'],

    'expiration' => null,

    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],

];

Traits in the User Model:

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Paddle\Billable;
use Spatie\Permission\Traits\HasRoles;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens, HasFactory, Notifiable, Billable, HasRoles;

.....more code not attached

Kernal.php:

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];


    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
        'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
        'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
    ];

}

This is how my token is created:

 public function update(Request $request, $id)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => ['required', Rule::unique('users')->ignore($id)],
            'has_token' => 'nullable|boolean',
            'password' => $request->filled('password') ? 'required|min:8' : '',
            'token_name' => [
                'nullable',
                'string',
                Rule::requiredIf(function () use ($request) {
                    return $request->has_token === 1;
                }),
            ],
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }

        try {
            $user = User::find($id);

            if ($request->email !== $user->email) {
                return $this->changeEmail($request->email, $request->password, $id);
            }

            // Update fields
            $user->name = $request->name;
            $user->current_credits = $request->current_credits;

            if ($request->filled('password')) {
                $user->password = Hash::make($request->password);
            }

            $user->save();

            $user = User::where('email', $request->email)->first();
            if ($request->has_token === 1 && $request->filled('token_name')) {
                $user->createToken($request->token_name, ['db:view', 'db:update', 'db:store']);
            } elseif ($request->has_token === 0) {
                $user->tokens()->delete(); 
            }

        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 500);
        }

        return response()->json(['success' => 'success'], 200);
    }

I can see the token in the database, image attached: https://i.stack.imgur.com/az8vS.png

Thanks in advance!

0 likes
8 replies
krisi_gjika's avatar

perhaps it has something to do with your abilities middleware? Check your logs. Try to follow the request and see where it fails. Put a dd() in your controller first. If it does not reach there try your middleware or form requests.

Also provide the route that you are trying to reach.

1 like
User1980's avatar

@krisi_gjika I tried Thanks for your help. I checked it further and I htink I know what is going on. In the bearer token I use the token that is shown in the database "as is" but it looks like you can only use the decoded version and not the encoded version in the request. My problem is that I have na input field with an "eye" show or hide the string and would like to show the decoded version on request form my admin panel like you see in many other API admin panels. But this only works upon the creation of the token:

$token = $user->createToken('authToken')->plainTextToken;
      ```

I would need to decode it after(even when the token has been created weeks ago).

I cannot work it out.

Any idea how to do this please?

Thanks
1 like
krisi_gjika's avatar

@User1980 that is not recommended as it's a security risk, it's the same as showing the user his password as plain text on his profile page

1 like
User1980's avatar

@krisi_gjika Yes you are right, but in many platforms like openAi for example, you can see your token and past it inside your app. This is what I have just achieve now, the user will only be able to see his token once and then it will get hidden. But I am still having issues, I got the right decoded token, no abilities set and still get a 503 error. I wonder if ths problem is here:

<?php

use Laravel\Sanctum\Sanctum;

return [

    /*
    |--------------------------------------------------------------------------
    | Stateful Domains
    |--------------------------------------------------------------------------
    |
    | Requests from the following domains / hosts will receive stateful API
    | authentication cookies. Typically, these should include your local
    | and production domains which access your API via a frontend SPA.
    |
    */

    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        Sanctum::currentApplicationUrlWithPort()
    ))),

    /*
    |--------------------------------------------------------------------------
    | Sanctum Guards
    |--------------------------------------------------------------------------
    |
    | This array contains the authentication guards that will be checked when
    | Sanctum is trying to authenticate a request. If none of these guards
    | are able to authenticate the request, Sanctum will use the bearer
    | token that's present on an incoming request for authentication.
    |
    */

 'guard' => ['api'],

    /*
    |--------------------------------------------------------------------------
    | Expiration Minutes
    |--------------------------------------------------------------------------
    |
    | This value controls the number of minutes until an issued token will be
    | considered expired. This will override any values set in the token's
    | "expires_at" attribute, but first-party sessions are not affected.
    |
    */

    'expiration' => null,

    /*
    |--------------------------------------------------------------------------
    | Sanctum Middleware
    |--------------------------------------------------------------------------
    |
    | When authenticating your first-party SPA with Sanctum you may need to
    | customize some of the middleware Sanctum uses while processing the
    | request. You may change the middleware listed below as required.
    |
    */

    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],

];

As you can see, there are: 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,

But I am only using a token from an external app, I am not using the same domain as where Laravel is installed, do I still need the above? I am kind of confused because the verify_csrf_token should be only when you have both the GUI and laravel app on the same domain no?

1 like
vincent15000's avatar

@krisi_gjika @user1980 However you need to see the plain text token if you want to use it, otherwise you can't use it an then there is no need to create a token.

User1980's avatar
User1980
OP
Best Answer
Level 4

Ok everyone I worked it out at the end..... In Sanctum.php I has the guard set to api instead of web:

 'guard' => ['web'],

In the service provider I had to add this:

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'paddle/*',
        'sanctum/token'
    ];
}

As there is no CSRF verifications when it is pure API communications(no logins).

1 like

Please or to participate in this conversation.