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

vjupix's avatar

How to use custom guards on API routes

I am trying to get my custom authentication workflow up and running. For this I am using an additional database table called "clients", an additional provider 'clients' and an additional guard called 'vcs' for the authentication workflow. Clients get a signed URL to login. For this I am using the ´Auth´-facade's login-method right after creating a client in the database. The login is working perfectly fine and a session cookie is generated.

The problem is, that all subsequent requests to laravel's API routes result in 401 errors - although the session cookie is sent with the request.

I already posted a very detailed thread over on stackoverflow.com:https://stackoverflow.com/questions/74336763/laravel-9-user-not-authenticated-on-api-routes-using-custom-guard

I did not want to cross-post but lately I don't see any real progress on my laravel-related questions over there and it seems like stackoverflow is no more a good place to ask questions and to get qualified answers for laravel-specific problems.

I would be very glad to find some help over here.

config/auth.php:

<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

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

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

        'clients' => [
            'driver' => 'eloquent',
            'model' => App\Models\Client::class,
        ],
    ],

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

    'password_timeout' => 10800,

];

My client model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Client extends Authenticatable
{
    use HasFactory, HasApiTokens;

    protected $guard = "vcs";

    /**
     * The primary key associated with the table.
     *
     * @var string
     */
    protected $primaryKey = 'uuid';

    /**
     * Indicates if the model's ID is auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

    protected $keyType = 'string';

    /**
     * Get the route key for the model.
     *
     * @return string
     */
    public function getRouteKeyName()
    {
        return 'uuid';
    }
}

The clients get a signed URL which points to the following controller action. The action checks for a valid query parameter in the URL (simplified for this thread). After that a new Client model gets created and the new Client gets logged in using the 'vcs' guard:

<?php

namespace App\Http\Controllers\VCS;

use Illuminate\Http\Request;
use App\Models\Client;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    public function redirectWithCookie(Request $request)
    {
        // reduced for the sake of simplicity here
        $credential = $request->someURLParameter;
        if ($credential) {
            $client = new Client;
            $client->uuid = Str::uuid()->toString();
            $client->ip = $request->ip();
            $client->status = 'pending';
            $client->save();
            Auth::guard('vcs')->login($client, $remember = true);
            // this logs the authenticated user correctly!
            Log::info('Authenticated User: ' . Auth::guard('vcs')->user());
            $cookieValue = json_encode(array('uuid' => $client->uuid));
            $cookie = cookie('mycookie', $cookieValue);
            $redirectUrl = config('my.redirect.url');
            return redirect()->away($redirectUrl)->withCookie($cookie);
        }
        return response(['message' => 'Invalid URL', 'error' => 'url'], 422);
    }
}

routes/web.php:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\VCS\AuthController;

Route::get('/', function () {
    return ['Laravel' => app()->version()];
});

Route::get('vcs/auth', [AuthController::class, 'redirectWithCookie'])->name('vcs.auth');

require __DIR__.'/auth.php';

routes/api.php:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\VCS\RoomController;

Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) {
    return $request->user();
})->name('profile');

Route::middleware(['auth:vcs'])->group(function () {
    Route::get('rooms', [RoomController::class, 'rooms']);
});

After the redirect I get a laravel_session as a cookie which should authenticate my subsequent requests. The problem is that I can't call any API routes with the custom guard and I am not authenticated anymore although the browser is sending my session cookie with the request. For example calling the /api/rooms GET-endpoint defined in the api.php results in a redirect to the login page.

I also see that the user is not authenticated in the auth-middleware:

<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {
        Log::info('Authenticated User: ' . Auth::guard('vcs')->user());
    }
}

The Log just returns an empty string so the user is not authenticated:

[2022-11-06 13:44:30] local.INFO: Authenticated User:

So my question is: How can I use a custom guard for my API routes after manually logging new users in?

I also tried the same workflow using Insomnia as a REST Client:

Login by URL: enter image description here

whichs gives me a sessions cookie.

Access some API Route: enter image description here

Which results in an Unauthorized-Status-Code..

0 likes
6 replies
Lumethys's avatar

first of all, how do you expect us to help debugging your code without provide the code?

Second of all. Why are you using a custom auth system? Unless you are an expert in both security and the language you are working on, you should never write a authentication system from scratch. Laravel had first party support for most authentication feature via Breeze and Jetstream, plus a Oauth client (sign in with google, facebook, etc.) via Socialize. They even have first party support to make yourself a Oauth provider via Passport

vjupix's avatar

@Lumethys Thanks for your response.

I added all the code I think is necessary.

Related to your 2nd question: I know exactly what I am doing and using another first/third party package is not working in my described scenario. Using a 2nd table for temporary users which holds custom access tokens for the third party system in use is the only option I found to work.

The workflow is like this:

  1. User clicks on a registration link
  2. (Temporary) User is created and saved to the database including an personal access token. The user is also logged in using this temporary user (using Laravel's Auth Facade)
  3. The Personal Access Token is returned to the frontend client in the redirect url
  4. The frontend picks the token from the url and makes a request to the third party system passing the access token as a query parameter
  5. The third party system makes a request to laravel and asks for authenticating the request (only the requested URL including query parameters e.g. the token, the user uuid is sent to laravel)
  6. Laravel checks if there is a uuid with an associated token in the database
  7. After the third party system finished the process it signals this to laravel and laravel deleted the temporary user from the database. The process needs about 2 hours to finish.

I am also using Breeze/Sanctum for authenticating my "normal" users so I am aware of those mechanisms.

vjupix's avatar

@Lumethys I think I don't understand. As you can see in the code I don't use an oAuth Provider, just another eloquent Table as a user provider. I don't use Laravel Passport, because I do not have any influence on the third party system and how the requests are sent from that system so I am forced to use the custom guard as described.

Lumethys's avatar

@NKODING you are mistaken between an auth provider vs an auth client

let take an example: you know the Login with Google, right? If you implement a Login with Google button in your website and authenticate using user's google account, Your website is a Client and Google is the provider

Being a Client does have anything to do with having control on the provider. Obviously you are not modifying Google's codebase if you want to have a Login with Google button on YOUR website, no?

back to Laravel, Passport is the tool to make yourself a provider, obviously you do not need this. However, are you sure Socialize is not the right tool for you?

Well even IF you cannot use Socialize, you should take a look at its source code to handle API token

vjupix's avatar

@Lumethys Socialize is for authenticating with third party services like Google Accounts etc. as you mentioned. This has nothing to do with the system I am implementing, because user's do not authenticate using a third party system. Also, I just mentioned that the authentication workflow seems to work if I just test it with Laravel's HTTP Tests. I also get the authenticated user back in my controller during the tests. So I think it has something to do with the session. It seems like making the requests from the actual frontend using axios running on the corresponding frontend domain should work.

Please or to participate in this conversation.