Backend Development

Laravel 8 Sanctum Authentication For SPA and Mobile APIS

Laravel 8 Sanctum Authentication For SPA and Mobile APIS

Laravel 8 Sanctum package provides a way to authenticate users through Token based authentication or using the normal cookie based authentication which make this a good choice for SPA and mobile apps authentication.

 

 

 

In previous releases of Laravel, in order to implement authentication process through Api, there were methods such as JWT or Laravel Passport. However, in Laravel 8, there is more than one way to implement Api authentication, such as Fortify and Sanctum. In addition to handling Api authentication Sanctum can be used to implement the normal form laravel authentication. So let’s start installing and use Sanctum.

 

Install laravel/sanctum using composer:

composer require laravel/sanctum

Next publish sanctum configuration and migration files using vendor:publish:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

After running this command you should see the sanctum config file located at config/ directory and the migration file in database/migrations.

config/sanctum.php

<?php

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',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
    )),

    /*
    |--------------------------------------------------------------------------
    | Expiration Minutes
    |--------------------------------------------------------------------------
    |
    | This value controls the number of minutes until an issued token will be
    | considered expired. If this value is null, personal access tokens do
    | not expire. This won't tweak the lifetime of first-party sessions.
    |
    */

    '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,
    ],

];

The stateful and middleware settings used with SPA only. stateful setting is an array that specifies the domains and subdomains the SPA will work on. The middleware setting specifies the middlewares in order that will be applied on SPA POST and PUT requests.

The expiration setting specifies expiration in minutes for the tokens to expire, in case expiration=null this means token not expire.

 

Next migrate the database using:

php artisan migrate

Sanctum will generate just one table for storing the API Tokens. Each user can have one or more token. Now after the database is migrated let’s see how we can issue API Tokens for token based apis like mobile applications.

 

 

Authentication With API Tokens

At first let’s update the Models/User.php to use the Laravel\Sanctum\HasApiTokens trait like so:

use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;
}

HasApiTokens trait provides various method like createToken() to create new user token, tokens() to get a list of user tokens, currentAccessToken() to get the currently active access token used in the authentication.

When calling createToken() return an instance of Laravel\Sanctum\NewAccessToken which return SHA-256 hashed token to be stored into the database. When sending tokens between requests you should send the plain text version of this token like so:

$token = $request->user()->createToken($request->token_name);

return ['token' => $token->plainTextToken];

Now let’s create a new controller called AuthController.php and add these methods:

app/Http/Controllers/AuthController.php

<?php


namespace App\Http\Controllers;


use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    public function token(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => '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.'],
            ]);
        }

        return response()->json(['user' => $user, 'token' => $user->createToken($user->name)->plainTextToken]);
    }

    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required'
        ]);

        $user = new User();
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = Hash::make($request->password);
        $user->save();

        return response()->json(['user' => $user, 'token' => $user->createToken($user->name)->plainTextToken]);
    }

    public function profile(Request $request)
    {
        return response()->json(['user' => $request->user()]);
    }

    public function refresh(Request $request)
    {
        $user = $request->user();

        $user->tokens()->delete();

        return response()->json(['token' => $user->createToken($user->name)->plainTextToken]);
    }
}

The token() method used to authenticate the user using email and password, and return a new token using $user->createToken(name) method, createToken() method accepts an argument which is the token name, in this case i passed the user name as the token name.

In a mobile app API this token must be stored in some place to be used in other requests and sent along with the Authorization header as a bearer token.

The register() method is straightforward which register a user account and also return a user token. The profile() method used to return the current user details, however this method must be invoked inside the “auth:sanctum” middleware as we will see in the routes below.

The refresh() method also must be invoked inside the “auth:sanctum” middleware and used to clear all the user tokens by calling $user->tokens() relationship and calling $user->tokens()->delete() and return a new token.

 

Now add the required routes in routes/api.php

<?php

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


Route::post("/token", [AuthController::class, "token"]);
Route::post("/register", [AuthController::class, "register"]);
Route::middleware('auth:sanctum')->get("/user", [AuthController::class, 'profile']);
Route::middleware('auth:sanctum')->get("/refresh", [AuthController::class, 'refresh']);

As you see the /user and /refresh routes is protected and called inside the Route::middleware(‘auth:sanctum’) middleware.

I used Postman to test these routes as shown in these screenshots:

 

Laravel 8 Sanctum Authentication For Mobile APIS - register2

 

Laravel 8 Sanctum Authentication For Mobile APIS - token

 

Laravel 8 Sanctum Authentication For Mobile APIS - user data

Laravel 8 Sanctum Authentication For Mobile APIS - refresh token

 

 

Authentication In SPA

The laravel sanctum can be used to authenticate Single Page Applications that comes in the same laravel project like Vuejs or Reactjs that shipped with laravel.

For this to work sanctum won’t use API Tokens like we saw above instead the authentication process use the Laravel built-in cookie session authentication services. With this way we have to send the CSRF token along each POST or PUT request otherwise Laravel will reject the request.

Fortunately sanctum provides us with “sanctum/csrf-token” endpoint, for example when making ajax request to a login route we must first send a GET request to this endpoint to generate the csrf token and then send it along with login request.

As per Laravel docs for the authentication to work the SPA and API must share the same top-level domain otherwise the process will fail because cookies not work if the domains  is different.

Open config/sanctum.php and update the stateful setting and set all the domains separated by comma that will receive API authentication cookies, for example in localhost i set 127.0.0.1, localhost, 127.0.0.1:8000

'stateful' => explode(',', env(
        'SANCTUM_STATEFUL_DOMAINS',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
    )),

Next open app/Http/Kernel.php and add the sanctum middleware to the api middleware group like so:

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

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

Another issue to care about when making ajax request is the CORS problem which usually happens when using javascript http clients like axios. So open config/cors.php and update supports_credentials to true:

'supports_credentials' => true,

Then if you are using axios in resources/js/bootstrap.js add those lines:

axios.defaults.withCredentials = true;
window.axios.defaults.baseURL = "http://127.0.0.1:8000";

Now let’s make a simple login Vuejs component:

<template>
    <div class="container">
        <div v-if="this.message!=''">{{this.message}}</div>
        <ul v-if="this.errors.length">
            <li v-for="error in this.errors" :key="error">{{error}}</li>
        </ul>
    <form method="post" v-on:submit.prevent="submit()">
       
       <p>
         <label for="email">
                            Email
                        </label>
<input type="text" name="email" id="email" v-model="email">
      </p>

     <p>
       <label for="password">
                            Password
                        </label>

      <input type="password" name="password" id="password" v-model="password">
     </p>

     <p>
        <button type="submit">
                    Login
                </button>
     </p>
    </form>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
          return {
              email: "",
              password: "",
              message: "",
              errors: []
          }
        },
        methods: {
            submit() {
                this.message = "";
                this.errors = [];

                window.axios.default.get("/sanctum/csrf-cookie").then(response => {
                   window.axios.default.post("/api/login", {email: this.email, password: this.password}).then(response => {
                       console.info(response.data.user);
                       // handle login success i.e redirect the user to the profile page
                   }).catch(errors => {
                       this.message = errors.response.data.message;
                       for(let key in errors.response.data.errors) {
                           this.errors.push(errors.response.data.errors[key][0]);
                       }
                   });
                });
            }
        }
    }
</script>

As you see before i send the login request i made a request to “/sanctum/csrf-cookie” endpoint which generate the csrf token. During this request Laravel set an “XSRF-TOKEN” cookie, if you check the axios response headers you will see this cookie. This cookie should then be passed along with subsequent requests as an “X-XSRF-TOKEN” header.  So here when we send the login request axios captures the returned cookie from “/sanctum/csrf-cookie” and add this as a header to the login request.

Note that axios will receive this token automatically and send it as a header, so you don’t have to send it yourself. If you use any http client other than axios you should ensure that you set this header.

Now add the login() method to the AuthController.php

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

        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return response()->json(['user' => Auth::user()]);
        }

        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

Here i implemented the login code manually using Auth::attempt() method.

Add this route in routes/api.php

Route::post("/login", [AuthController::class, "login"]);

To protect routes use auth:sanctum middleware:

Route::middleware('auth:sanctum')->get("/user", [AuthController::class, 'profile']);

 

 

5 2 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
1
Confused
0

You may also like

Subscribe
Notify of
guest
8 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
willian Babuu
willian Babuu
1 month ago

thank you for this amazing tutorial it work a lot for me. but I have one problem in loging in the response I get is 404 not found. It seems that vue could not get /api/sanctum/csrf-cookie

willian Babuu
willian Babuu
1 month ago
Reply to  Wael Salah

thank you for reply, I have done so all other apis work only expect for /sactum/xsrf-cookie. here is my code
loginUser(){
            axios.post(‘http://127.0.0.1:8000/api/sanctum/csrf-cookie’).then(response =>{
                window.axios.default.post(‘http://127.0.0.1:8000/api/login’,this.form).then(response =>{
                    console.log(response.data.user)
                })
                // this.$router.push({ name: ‘Dashboard’})
            }).catch((error=> {
                this.errors = error.response.data.errors
            })
        }

willian Babuu
willian Babuu
1 month ago
Reply to  willian Babuu

i have solved it, thank you sir for your tutorial

willian Babuu
willian Babuu
1 month ago

i was able to authenticate but I dont ‘“XSRF-TOKEN” and i could not get user details and I wonder how I could get user info

ArtJam
ArtJam
23 days ago

Hi, Thanks for the tutorial, I would like to know if I can use both in one application.
Session authentification for my SPA
And Token-based authentification for API’s (mobile and other authentification),
And if I can how to implement that? should I have two logins functional?