kevinv's avatar

Sanctum SPA multi auth

I am working on a project that is primarily a backend for a two mobile apps. A partner app and a user app. Users and partners are both stored in different tables as they require different fields, but also so that partners can also be users with the same email address. I also have a need for admins, stored in a third table, who will be able to log in to a web based admin dashboard. Partners should also be able to access a different web based partner dashboard and users should not be able to log in to any web based dashboards, just the mobile app.

Access to the API routes is protected with Sanctum tokens and token abilities stop user tokens accessing routes that are limited to partners.

For the dashboards I'm looking at a React SPA for the admin dashboard and another SPA for the partner dashboard. I'm wanting to handle auth using Sanctum cookie based session auth.

I've read the docs and some tutorials and understand how to implement what I need for a single SPA and how I could customise the login controller to use the correct guard but my current confusion is around the situation with multiple SPA's.

The docs and tutorials show that you add the auth:sanctum middleware to protect routes but to me this seems as though a partner could login to their dashboard but also access the admin dashboard as they'd pass the sanctum middleware if both admin and partner routes were only protected by auth:sanctum.

Is my thinking correct? If my thinking is correct, how would I go about differentiating the guard and protecting two different dashboards with Sanctum?

0 likes
12 replies
rodrigo.pedra's avatar
Level 56

You can have multiple guards using sanctum as a driver, with multiple providers each using a different Eloquent Model.

In you ./config/auth.php file, add these guards:

'guards' => [
    'users' => [
        'driver' => 'sanctum',
        'provider' => 'users',
    ],

    'partners' => [
        'driver' => 'sanctum',
        'provider' => 'partners',
    ],

    'admins' => [
        'driver' => 'sanctum',
        'provider' => 'admins',
    ],

    // ...
],

And in the same file add these providers:

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

    'partners' => [
        'driver' => 'eloquent',
        'model' => App\Models\Partner::class,
    ],

    'admins' => [
        'driver' => 'eloquent',
        'model' => App\Models\Admin::class,
    ],
],

Models App\Models\User, App\Models\Partner, and App\Models\Admin will need to implement the Authenticatable interface.

So you could just copy the User model as a base to the Partner and Admin ones, for example:

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Partner extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

Then in your controllers, and/or route definitions you would use: auth:users for routes that should be for users only, auth:partners for routes that should be for partners only, and auth:admins for routes that should be for admins only.

===

Some thoughts on multiple models authentication

Although I think having multiple guards and providers have valid use cases, (I even use this approach in some apps), by your description I think you would be better using a single User model with a belongsToMany relation to a roles table. You can even use Spatie's package for roles and permissions management.

The additional fields needed for regular users, partners and admins can be added to tables with a belongsTo/hasOne relation. I also use this other approach, in this particular project the additional table for regular users is called members.

The decision point on using one user model or multiple user models is if a screen/route/resource can be accessed by different roles.

The app where I use multiple tables for different roles is a custom e-commerce platform. So a member never sees the admin screens, and while an admin can have a member account (using the same e-mail), if we consider only the data aspect of it, they have access to completely different screens than a member. Essentially it is like two separate apps that share the same DB, but I keep them in the same codebase as they share some business logic related code.

Phrasing it differently, I would go with multiple tables for different roles if each access level access looks like they are accessing a completely different app that shares the same DB.

If different roles have just different permissions on the same set of screens/resources, them I would go with a single users table and model for authentication, use a role management package, and store different fields needed in a related table using a one-to-one relation.

But these are just my two cents, in the end of day use what fits better to your app's requirements and team's preferences.

Hope it helps somehow.

26 likes
hilmialf@gmail.com's avatar

Hello. Thank you for your thorough answer.

I have a question regarding the login procedure. Yes, the sanctum does not limit us on how to perform login. But the default is I think sanctum assumes us to use auth:web to do login. In the case of multiple guards as explained in this thread, how should I perform the login/logout functionality?

Since laravel does not allow driver other than session to call attempt() login, should I prepare additional corresponding web guard which uses session driver for each sanctum guard, as in (userWeb, userSanctum, adminWeb, adminSanctum)?

In addition, I actually did what I mentioned above, I successfully login. But unfortunately, it seems that the session seems different (auth:userSanctum does not recognize the session cookie generated for auth:userWeb). Well, basically what I wanted to know if is there a possibility to set the guard inside __invoke(Request $request) on sanctum/src/Guard[dot]php to point to each corresponding web counterparts?

Or do you have better suggestion for this? I am looking forward to hearing from you.

Thank you.

3 likes
oluwatyson's avatar

Hi mate did you ever solve this issue? I have the same problem

reza305's avatar

@rodrigo.pedra Thanks Rodrigo... I have used this way in my laravel app for SPA Authentication and wrote tests for it, but the problem is when I try to run logout test, It gives me this error:

Method Illuminate\Auth\RequestGuard::logout does not exist.

This is my test that I wrote with Pest:

test('admin_can_log_out', function () {
    $admin = Admin::factory()->create();

    $response = $this->actingAs($admin, 'admin')->json('POST', '/api/v1/admin/auth/logout');

    $this->assertGuest('admin');
    $response->assertStatus(200)
        ->assertExactJson([
            'success' => true
        ]);
});

This is the controller:

    public function logout(Request $request)
    {
        if (!auth('admin')->check()) {
            throw new AuthenticationException();
        }
        Auth::guard('admin')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        return response()->json([
            'success' => true
        ]);
    }

This is the logout route:

Route::post('v1/admin/auth/logout', [AdminController::class, 'logout'])->name('admin.logout');

and This is the main part of my auth.php config file:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

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

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session"
    |
    */

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

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

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

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

];

So where is the problem? By the way, I'm using Laravel 8.

kevinv's avatar

Thanks for your detailed reply. It was extremely helpful.

Your last points about single model vs multiple models has been something that I've thought about many times and never been 100% on the best option. I feel like each access level is accessing a total different app which would go more towards individual guards as per your I would go with multiple tables for different roles if each access level access looks like they are accessing a completely different app that shares the same DB. comment.

1 like
rodrigo.pedra's avatar

If that is the case go for it. I find it easier to manage in situation like those.

Glad it was helpful. Have a nice week =)

1 like
lalitesh's avatar

A little late on this, but I thought to update you all who would like to use multiple guards for sanctum auth in the Laravel.

The latest version of Sanctum ( with Laravel 8) actually supports the multi-guards, you will just need to change config/sanctum.php file to add the required guards, such as:

'guard' => ['web','web_admin'],
1 like
ivand's avatar

@lalitesh Actually I couldn't make this work, because it has a problem with session based requests. If you try to login with web auth, and use sanctum as a guard, you will see that you get response code 200 instead of a 401 for the routes that were meant to be guarded by sanctum admin guard.

This was exactly the thing I needed and that actually worked for me: https://github.com/laravel/sanctum/pull/149

reza305's avatar

@ivand In that case, You shouldn't use web guard anymore, make a custom guard with session driver.

Please or to participate in this conversation.