go to part...
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.
All the materials at voerro are absolutely free and are worked on in the author's spare time. If you found any of the tutorials helpful, please consider supporting the project. Thank you!