DEV Community

Cover image for Authenticating a Vue SPA is easy with Laravel Sanctum
Andrew Schmelyun
Andrew Schmelyun

Posted on

Authenticating a Vue SPA is easy with Laravel Sanctum

Released earlier this year, Laravel Sanctum (formerly Laravel Airlock), is a lightweight package to help make authentication in single-page or native mobile applications as easy as possible. Where before you had to choose between using the web middleware with sessions or an external package like Tymon's jwt-auth, you can now use Sanctum to accomplish both stateful and token-based authentication.

In this short walk-through, I'll show you how to get started with the former. We'll create a dummy API, authenticate a user from a Vue component, and get data associated with that signed-in user.

If you'd like to skip the written tutorial, you can check out this video I've made instead. You can also go directly to the finished source code, available in this repository on GitHub.

Alright, let's dive in!

Creating a test API

The first thing we'll need to do is create an API that we can get data from. I'm thinking of a super simplistic app that will retrieve a list of secrets associated with our user.

I've installed an out-of-the-box Laravel app, and have it and a MySQL database set up in a local environment using my Laravel Docker setup. The first thing that I'm going to create is a model and migration for our secret. Using the command line, this is easy with artisan.

php artisan make:model Secret --migration

Let's open up that migration file and add in our data columns needed for a secret. I think all we'll need from this (aside from the default ID and timestamps that Laravel provides) is a user_id integer to form the relationship to a user, and the actual secret.

Schema::create('secrets', function (Blueprint $table) {
    $table->id();
    $table->integer('user_id');
    $table->text('secret');
    $table->timestamps();
});

Okay, running the migration will bring up our users and secrets tables:

php artisan migrate

A couple of quick modifications will need to be made to both of our app's models enabling the relationships, so let's open them up and get started:

// User.php

public function secrets()
{
    return $this->hasMany('App\Secret');
}
// Secret.php

public function user()
{
    return $this->belongsTo('App\Secret');
}

The final piece of our API structure is the actual routes and controllers. We're only going to be accessing a single path, which will retrieve the secrets associated with our user. So, I've added the following to my app's routes/api.php file:

Route::get('/secrets', 'SecretController@index');

The controller for this can be created easily using an Artisan command:

php artisan make:controller SecretController

Opening up that controller, let's create our index method and just return back all of our app's secrets for now. Since we have no way of getting an authenticated user, yet:

public function index()
{
    return App\Secret::all();
}

Our dummy API is done for now, let's create some fake users and secrets.

Populating the database

You can easily go right into the database and populate it manually, create a controller and form for users to input their own data, or use Artisan tinker to semi-automate user creation. I'm going to skip these methods and instead use the built-in Laravel factories to generate fake data for our users and secrets.

Out of the box, Laravel comes with a UserFactory.php class, to generate dummy users. We're going to create one for our secrets as well. In your terminal, run the following Artisan command:

php artisan make:factory SecretFactory --model=Secret

Open up the generated file, and we're going to need to populate each model with just two data points. Our user_id, and a secret:

$factory->define(Secret::class, function (Faker $faker) {
    return [
        'user_id' => 1,
        'secret' => $faker->text
    ];
});

You might be wondering why we're hard-coding in our user_id in the above snippet. Instead of generating it randomly based on the amount of users, I want to have more control over it. I'll show you shortly how I can overwrite this when we start generating our secrets.

Let's start by creating a couple of fake users. Open up your tinker shell by running the php artisan tinker command from your site root. Once it's open, we can create two users by running the global factory helper twice:

factory(App\User::class)->create(); //create saves our user in the database, unlike make

Now that we have them generated, let's create our secrets. I'm going to run the following in the tinker shell twice to create two for user_id 1:

factory(App\Secret::class)->create();

But what about our second user with a different id? Overwriting any of the values in our factory class is easy, all we do is pass in an override array to the create() method. So, we'll run the following twice to create two secrets for our second fake user:

factory(App\Secret::class)->create(['user_id' => 2]);

Our database is populated with enough fake data, let's move on to installing and prepping the Laravel Sanctum package.

Installing Laravel Sanctum

Installation is a breeze, and can be accomplished by running a few commands in our terminal. First, let's actually install the package using Composer:

composer require laravel/sanctum

Then we'll have to publish the migration files (and run the migration) with the following commands:

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

The last part of Sanctum's installation requires us modifying the app\Http\Kernel.php file to include a middleware that will inject Laravel's session cookie into our app's frontend. This is what will ultimately enable us to pass and retrieve data as an authenticated user:

'api' => [
    EnsureFrontendRequestsAreStateful::class,
    'throttle:60,1'
]

Now, we can move onto our app's frontend!

Building the frontend

Starting with Laravel 7, the frontend and authentication boilerplates were stripped out of the main package and instead can be installed with a separate bundle. For the sake of this demonstration, we're going to use it and Vue to craft our frontend.

Running the following commands from our app's root will help us get that set up:

composer require laravel/ui
php artisan ui vue --auth
npm install && npm run dev

These commands do three things:

  1. Install the Laravel UI package with Composer

  2. Generate the JS/UI files, auth boilerplate, and package.json modifications

  3. Install the frontend dependencies and compile development JS/CSS assets

I'm going to copy over everything in the welcome.blade.php file to an app.blade.php file. In it, I'll be stripping out the interior content and adding an id="app" attribute to the outside div. This will be our Vue app's mount point, as detailed in the generated app.js file.

Let's create our Vue component that will hold our login form and display some secrets.

Creating a Vue component

When we generated our frontend code earlier using php artisan ui vue, an example component was generated under resources/js/components/ExampleComponent.vue. Let's create a brand new component called SecretComponent.vue, with a basic data structure:

<template>

</template>
<script>
export default {
    data() {
        return {
            secrets: [],
            formData: {
                email: '',
                password: ''
            }
        }
    }
}
</script>

Our secrets are kept in an array, and we also have a formData object that will be used to store the email address and password for our login form.

Speaking of which, let's get started building out the markup that will create that form. Expanding between the <template> tags a bit, we'll add two divs. One for the login form, which will be hidden when there's secrets available, and another for the list of secrets, which will be hidden until there's secrets.

<template>
    <div>
        <div v-if="!secrets.length" class="row">
            <form action="#" @submit.prevent="handleLogin">
                <div class="form-row">
                    <input type="email" v-model="formData.email">
                </div>
                <div class="form-row">
                    <input type="password" v-model="formData.password">
                </div>
                <div class="form-row">
                    <button type="submit">Sign In</button>
                </div>
            </form>
        </div>
    </div>
</template>

Now, we should have a good-looking sign-in form:

Screenshot of a login form made with Bootstrap

In the code above, we're excluding an action from the form, and instead using Vue's submit handler to take care of the form submission. Let's create the method called handleLogin, which will be fired whenever a user tries to sign in:

<script>
export default {
    data() {
        return {
            secrets: [],
            formData: {
                email: '',
                password: ''
            }
        }
    },
    methods: {
        handleLogin() {
            // handle user login
        }
    }
}
</script>

Finally, we can go into our resources/js/app.js file and register our new component:

Vue.component('secret-component', require('./components/SecretComponent.vue).default);

Then going back into our app.blade.php file, replacing with . Let's put that handleLogin() method to use and authenticate a user!

Authenticating a user

If we take a look at the Laravel Sanctum documentation for SPA authentication, it details that we first need to make a call to a route at /sanctum/csrf-cookie, which will set the CSRF protection on our app and enable POST requests uninterrupted.

From there, we can proceed to send a request to the /login route, provided by the authentication framework we added in earlier. All we have to do is pass in our user's email and password, and it will authenticate our user if the credentials match.

Let's try implementing that in our handleLogin() method from earlier:

handleLogin() {
    axios.get('/sanctum/csrf-cookie').then(response => {
        axios.post('/login', this.formData).then(response => {
            console.log('User signed in!');
        }).catch(error => console.log(error)); // credentials didn't match
    });
}

Okay, if we save that and try to sign in with one of the dummy users in our database, everything should go through smoothly! The first request sets the CSRF protection in our frontend, and the second one sends over the formData object containing our user's email and password. No response data is sent back over, so we can just continue on using a Promise's resolve.

What's there to do now? Well, let's get our user's secrets!

Retrieving user data

In our Vue component, let's create a new method called getSecrets(), which will make a call to our secrets API endpoint that we created earlier. If everything goes successfully, that call should return back an array of secrets, which we can use to replace the array in our component's data object.

It will need to be called after our user has successfully logged in, so the flow will look something like this:

handleLogin() {
    axios.get('/sanctum/csrf-cookie').then(response => {
        axios.post('/login', this.formData).then(response => {
            this.getSecrets();
        }).catch(error => console.log(error)); // credentials didn't match
    });
},
getSecrets() {
    axios.get('/api/secrets').then(response => this.secrets = response.data);
}

But wait, we're returning all of the app's secrets, remember? Not just the user's. We can change that with a single line though, thanks to Laravel Sanctum. Let's open up our SecretController and navigate to the index() method, making a few changes:

public function index(Request $request)
{
    return $request->user()->secrets;
}

Included in each API call (after we've successfully logged our user in), is a laravel_session cookie in the header of each request. Sanctum takes this cookie, and using the fact that our API endpoint is stateful due to the added-in middleware from earlier, and determines if a user is logged in.

This user in its entirety, can be retrieved using the $request object. We can then chain the secrets relationship onto it, returning back the array of our user's secrets.

Finally, let's add in some boilerplate markup to render out these secrets to our user:

<template>
    <div>
        <div v-if="secrets.length" class="row">
            <div class="secret" v-for="(secret, index) in secrets" :key="index">
                <strong v-text="secret.secret"></strong> - created at <span v-text="secret.created_at"></span>
            </div>
        </div>
    </div>
</template>

💥 Now if we refresh our app and sign in using our fake user's credentials, we'll see a list of our user's secrets displayed out for us:

Screenshot of generated text making up a secrets list

So, now what's left?

Conclusions and next steps

I've only just scratched the surface of what this incredibly powerful and easy-to-use package offers. If you're using the techniques above to authenticate a user in an SPA, the entire time they're on your app after being authenticated, you can make calls to your accompanying Laravel API as if you were a logged-in user in a traditional web app.

Additionally, you can use token-based authentication for standalone SPAs, native mobile applications, or something like ElectronJS desktop apps. Everything is handled in such an eloquent way, and the documentation around the package is incredibly robust.

I hope this article's helped you get started authenticating users into your Vue SPA with Laravel. As always, if you have any questions please don't hesitate to reach out to me in the comments below or on my Twitter.

Top comments (12)

Collapse
 
samora92 profile image
samora92

I did all these steps, but when i am trying to get user data i get Unauthenticated error
routes/api.php

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});
Enter fullscreen mode Exit fullscreen mode

home.vue

<script>
import axios from "axios";
axios.defaults.withCredentials = true;
axios.defaults.baseURL = "http://localhost:8000/";

export default {
 mounted(){
     axios.get("/api/user").then((response)=>{
       console.log(response);
     })
 }
}
</script>
Enter fullscreen mode Exit fullscreen mode

I do not know if i did any thing wrong ?

Collapse
 
maxeze profile image
Maximiliano Basoalto

Happening the same to me :(

Collapse
 
samora92 profile image
samora92 • Edited

I solved it , in your Providers/RouteServiceProvider.php, change the mapApiRoutes function from middleware(['api']) to middleware(['web'])

protected function mapApiRoutes()
    {
        Route::prefix('api')
            ->middleware('web')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));
    }
Enter fullscreen mode Exit fullscreen mode

Hope this helping you.

Thread Thread
 
harmlezprinze profile image
Olami Lekan

Great work, please i will like to have a little chat with u man

Thread Thread
 
aymenalhattami profile image
Ayman Alhattami

This is not a good solution,

Thread Thread
 
aymenalhattami profile image
Ayman Alhattami

The solution is to set value for

SANCTUM_STATEFUL_DOMAINS='127.0.0.1:3000'
Enter fullscreen mode Exit fullscreen mode

in the .env file

Collapse
 
alyve profile image
Alyve

Hi. Thank you for this amazing article. ☺️
Just got a question:

    axios.get('/sanctum/csrf-cookie').then(response => {
        axios.post('/login', this.formData).then(response => {
            this.getSecrets();
        }).catch(error => console.log(error)); // credentials didn't match
    });

Why are you asking for a CSRF token right here ? I'm asking because I don't do that and I'm interested about learning something else here. 🤗
Thanks !

Collapse
 
coderincode profile image
Coder

when I want to use it with nuxt js I have many problems . I reveive data correctly from api but there is anything to do. how can I debug it. this is my setting in nuxt:


auth: {
        strategies: {
            local: {
                endpoints: {
                    login: { url: '/api/auth/login', method: 'post', propertyName: false },
                    user: { url: '/api/auth/me', method: 'get', propertyName: false }
                },
               tokenRequired: false,
               tokenType: false
            }
        },
        localStorage: false
    },

this is my login.vue

methods:{
           async submit (){
                try{
                    await this.$axios.$get('/sanctum/csrf-cookie');

                    await this.$auth.loginWith('local' ,{data: this.form});

                    console.log(111);
                }catch (e) {
                    console.log(e);
                }
            }
                }

this is login api:

public function action(LoginRequest $request){

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

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

        return (new PrivateUserResource($user))
            ->additional([
                'meta' => [
                    'token' => $user->createToken($request->email)->plainTextToken
                ]
            ]);
    }
Collapse
 
lexiebkm profile image
Alexander B.K.

Suppose there are 2 apps :

  • app 1 deals with human resource development (HRD) that already exists in the client
  • app 2 that I am developing, deals with performance management system for every employee in the client, but in different domain.

My app (app 2) will provide API that can be consumed by app 1, i.e app 1 can post employee records to my app (post/put request) as well as retrieve performance data (get request) from my app.
Is Sanctum adequate for this kind of apps ? I have read both Passport (OAuth 2) and Sanctum, but am still not sure which one I will pick.

Collapse
 
dylanglockler profile image
Dylan

Nice write up. I have Larave/Sanctum working well with Quasar with Vuex - wondering if there is a good way to know on the vue side if the user is authenticated - or perhaps at the login process I just need to track that state myself. I'm loading their user data into Vuex but on refresh it's lost, while the authentication is maintained - I'd like to query that auth state to then re-query the user details.

Collapse
 
koiki13 profile image
Koiki Damilare Solomon

Can I use this with a project that is not SPA

Collapse
 
pimhooghiemstra profile image
Pim Hooghiemstra

Great article, gets me ready to add this to my SPA too! I noticed a small error in the text where you write about the laravel ui package. I think this was first added in Laravel 6.