Autenticación completa de usuarios y control de acceso: un tutorial de Laravel Passport, pt. 1

Publicado: 2022-03-11

Al desarrollar una aplicación web, generalmente es una buena idea dividirla en dos niveles. Una API de nivel medio interactúa con la base de datos, y un nivel web generalmente consta de un SPA o MPA de front-end. De esta forma, una aplicación web se acopla de forma más flexible, lo que facilita su administración y depuración a largo plazo.

Cuando se ha creado la API, configurar la autenticación y el estado en un contexto de API sin estado puede parecer algo problemático.

En este artículo, veremos cómo implementar la autenticación de usuario completa y una forma simple de control de acceso en una API usando Laravel y Passport. Debe tener experiencia trabajando con Laravel ya que este no es un tutorial introductorio.

Requisitos previos de instalación:

  • PHP 7+, MySQL y Apache (los desarrolladores que deseen instalar los tres a la vez pueden usar XAMPP).
  • Compositor
  • Laravel 7
  • Pasaporte Laravel. Dado que las API generalmente no tienen estado y no usan sesiones, generalmente usamos tokens para mantener el estado entre solicitudes. Laravel usa la biblioteca Passport para implementar un servidor OAuth2 completo que podemos usar para la autenticación en nuestra API.
  • Postman, cURL o Insomnia para probar la API; esto depende de las preferencias personales
  • Editor de texto de su elección
  • Ayudantes de Laravel (para Laravel 6.0 y versiones posteriores): después de instalar Laravel y Passport, simplemente ejecute:
 composer require laravel/helpers

Con lo anterior instalado, estamos listos para comenzar. Asegúrese de configurar la conexión de su base de datos editando el archivo .env .

Tutorial de Laravel Passport, Paso 1: Agregue un controlador y un modelo para solicitudes ficticias

Primero, vamos a crear un controlador y un modelo para solicitudes ficticias. El modelo no será de mucha utilidad en este tutorial, es solo para dar una idea de los datos que el controlador debe manipular.

Antes de crear el modelo y el controlador, debemos crear una migración. En una ventana de terminal, o cmd.exe , si está usando Windows, ejecute:

 php artisan make:migration create_articles_table --create=articles

Ahora, vaya a la carpeta base de database/migrations y abra el archivo con un nombre similar a xxxx_xx_xx_xxxxxx_create_articles_table.php .

En la función up de la clase, escribiremos esto:

 Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->string('body'); $table->integer('user_id'); $table->timestamps(); });

A continuación, crearemos un modelo de Article . Para hacer eso, ejecuta:

 php artisan make:model Article

Luego creamos el controlador ArticleController ejecutando:

 php artisan make:controller ArticleController --resource

A continuación, editaremos el archivo app/Providers/AppServiceProvider.php e importaremos la clase Illuminate\Support\Facades\Schema agregando:

 use Illuminate\Support\Facades\Schema

…al final de las importaciones en la parte superior del archivo.

Luego, en la función de boot , escribiremos:

 Schema::defaultStringLength(191);

Una vez hecho todo esto, podemos ejecutar:

 php artisan migrate

…para aplicar la migración que creamos arriba.

Tutorial de Laravel Passport, Paso 2: Cree las Piezas Necesarias de Middleware

Aquí agregaremos las piezas de middleware que serán necesarias para que la API funcione.

Respuestas JSON

La primera pieza necesaria es el middleware ForceJsonResponse , que convertirá todas las respuestas a JSON automáticamente.

Para hacer esto, ejecute:

 php artisan make:middleware ForceJsonResponse

Y esta es la función de manejo de ese middleware, en App/Http/Middleware/ForceJsonReponse.php :

 public function handle($request, Closure $next) { $request->headers->set('Accept', 'application/json'); return $next($request); }

A continuación, agregaremos el middleware a nuestro archivo app/Http/Kernel.php en la matriz $routeMiddleware :

 'json.response' => \App\Http\Middleware\ForceJsonResponse::class,

Luego, también lo agregaremos a la matriz $middleware en el mismo archivo:

 \App\Http\Middleware\ForceJsonResponse::class,

Eso aseguraría que el middleware ForceJsonResponse se ejecute en cada solicitud.

CORS (intercambio de recursos de origen cruzado)

Para permitir que los consumidores de nuestra API REST de Laravel accedan a ella desde un origen diferente, debemos configurar CORS. Para hacer eso, crearemos una pieza de middleware llamada Cors .

En una terminal o símbolo del sistema, cd en el directorio raíz del proyecto y ejecute:

 php artisan make:middleware Cors

Luego, en app/Http/Middleware/Cors.php , agrega el siguiente código:

 public function handle($request, Closure $next) { return $next($request) ->header('Access-Control-Allow-Origin', '*') ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') ->header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, X-Token-Auth, Authorization'); }

Para cargar esta pieza de middleware, necesitaremos agregar una línea a la matriz $routeMiddleware de app/Http/Kernel.php :

 'cors' => \App\Http\Middleware\Cors::class,

Además, tendremos que agregarlo a la matriz $middleware como hicimos con el middleware anterior:

 \App\Http\Middleware\Cors::class,

Después de hacer eso, agregaremos este grupo de routes/api.php :

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... });

Todas nuestras rutas API entrarán en esa función, como veremos a continuación.

Tutorial de Laravel Passport, Paso 3: Crear controladores de autenticación de usuario para la API

Ahora queremos crear el controlador de autenticación con funciones de inicio de login y register .

Primero, ejecutaremos:

 php artisan make:controller Auth/ApiAuthController

Ahora importaremos algunas clases al archivo app/Http/Controllers/Auth/ApiAuthController.php . Estas clases se utilizarán en la creación de las funciones de inicio de login y register . Vamos a importar las clases añadiendo:

 use App\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str;

…hasta la parte superior del controlador.

Ahora, para agregar la autenticación API de Laravel para nuestros usuarios, vamos a crear funciones de inicio de login , logout de sesión y register (registro) en el mismo archivo.

La función de register se verá así:

 public function register (Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6|confirmed', ]); if ($validator->fails()) { return response(['errors'=>$validator->errors()->all()], 422); } $request['password']=Hash::make($request['password']); $request['remember_token'] = Str::random(10); $user = User::create($request->toArray()); $token = $user->createToken('Laravel Password Grant Client')->accessToken; $response = ['token' => $token]; return response($response, 200); }

La función de inicio de login es así:

 public function login (Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|string|email|max:255', 'password' => 'required|string|min:6|confirmed', ]); if ($validator->fails()) { return response(['errors'=>$validator->errors()->all()], 422); } $user = User::where('email', $request->email)->first(); if ($user) { if (Hash::check($request->password, $user->password)) { $token = $user->createToken('Laravel Password Grant Client')->accessToken; $response = ['token' => $token]; return response($response, 200); } else { $response = ["message" => "Password mismatch"]; return response($response, 422); } } else { $response = ["message" =>'User does not exist']; return response($response, 422); } }

Y finalmente, la función de logout de sesión:

 public function logout (Request $request) { $token = $request->user()->token(); $token->revoke(); $response = ['message' => 'You have been successfully logged out!']; return response($response, 200); }

Después de esto, necesitamos agregar las funciones de inicio de login , register y cierre de logout a nuestras rutas, es decir, dentro del grupo de rutas que ya está en la API:

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... // public routes Route::post('/login', 'Auth\ApiAuthController@login')->name('login.api'); Route::post('/register','Auth\ApiAuthController@register')->name('register.api'); Route::post('/logout', 'Auth\ApiAuthController@logout')->name('logout.api'); // ... });

Por último, debemos agregar el rasgo HasApiToken al modelo de User . Vaya a app/User y asegúrese de tener:

 use HasApiTokens, Notifiable;

…en la parte superior de la clase.

Lo que tenemos hasta ahora…

Si iniciamos el servidor de aplicaciones, es decir, ejecutamos el php artisan serve , y luego intentamos enviar una solicitud GET a la ruta /api/user , deberíamos recibir el mensaje:

 { "message": "Unauthenticated." }

Esto se debe a que no estamos autenticados para acceder a esa ruta. Para proteger algunas rutas de su elección, podemos agregarlas a routes/api.php justo después de las líneas Route::post :

 Route::middleware('auth:api')->group(function () { // our routes to be protected will go in here });

Antes de continuar, agregaremos la ruta de cierre de sesión al middleware auth:api porque Laravel usa un token para cerrar la sesión del usuario, un token al que no se puede acceder desde fuera del middleware auth:api . Nuestras rutas públicas se ven así:

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... // public routes Route::post('/login', 'Auth\ApiAuthController@login')->name('login.api'); Route::post('/register', 'Auth\ApiAuthController@register')->name('register.api'); // ... });

Nuestras rutas protegidas , por otro lado, se ven así:

 Route::middleware('auth:api')->group(function () { // our routes to be protected will go in here Route::post('/logout', 'Auth\ApiAuthController@logout')->name('logout.api'); });

Ahora navegaremos al ArticleController que creamos en app/Http/Controllers/ArticleController.php y eliminaremos los métodos de create y edit en esa clase. Después de eso, agregaremos el siguiente fragmento de código, ligeramente editado, a cada función restante:

 $response = ['message' => '<function name> function']; return response($response, 200);

Completaremos <function name> según corresponda. Por ejemplo, la función de update tendrá esto como su cuerpo:

 $response = ['message' => 'update function']; return response($response, 200);

Una prueba de autenticación manual de Laravel: creación de un usuario

Para registrar un usuario, enviaremos una solicitud POST a /api/register con los siguientes parámetros: name , email (que debe ser único), password y password_confirmation .

Una captura de pantalla del envío de una solicitud POST a /api/register mediante Postman.

Cuando se crea el usuario, la API devolverá un token, que usaremos en solicitudes posteriores como nuestro medio de autenticación.

Para iniciar sesión, enviaremos una solicitud POST a /api/login . Si nuestras credenciales son correctas, también obtendremos un token de nuestra API de inicio de sesión de Laravel de esta manera.

Una captura de pantalla del envío de una solicitud POST a /api/login usando Postman.

El token de autorización que recibimos de esta solicitud lo podemos usar cuando queremos acceder a una ruta protegida. En Postman, la pestaña "Autorización" tiene un menú desplegable donde el tipo se puede configurar en "Ficha de portador", después de lo cual la ficha puede ir al campo de ficha.

El proceso es bastante similar en Insomnia.

Los usuarios de cURL pueden hacer el equivalente pasando el parámetro -H "Authorization: Bearer <token>" , donde <token> es el token de autorización proporcionado por la respuesta de inicio de sesión o registro.

Al igual que con cURL, si los desarrolladores planean consumir la API usando axios o una biblioteca de ese tipo, pueden agregar un encabezado de Authorization con el valor Bearer <token> .

Tutorial de Laravel Passport, Paso 4: Crear funcionalidad de restablecimiento de contraseña

Ahora que se realizó la autenticación básica, es hora de configurar una función de restablecimiento de contraseña.

Para hacer esto, podemos optar por crear un directorio de controlador api_auth , crear nuevos controladores personalizados e implementar la función; o podemos editar los controladores de autenticación que podemos generar con Laravel. En este caso, editaremos los controladores de autenticación, ya que toda la aplicación es una API.

Primero, generaremos los controladores de autenticación ejecutando:

 composer require laravel/ui php artisan ui vue --auth

Editaremos la clase en app/Http/Controllers/Auth/ForgotPasswordController.php , agregando estos dos métodos:

 protected function sendResetLinkResponse(Request $request, $response) { $response = ['message' => "Password reset email sent"]; return response($response, 200); } protected function sendResetLinkFailedResponse(Request $request, $response) { $response = "Email could not be sent to this email address"; return response($response, 500); }

A continuación, debemos configurar el controlador que realmente restablece la contraseña, por lo que navegaremos a app/Http/Controllers/Auth/ResetPasswordController.php y anularemos las funciones predeterminadas como esta:

 protected function resetPassword($user, $password) { $user->password = Hash::make($password); $user->save(); event(new PasswordReset($user)); } protected function sendResetResponse(Request $request, $response) { $response = ['message' => "Password reset successful"]; return response($response, 200); } protected function sendResetFailedResponse(Request $request, $response) { $response = "Token Invalid"; return response($response, 401); }

También necesitamos importar algunas clases en el controlador agregando:

 use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash;

…hasta la parte superior del controlador.

También querremos modificar qué notificación de correo electrónico se usa, porque la notificación de correo que viene con Laravel no usa tokens API para la autorización. Podemos crear uno nuevo en app/Notifications ejecutando este comando:

 php artisan make:notification MailResetPasswordNotification

Tendremos que editar el archivo app/Notifications/MailResetPasswordNotification.php para que se vea así:

 <?php namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Support\Facades\Lang; class MailResetPasswordNotification extends ResetPassword { use Queueable; protected $pageUrl; public $token; /** * Create a new notification instance. * * @param $token */ public function __construct($token) { parent::__construct($token); $this->pageUrl = 'localhost:8080'; // we can set whatever we want here, or use .env to set environmental variables } /** * Get the notification's delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return ['mail']; } /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { if (static::$toMailCallback) { return call_user_func(static::$toMailCallback, $notifiable, $this->token); } return (new MailMessage) ->subject(Lang::getFromJson('Reset application Password')) ->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.')) ->action(Lang::getFromJson('Reset Password'), $this->pageUrl."?token=".$this->token) ->line(Lang::getFromJson('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.users.expire')])) ->line(Lang::getFromJson('If you did not request a password reset, no further action is required.')); } /** * Get the array representation of the notification. * * @param mixed $notifiable * @return array */ public function toArray($notifiable) { return [ // ]; } }

Para hacer uso de esta nueva notificación, debemos anular el método sendPasswordResetNotification que User hereda de la clase Authenticatable . Todo lo que tenemos que hacer es agregar esto a app/User.php :

 public function sendPasswordResetNotification($token) { $this->notify(new \App\Notifications\MailResetPasswordNotification($token)); }

Con una configuración de correo que funcione correctamente, las notificaciones deberían estar funcionando en este punto.

Todo lo que queda ahora es el control de acceso de los usuarios.

Tutorial de Laravel Passport, Paso 5: Crear middleware de control de acceso

Antes de crear el middleware de control de acceso, necesitaremos actualizar la tabla de user para tener una columna llamada type , que se usará para determinar el nivel de usuario: el tipo 0 es un usuario normal, el tipo 1 es un administrador y el tipo 2 es un superadministrador

Para actualizar la tabla de user , tenemos que crear una migración ejecutando esto:

 php artisan make:migration update_users_table_to_include_type --table=users

En el archivo recién creado del formulario base de database/migrations/[timestamp]_update_users_table.php , necesitaremos actualizar las funciones up y down para agregar y eliminar la columna de type , respectivamente:

 public function up() { Schema::table('users', function (Blueprint $table) { $table->integer('type'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropIfExists('type'); }); }

A continuación, ejecutaremos la php artisan migrate . Una vez hecho esto, tenemos que editar nuestra función de register en el archivo ApiAuthController.php , agregando esto justo antes de la línea con $user = User::create($request->toArray()); :

 $request['type'] = $request['type'] ? $request['type'] : 0;

Además, necesitaremos agregar esta línea a la matriz $validator :

 'type' => 'integer',

La primera de estas dos ediciones hará que todos los usuarios registrados sean "usuarios normales" por defecto, es decir, si no se ingresa ningún tipo de usuario.

El propio middleware de control de acceso

Ahora estamos en posición de crear dos piezas de middleware para usar en el control de acceso: una para administradores y otra para superadministradores.

Así que ejecutaremos:

 php artisan make:middleware AdminAuth php artisan make:middleware SuperAdminAuth

Primero, navegaremos a app/Http/Middleware/AdminAuth.php e importaremos Illuminate\Support\Facades\Auth , luego editaremos la función de handle así:

 public function handle($request, Closure $next) { if (Auth::guard('api')->check() && $request->user()->type >= 1) { return $next($request); } else { $message = ["message" => "Permission Denied"]; return response($message, 401); } }

También necesitaremos editar la función de handle en app/Http/Middleware/SuperAdminAuth.php :

 public function handle($request, Closure $next) { if (Auth::guard('api')->check() && $request->user()->type >= 2) { return $next($request); } else { $message = ["message" => "Permission Denied"]; return response($message, 401); } }

También debe importar la clase Auth en la parte superior de ambos archivos agregando:

 use Illuminate\Support\Facades\Auth;

…hasta el fondo de las importaciones encontradas allí.

Para usar nuestro nuevo middleware, haremos referencia a ambas clases en el kernel, es decir, en app/Http/Kernel.php agregando las siguientes líneas a la matriz $routeMiddleware :

 'api.admin' => \App\Http\Middleware\AdminAuth::class, 'api.superAdmin' => \App\Http\Middleware\SuperAdminAuth::class,

Si los desarrolladores quieren usar el middleware en una ruta determinada, todo lo que necesitan hacer es agregarlo a la función de ruta de esta manera:

 Route::post('route','Controller@method')->middleware('<middleware-name-here>');

<middleware-name-here> en este caso puede ser api.admin , api.superAdmin , etc., según corresponda.

Eso es todo lo que se necesita para crear nuestro middleware.

Poniendolo todo junto

Para probar que nuestra autenticación y control de acceso está funcionando, hay algunos pasos adicionales que debe seguir.

Prueba de autenticación y control de acceso de Laravel: Paso 1

Necesitamos modificar la función de index de ArticleController y registrar la ruta. (En proyectos del mundo real, usaríamos PHPUnit y haríamos esto como parte de una prueba automatizada. Aquí, estamos agregando manualmente una ruta con fines de prueba; se puede eliminar después).

Navegaremos hasta el controlador ArticleController en app/Http/Controllers/ArticleController y modificaremos la función de index para que se vea así:

 public function index() { $response = ['message' => 'article index']; return response($response, 200); }

A continuación, registraremos la función en una ruta yendo al archivo routes/api.php y agregando esto:

 Route::middleware('auth:api')->group(function () { Route::get('/articles', 'ArticleController@index')->name('articles'); });

Prueba de autenticación y control de acceso de Laravel: Paso 2

Ahora podemos intentar acceder a la ruta sin un token de autenticación. Deberíamos recibir un error de autenticación.

Una captura de pantalla del envío de una solicitud GET a /api/articles usando Postman, recibiendo un mensaje "No autenticado" en respuesta.

Prueba de autenticación y control de acceso de Laravel: Paso 3

También podemos intentar acceder a la misma ruta con un token de autorización (el que obtuvimos al registrarnos o iniciar sesión anteriormente en este artículo).

A veces, esto puede causar un error similar a este:

 Unknown column 'api_token' in 'where clause' (SQL: select * from `users` where `api_token` = ... 

Una captura de pantalla del error de columna desconocido de GETting the api/articles route using Postman.

Si esto sucede, los desarrolladores deben asegurarse de haber ejecutado una migración de Passport y tener ['guards']['api']['driver'] configurados en Passport en config/auth.php passport

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

Después de eso, la memoria caché de configuración también debe actualizarse.

Una vez que se solucione, deberíamos tener acceso a la ruta.

Captura de pantalla del envío de una solicitud GET a /api/articles usando Postman, con una respuesta JSON normal.

Prueba de autenticación y control de acceso de Laravel: Paso 4

Es hora de probar el control de acceso. ->middleware('api.admin') a la ruta de artículos, para que se vea así:

 Route::get('/articles', 'ArticleController@index')->middleware('api.admin')->name('articles');

Hicimos que a un usuario recién creado se le asigne automáticamente el tipo 0, como podemos ver a través de la ruta api/user .

Una captura de pantalla del envío de una solicitud GET a /api/user mediante Postman. La respuesta incluye una identificación, nombre, correo electrónico, marca de tiempo de verificación de correo electrónico nula, marcas de tiempo creadas y actualizadas rellenadas, y un tipo.

Debido a eso, deberíamos obtener un error al intentar acceder al punto final de los articles como tal usuario.

Captura de pantalla del envío de una solicitud GET a /api/articles mediante Postman, con una respuesta JSON de permiso denegado.

Para fines de prueba, modifiquemos el usuario en la base de datos para que tenga un type de 1. Después de verificar ese cambio a través de la ruta api/user nuevamente, estamos listos para intentar GET nuevamente la ruta /articles/ .

Captura de pantalla del envío de una solicitud GET a /api/articles como usuario autenticado de Laravel mediante Postman, idéntica a una solicitud anterior, con la excepción de que el tiempo de respuesta de esta fue de 1280 ms en lugar de 890 ms.

Funciona perfectamente.

Los desarrolladores que crean aplicaciones más complejas deben tener en cuenta que los controles de acceso adecuados no serán tan simples. En ese caso, se pueden usar otras aplicaciones de terceros o las puertas y políticas de Laravel para implementar un control de acceso de usuario personalizado. En la segunda parte de esta serie, veremos soluciones de control de acceso más sólidas y flexibles.

Autenticación de la API de Laravel: lo que hemos aprendido

En este tutorial de Laravel Passport, discutimos:

  1. Creando un controlador y modelo ficticio para tener algo para usar mientras probamos nuestro ejemplo de Laravel Passport.
  2. Creando el middleware necesario para que nuestra API funcione sin problemas, abordando CORS y obligando a la API a devolver siempre respuestas JSON.
  3. Configuración de la autenticación básica de la API de Laravel: registro, inicio de sesión y cierre de sesión.
  4. Configuración de la funcionalidad de "restablecimiento de contraseña" basada en el valor predeterminado de Laravel.
  5. Creación de middleware de control de acceso para agregar niveles de permiso de autorización de usuario a diferentes rutas.

Estas son habilidades esenciales para cualquiera que trabaje en el campo de los servicios de desarrollo de Laravel. Los lectores encontrarán el resultado final en este repositorio de GitHub y ahora deberían estar bien posicionados para implementar la autenticación con Laravel. Esperamos comentarios a continuación.