Vue SPA + Laravel API autenticada con Passport – Parte 1 Laravel API

1- Instalar Laravel y configurar base de datos

Puedes seguir los pasos aquí: https://laravel.com/docs/6.x/installation.
En mi caso utilizaré Laravel 6.

laravel new spa-auth
cd spa-auth

2- Instalar Passport

Puedes seguir los pasos aquí: https://laravel.com/docs/6.x/passport

composer require laravel/passport
php artisan migrate
php artisan passport:install

Una vez realicemos la migración e instalamos, passport creará inicialmente 2 clientes. Para autenticarnos a nuestra API utilizaremos el cliente password grant. El cliente con id 2 es del tipo esperado. Lo puedes ver en el siguiente fragmento.

Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: tGMXW17y3FDx3NhmPDyH4pVQyhMNxAcBlY3E5nuV
Password grant client created successfully.
Client ID: 2
Client secret: Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ

Agregaremos el trait Laravel\Passport\HasApiTokens a nuestro modelo de usuario App\User

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

El proximo paso será llamar al método Passport::routes dentro del método boot del AuthServiceProvider. Recuerda importar Laravel\Passport\Passport;

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

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

        Passport::routes();
    }
}

Haremos una pequeña modificación en lo que acabamos de insertar para que passport no genere todas las rutas, ya que vamos a utilizar solo el cliente password grant. En vez de agregar Passport::routes(); agregaremos:

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

Para finalizar en el archivo config/auth.php modificaremos el driver que utilizará la API para autenticar a passport.

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

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

3- Prueba de la API

Utilizaremos un cliente como Postman o Insomnia.
Postman: https://www.getpostman.com
Insomnia: https://insomnia.rest

En mi caso estoy utilizando Laravel Valet, por lo que mi URL sería http://spa-auth.test.
Si quieres saber como configurar Laravel Valet puedes hacerlo aquí: https://laravel.com/docs/6.x/valet

3.1- Crear usuario de prueba

Antes que nada debemos agregar algún usuario para realizar las pruebas simulando usuarios reales en el futuro. Lo haremos utilizando tinker.

php artisan tinker
factory('App\User')->create()

El resultado será algo randómico. El password predeterminado es password.

=> App\User {#3052
      name: "Nickclaus Koch",
      email: "[email protected]",
      email_verified_at: "2019-10-07 19:18:24",
      updated_at: "2019-10-07 19:18:24",
      created_at: "2019-10-07 19:18:24",
      id: 1,
    }

3.2- Obtener el access_token

Abriremos Postman y crearemos un nuevo POST request a
http://spa-auth.test/oauth/token

En la pestaña body, agregaremos campos como form-data:

grant_type: password
client_id: 2
client_secret: Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ
username: [email protected]
password: password
  • grant_type, será password, ya que es el cliente seleccionado anteriormente.
  • client_id, será 2 (si miramos más arriba el cliente con el id 2 es del tipo Password grant)
  • client_secret, ingresaremos el hash del cliente 2 de passport (esto lo puedes encontrar en la tabla oauth_clients)
  • username, será el usuario con el que queremos autenticarnos a la API
  • password, será la contraseña del usuario que estamos autenticando, en este caso el predeterminado al crear un cliente desde un factory: password.

La respuesta si lo hemos configurado todo tal cual, será:

En lo que estamos interesados es, él access_token ya que para seguir realizando requests a la API de manera autenticada, debemos pasar ese valor en cada request.

3.3- Realizar un request autenticado con el access_token

Haremos la prueba de realizar un request de manera autenticada con el access_token obtenido. En el archivo routes/api.php encontraremos que existe de manera predeterminada esta ruta:

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Como indica el middleware aplicado, http://spa-auth.test/api/user es una ruta protegida por el guard api (que lo hemos configurado para utilizar Passport).

Crearemos un nuevo request en Postman a esa ruta enviando las siguientes cabeceras:

Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjAyMjgwZDEzY2E3ZmNhOGJkZDcxZWUxNDZmMjM5MTU1YzQ4MjRmMTA2ZjM4MmRhZWYwZjI5MGM1YTBiZTI4NDExYTY5ZjhhMDVkZDllNWI0In0.eyJhdWQiOiIyIiwianRpIjoiMDIyODBkMTNjYTdmY2E4YmRkNzFlZTE0NmYyMzkxNTVjNDgyNGYxMDZmMzgyZGFlZjBmMjkwYzVhMGJlMjg0MTFhNjlmOGEwNWRkOWU1YjQiLCJpYXQiOjE1NzA0NzY1MTMsIm5iZiI6MTU3MDQ3NjUxMywiZXhwIjoxNjAyMDk4OTEyLCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.ohXg5nKHmZMEHADYttHeshNaTad4wKSUD1ByT4NPhfRSTR0b5ZUPj13aNv2e6qNv06dUQimOiYn3C0yUzudcXVbww1lN2jz_UeYX7N-s1lsrQL04JgzFM48i9VzyF6Ujjz3YWrVTIqh12FFschboiqZsEY7ZvAXxXvLKyOin8mI4XIh47sbA_ABTZSMpjuC9ZiwxRlq3byXhzw7Khj5rwHS1SHsg7dPP32w3M4gmA8WqnjsThIwAI84pyFpX1EOtedOSydRLrvTAy-OpivNlCmvOaWq-DcseVZgmwCldvaWIMe_8AQDmIHX0E94ZUuqrLeDZfjdbAayXwRzL8mzVs4hNBSfLPNHk3ot885NjrklBtEhaKliwpdkcoGEjobdKvjgd8AVWJgzgRe-z15dEbNAZQiPtcfc5CGG8BTbwSvF__hIgUrZNN_JUiWdtlx5RnpcDC4WfyecEhW4GplB0nbnmFV9QymFCeSukgNFdezeqCL1Lm9reLrVU8ovM6qyvOasn4bX0C4Z2wA2zbKP_y-7RDZN7X9lQwU1XWdvyyUAVArfm3uKlFa2bre3kcDpSVYdtXzjadybA1cROEzYwTQ7u-cJG90N1VNYsw5ni4PK7jqPYdeSYf2atvT_-PkcA4uA-g5PU7Sdz5ypONcKuOeR8v9n6SiE5q-IikwQFw54
  • Accept, será el tipo de respuesta que esperamos recibir en el cliente, en este caso application/json.
  • Authorization, aquí enviaremos nuestro access_token agregándole antes la palabra Bearer seguida de un espacio.

La respuesta será la información del usuario:

{
    "id": 1,
    "name": "Nicklaus Koch",
    "email": "[email protected]",
    "email_verified_at": "2019-10-07 19:18:24",
    "created_at": "2019-10-07 19:18:24",
    "updated_at": "2019-10-07 19:18:24"
}

Si por alguna razón no enviamos la cabecera Authorization o modificamos el access_token, no podremos acceder a los datos de nuestra API de manera autenticada, y el error será:

{
    "message": "Unauthenticated."
}

4- Capa de seguridad

Si volvemos a nuestro request inicial para obtener el access_token vemos que estamos pasando datos que no deberían ser públicos, como grant_type, client_id y client_secret.

Vamos a crear una ruta y lógica específica en Laravel que se encargue de resolver esta cuestión y de proveernos estos datos sin que el usuario en el cliente deba enviarlos. El cliente -en este caso nuestra SPA hecha en Vue- solo enviará su usuario y password.

4.1- Crear el controlador e instalar Guzzle:

php artisan make:controller AuthController

Instalaremos un cliente http para PHP, Guzzle. Puedes obtener más información aquí: http://docs.guzzlephp.org/en/stable/.

composer require guzzlehttp/guzzle

Volveremos a AuthController.php y crearemos un método login y otro método logout, al que le pasaremos el request del cliente.

El método login realiza un request http a nuestra API enviando los datos de username y password que vendrán del cliente $request y agregando los datos que queríamos ocultar, grant_type, client_id y client_secret.

Estos datos sensibles los ocultaremos en variables en el archivo .env.
Antes agregaremos al archivo config/services.php un nuevo array. Este array cargará los datos de nuestras variables.

'passport' => [
        'login_endpoint' => env('PASSPORT_LOGIN_ENDPOINT'),
        'client_id' => env('PASSPORT_CLIENT_ID'),
        'client_secret' => env('PASSPORT_CLIENT_SECRET'),
    ],

Ahora agregaremos las variables al final de nuestro archivo .env.
Recuerda cambiar esto por tus credenciales.

PASSPORT_LOGIN_ENDPOINT=http://spa-auth.test/oauth/token
PASSPORT_CLIENT_ID=2
PASSPORT_CLIENT_SECRET=Kt2zlijS7PrqNWGINhbAmEb5YmUpzGRfkgqhJMEZ

Volviendo al método login de AuthController.php, hemos envuelto el request http en un bloque try-catch para poder obtener información sobre si la consulta se realizó con éxito o si ha fallado.
Si todo sale bien, la respuesta que obtendremos será el access_token.

En el caso de que algo salga mal, enmascararemos los errores con mensajes personalizados, esto lo haremos para no mostrar información relevante sobre un fallo a usuarios/clientes sin autorización para acceder a nuestra API.

Evaluaremos tres posibilidades, el error 400 ,el error 401 o cualquier otro error. El error 400 Bad Request será de carácter general por ejemplo por falta de un parámetro en el request, mientras que el error 401 Unauthorized será de credenciales erróneas. Si ocurre algún otro error que no sea 400 o 401, también lanzaremos un mensaje de error más genérico.

public function login(Request $request)
    {
        $http = new \GuzzleHttp\Client;
        try {
            
            $response = $http->post(config('services.passport.login_endpoint'), [
                'form_params' => [
                    'grant_type' => 'password',
                    'client_id' => config('services.passport.client_id'),
                    'client_secret' => config('services.passport.client_secret'),
                    'username' => $request->username,
                    'password' => $request->password,
                ]
            ]);

            return $response->getBody();

        } catch (\GuzzleHttp\Exception\BadResponseException $e) {
            
            if ($e->getCode() === 400) {
                return response()->json('Invalid Request. Please enter a username or a password.', $e->getCode());
            } else if ($e->getCode() === 401) {
                return response()->json('Your credentials are incorrect. Please try again', $e->getCode());
            }

            return response()->json('Something went wrong on the server.', $e->getCode());
        }
    }
    
    public function logout()
    {
        auth()->user()->tokens->each(function ($token, $key) {
            $token->delete();
        });
        
        return response()->json('Logged out successfully', 200);
    }

El método logout será encargado de borrar todos los access_token relacionados con nuestro usuario y devolver una respuesta de confirmación.

4.2- Crear las rutas en api/routes.php

Route::post('/login', '[email protected]');

Route::middleware('auth:api')->post('/logout', '[email protected]');

4.3- Prueba de nuestra ruta

Hasta ahora nuestra API tiene 3 endpoints:

http://spa-auth.test/api/login (pública)
http://spa-auth.test/api/logout (protegida con auth:api)
http://spa-auth.test/api/user (protegida con auth:api)

Si quieres ver que rutas tienes configuradas en Laravel usa este comando:

php artisan route:list

Utilizaremos nuevamente Postman para realizar la prueba, esta vez realizaremos un POST request a http://spa-auth.test/api/login solo enviando los campos username y password del usuario que queremos autenticar.

username: [email protected]
password: password

La respuesta esperada será el access_token para incluir en cualquier endpoint que queramos visitar de manera autenticada.

Quien se encarga de los datos sensibles será el backend, nunca pasaran por el cliente.

Por ultimo nos queda probar nuestra ruta de logout, y lo debemos hacer de manera autenticada. Por lo tanto realizaremos un POST request a http://spa-auth.test/api/logout enviando el access_token obtenido.

Antes de realizar el request para desconectarnos sería bueno revisar la tabla de la base de datos oauth_access_tokens, en la misma veremos varias entradas para nuestro usuario, son los tokens que ha creado cada vez que se conecta.

Si nuestro request para desconectarnos es correcto, se borraran de la base de datos todos los token solicitados para ese usuario.

5- Conclusión: Parte 1 Laravel API

Hemos creado una pequeña API con autenticación con Laravel y Passport.
Esto lo hemos hecho para poder conectarnos a esta API desde una SPA (Single Page Application) realizada con Vue y acceder a los datos de manera segura.

En la parte 2 veremos como realizar una SPA básica y realizar consultas autenticadas a nuestra API.

Este trabajo más la parte 2, la hemos incluido en un repo para que puedas ver y experimentar: https://github.com/danielcharrua/laravelvue-boilerplate

Si te he ayudado, comparte! 🤓


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Nombre *