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

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

¿NECESITAS POTENCIAR TU PÁGINA WEB O TIENDA ONLINE?

Si buscas realizar mejoras en tu página web o tienda online, estás en el lugar adecuado. Podemos trabajar juntos para impulsar tu presencia en línea y hacer realidad tus objetivos digitales.

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

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,
    }

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.

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."
}

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.

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.

Crear las rutas en routes/api.php

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

Route::middleware('auth:api')->post('/logout', 'AuthController@logout');

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.

Conclusión

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! 🤓

HAGAMOS DESPEGAR TU NEGOCIO ONLINE 🚀

Te ayudo a potenciar tu página web o tienda online con estos servicios.

Publicaciones Similares

28 comentarios

    1. Hola Juan, para proteger ante un ataque de fuerza bruta deberías implementar alguna manera de limitar los intentos de autenticación fallidos y bloquear el acceso por un determinado tiempo. Puedes inspirarte en el paquete que usa Laravel Illuminate\Foundation\Auth\ThrottlesLogins.
      En el caso de CSRF, eso ya lo trae implementado Laravel, lo puedes ver en el archivo laravel/resources/js/bootstrap.js.
      Si tienes algo que aportar puedes agregarlo aquí, así ayudas a otros lectores.

  1. Hola Daniel!
    Gran guía, me ha ayudado mucho.
    Solo me ha confundido un poco el título del punto 4.2. Imagino que quieres decir «Crear las rutas en routes/api.php», no? Parece una errata.

    1. Hola Daniel, antes que nada comentarte que me alegra que te sea de ayuda el artículo. Gracias por tomarte el tiempo de comentar para solucionar este tipo de detalles. Un saludo!

  2. Hola Buenas
    Seguí todos los pasos, todo bien hasta que llegue al paso 4,1, donde intente hacer el login a mi url que sería localhost:8000/api/login con postman y al momento de presionar send, enviando los datos de username y password, se queda todo el rato cargando, no muestra absolutamente nada, ni siquiera un error, queda cargando eternamente, alguna idea de que podría estar ocurriendo?

    1. Hola Hugo, ¿te salieron bien las pruebas del paso 3?
      Si es así, quizá te faltó hacer el paso 4.2 donde se crean las rutas de la aplicación de Laravel.

  3. Hola Hugo, me ha pasado exáctamente lo mismo que a ti. Estaba usando el servidor de angular (php artisan serve) y por si diera problemas con Guzzle, lo monté en xampp con un vhost. Así me funcionaba.

    1. Hola Daniel, te refieres a que estas usando el servidor de PHP? (has escrito Angular y luego mencionas «php artisan serve» y este último lo que ejecuta es el servidor de PHP)

  4. Hola Daniel! Me parece genial tu articulo. Después de un montón de tiempo buscando tutoriales sobre como hacer un login en condiciones con Laravel Passport, ¡eres mi salvador!

    Solo un par de preguntas: Consideras que este login está listo para ser subido a producción, o necesita algún detalle más? (Como lo que comentaban de la protección de ataque por fuerza bruta). Después, ¿hay alguna manera de diferenciar donde han sido generados los tokens? De esa manera el usuario podría controlar donde ha iniciado sesión y cerrarla selectivamente.

    Muchas gracias crack!

  5. Perdón, quiero decir, el servidor de Laravel ejecutando php artisan serve. Como decía en el siguiente comentario, tanto tutorial ya me ha vuelto loco jaja.

  6. Hola Daniel, muchas gracias por el post pero me surge una duda sobre otras maneras de hacerlo. He visto otros sitios donde directamente hacen una consulta con el usuario y contraseña(sin enviar client_id y client_secret) y ya les devuelve el Bearer token, sin tener que utilizar una llamada con Guzzle al mismo servidor para pasarle el client_id y el client_secret.
    Utilizan un Auth::attempt($credentials) y creo que por defecto Laravel ya coge el client_id y el client_secret de la password key que se genera al utilizar el comando php artisan passport:install

    No es más facil hacerlo así y no tener que utilizar Guzzle?

    Muchas gracias y un saludo, te dejo un link al post que me refiero a ver que tal lo ves https://medium.com/@cvallejo/sistema-de-autenticaci%C3%B3n-api-rest-con-laravel-5-6-240be1f3fc7d

  7. Daniel Excelente post muchas gracias tengo el mismo problema que Daniel y que Hugo que al mandar el request al /oauth se queda cargando y nunca responde

  8. Lo hice desde el localhost, creo que es porque la acción en el POST del login coincide con la otra acción que se hace para generar el access_token y como es en el mismo puerto se queda cargando. No se supongo que es eso , la solución fue la que comento mas arriba Florencia . De cualquier forma vuelvo a felicitarte por este gran trabajo.

  9. Hola, vi que muchos tuvieron un problema con las respuestas de guzzle, el detalle es que «php artisan serve» es un servidor que al parecer solo puede ejecutar un hilo, esto quiere decir que o ejecuta la petición que se le esta enviando que en este caso es «http://localhost:8000/api/login» o ejecuta la que le indicas por medio de guzzle, entonces para que esto no suceda pueden configurar un vhost en su archivo de host de su sistema correspondiente, y configurar un vhost en el servidor de apache indicando donde se encuentra la carpeta public de su proyecto.

    Aquí hay uno para linux y mac (puede variar un poco):
    https://styde.net/como-crear-virtual-hosts-con-apache-para-linux-y-mac/

    Aquí hay un articulo para hacerlo en windows
    https://medium.com/@jhordydelaguila/creando-y-configurando-virtual-host-con-apache-para-xampp-windows-f90c2b0527ac

    Happy coding & Enjoy! ~ 𝕳𝖆𝖓𝕲𝖔𝖚𝖍

  10. Gracias por este POST ha servido de nuevas experiencias, en mi caso me he quedado,

    4.3- Prueba de nuestra ruta

    respuestas de guzzle configure, un vhost en el servidor pero no encontre respuesta cuando accedo a la Ruta, me trae el siguiente Error.

    «Something went wrong on the server.»

  11. Hola Buenas,

    muchas gracias por tu post,
    realmente era lo que necesitava i nos lo has puesto en bandeja :D.

    Queria preguntar-te sobre la pregunta numero 8 de Sergio. Has podido investigar alguna manera «correcta» de conseguir el token sin la necessidad de instalar una libreria de php http para connectar con tu aplicacion.

    Muchas Gracias !

  12. Hola Daniel Excelente post muchas gracias, solo tengo una duda, que tan seguro es una ruta publica como tú ejemplo lo indica http://spa-auth.test/api/login (pública), si realizan una petición a la ruta ¿Pueden obtener el client_secret, client_id? por el config(‘services.passport.client_id’) y config(‘services.passport.client_secret’), que se guarda en el archivo config/services.php por medio de las variables PASSPORT_CLIENT_SECRET y PASSPORT_CLIENT_ID que están guardados en el .env si estamos utilizando un administrador de servidor como ejemplo Forge Laravel

    Gracias por el POST

  13. Buenas Daniel! Gracias por el post, me ayudó mucho.
    Tengo un problema, quise probar con postman los codigos de error y resulta que si pongo el mail o la password, me lo toma como una bad request y me devuelve el codigo 400 y no el 401, no tengo muy claro cómo es que se hace (o en qué momento) la comprobacion de las credenciales de la request contra las de la db. Tenes alguna idea de por qué podria ser?

    Saludos!

Deja una respuesta

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

Responsable: charrua.es. Finalidad: Poder contestar tu solicitud. Almacenamiento de los datos: Los datos son almacenados en un servidor alojado dentro de la UE y gestionado por charrua.es. Derechos: En cualquier momento puedes limitar, recuperar y borrar tu información.