Note: I just want to demonstrate the workflow and basic principles of developing an SPA here. The code will be dirty, eliminating proper folder structure, form validation, styling and other stuff like that. You're supposed to have some Laravel and Vue.js knowledge if you're going to learn how to make SPAs, so I'm sure you can take care of all these things yourself in your real projects.

We'll start by building the API. Laravel needs some tweaks if you're developing an API, for example you need to handle exceptions to return JSON responses with errors instead of the standard Laravel HTML responses. This is out of the scope of this article though.

Laravel Passport

To start just create a fresh Laravel project and set up the database. Now the most tedious part is the authentication. We'll be implementing an OAuth2 authentication using Laravel Passport. Please read on OAuth2 if you're completely unfamiliar with it, I'm also not going to go into details about it here. Anyway, let's install the package:

composer require laravel/passport

The packages comes with a bunch of migrations required to store clients, access tokens, and such. Therefore we need to run those migrations:

php artisan migrate

Then run this:

php artisan passport:install

Note that this command outputs client ID and client secret for two clients. We'll be using the Password grant client which allows users to "login" using their username (or email) and password. Copy the client ID and secret somewhere, we'll need it later. You can always look it up in the database in the oauth_clients table.

Call the Passport::routes() method within the boot method of your AuthServiceProvider:

...
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
    $this->registerPolicies();

    Passport::routes(function ($router) {
        $router->forAccessTokens();
    });
}
...

We're only going to register the routes required for password grant type of authentication. Now add the Laravel\Passport\HasApiTokens trait to the App\User model class. This is not only required for the Passport to work, but also adds some useful methods to the class.

...
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
...

Finally, open config/auth.php and change the driver of the api guard to passport:

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

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],
...

Authentication

Now we need to implement our own routes and controllers for user authentication. We'll implement registration, login and logout functionality. We'll keep it simple and put everything into a single controller. Let's create it:

php artisan make:controller 'API\AuthController'

Open routes/api.php and delete the default route. Add these routes instead:

Route::post('/register', 'API\AuthController@register');
Route::post('/login', 'API\AuthController@login');

Route::middleware('auth:api')->group(function () {
    Route::post('/logout', 'API\AuthController@logout');
});

Note that the "/logout" endpoint is protected with the auth:api middleware which means only authenticated users can call it. Now let's create the register method. Again, we're keeping everything dead simple omitting request validation and everything else.

public function register()
{
    User::create([
        'name' => request('name'),
        'email' => request('email'),
        'password' => bcrypt(request('password'))
    ]);

    return response()->json(['status' => 201]);
}

I recommend you using Postman for testing APIs. It's a free tool which allows you to send requests to endpoints and get the responses. Go ahead and install it, run the development server php artisan serve and send a POST request to http://localhost:8000/api/register. Add "name", "email" and "password" keys with corresponding values on the Body tab and click Send. You should get this JSON in the response:

{"status":201}

Now let's move to the trickier part, i.e. the login method. Laravel Passports provides us with an endpoint to authenticate users. You need to POST grant_type, client_id, client_secret, username and password to it and in return you will get two tokens: an access token, which you can use to make API calls, and a refresh token used to get a fresh access token when the old one expires. By default access tokens are valid for one year and we won't bother with refreshing them here.

Why don't we use the default endpoint directly? Because we'd have to store the client_id and client_secret on the client side inside our JavaScript/Vue code, which is unsafe. Instead we'll append those in our login method and forward the request to the Passport's endpoint.

public function login()
{
    $client = DB::table('oauth_clients')
        ->where('password_client', true)
        ->first();

    $data = [
        'grant_type' => 'password',
        'client_id' => $client->id,
        'client_secret' => $client->secret,
        'username' => request('username'),
        'password' => request('password'),
    ];

    $request = Request::create('/oauth/token', 'POST', $data);
    return app()->handle($request);
}

We're faking a request here to make an internal API call. Make a POST request to http://localhost:8000/api/login with a username (which is in fact the email) and a password. In response you should get something like this:

{
    "token_type": "Bearer",
    "expires_in": 31536000,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6Ijc0MDc1YzhlMzkzZDE5YTIwZDk5YTc5MjlkZGI5MjlkYjQzN2RjMDNhNjY4ZmM4ZTUyZmI3ZjRiMDY4ZTBjYmJkYzY4YWI3MDkyYWMyOTk4In0.eyJhdWQiOiIyIiwianRpIjoiNzQwNzVjOGUzOTNkMTlhMjBkOTlhNzkyOWRkYjkyOWRiNDM3ZGMwM2E2NjhmYzhlNTJmYjdmNGIwNjhlMGNiYmRjNjhhYjcwOTJhYzI5OTgiLCJpYXQiOjE1MTgwOTc0MzQsIm5iZiI6MTUxODA5NzQzNCwiZXhwIjoxNTQ5NjMzNDM0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.cxa6WNUWKy81uXJSMtx7c4zFEneE9rhYKklH_gXG2qvLSeyoly6zg923tzTkbAUASZiXzvU9l5MOs0qzrK3jPLFJmIz6P0uCEcSCQL9oM_6lOvoLOL6aaJyRIur2MrO_jJ1O5KX91HGOuUsbHiwqsxjjaV64AN4HUYObhgfOBWOT-693Xj1QL-soTSRLdCefPn-R3aT3XkgBd06RHJ2QW3qVWHFgfPiEsPG5EwRZdNWR8JEmCd_KwSDyEcdBrlmEe77JBwexI_YE2bcWbJYQEMUU7WmKQn0ELaLzm5iReRiMsRTITWx7s0uSn8cjwofO0f0s7TFou7hOIw4uuRBOYo2VyQHMhbpzQxE9_e6hef5PXN3J0sKZR_d8TGvwEekwUGpj9xxbZjF5gsPfSGl1f6Bmumamhosx-nplhE-HAPr4FCgCJTQhLnpjzev-P2Xhe_qDOo61WAuIcs1yPYb8TjDFy9wiOLeifRIr9-FEyn2AWxxPFZMcLOJoDBlTiLWOkWm21mKHRPwDdWi7-mlMUnJvmjeGJp2s64hrQES6AOgtjG4NSye9ab_jU4RwPA2t0coKDkyUSU7RQFEKOv5RcB620vEgSSIuz4W_LFZ4tbCDcCNqda2FqcJxEeB6JOVifGabq3-_ZKITlcGZ5AX-6ECvpGczd9WZnGz2pVScJyQ",
    "refresh_token": "def502009b8b24b62eacb618172846eee036b135eda5ba8516af270c40d8bf4dc25043277c4ed91636981c841a5558d821b34e4c6620d20b78b62859e66f7dfe478d7c603cb747527e6cfde6a3ae0eec8fb2459714ba695f787cdf485cca1212779b17f0aaa729e6e0897633dc45eeebdd065a3ee477be28c47334c5f0fa895ad7f85f1d2ebabcec1e9536afc0713073d1c650a57c06c1d3a6a54ff8702f9afd5d8ae6da12562184ffa7e6a27fb4f6b054c25f396b97e0588059f2d18ef1cf119ec1994b64fe4be42860fb8ae52b490752079e2edee22c73838c6dfa4645cb1f4ae736f363e5111c447c093939713595d24695b963b4da2d5c75c6259e30aaade7034fc718d8cf2d9eeb0fe11524b527b2b0ac7175c6e9a6a41150327fbdc6c472c01149a5bff2e66cf9bff0f5e80a4fac1927c70b191bea4b874000fb94ea3b2bea878d93a0ed40d40cfd078532862baeac0e84781a4e973b3c2a7afeadc504eb"
}

Let's try making requests with the token. But first let's create the logout method in our controller - since it's a protected method, we can test the token with it.

public function logout()
{
    return 'Protected route';
}

Now go to the Headers tab in Postman and add a new key Authorization with value Bearer %token% where %token% is the access_token from the JSON response you received earlier. Hit the http://localhost:8000/api/logout endpoint and you should see "Protected route" in the response. Turn off or remove the Authorization header and the request will fail.

Finally, let's implement the real logout method. Laravel Passport provides a method to easily revoke a user's access token. Although, we have to update the database manually in order to revoke the refresh token.

public function logout()
{
    $accessToken = auth()->user()->token();

    $refreshToken = DB::table('oauth_refresh_tokens')
        ->where('access_token_id', $accessToken->id)
        ->update([
            'revoked' => true
        ]);

    $accessToken->revoke();

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

Make a call to the logout endpoint again and you should see this in the response:

{"status":200}

Make another call and Laravel will probably attempt to redirect you to a login page - this means we are not longer authenticated and authorized to perform the request, which you'd expect. Congratulations! We'll start implementing the web client next.