Regresar

Creando una API en Laravel con rutas básicas de autenticación

API

13 de febrero de 2025

En este proyecto, hemos desarrollado una API en Laravel que incluye rutas básicas para la autenticación de usuarios, implementando funcionalidades como registro, inicio de sesión, restablecimiento de contraseña y verificación de correo electrónico.

Enlaces

Probar API Repositorio Github

Prerrequisitos

Para implementar este proyecto, es necesario contar con:

  • Laravel 10+ instalado y configurado.
  • PHP 8.1+ con las extensiones necesarias habilitadas.
  • Composer para gestionar las dependencias.
  • Base de datos MySQL o PostgreSQL, configurada en el archivo .env.
  • Sanctum para la autenticación basada en tokens.
  • Configuración de correo en .env para el envío de emails de recuperación y verificación.

Creación y configuración del proyecto

Para comenzar, creamos un nuevo proyecto en Laravel utilizando el Laravel Installer. Ejecutamos el siguiente comando en la terminal:

laravel new api-auth

Esto iniciará el proceso de instalación, durante el cual se nos presentarán varias opciones de configuración:

  1. Starter Kit: Seleccionamos No starter kit, ya que construiremos la autenticación manualmente con Laravel Sanctum.
  2. Framework de pruebas: Elegimos PHPUnit, que es el predeterminado en Laravel.
  3. Inicializar un repositorio Git: Seleccionamos Yes para comenzar con control de versiones desde el inicio.
  4. Base de datos: Elegimos MySQL, que es el sistema de gestión de bases de datos que utilizaremos.
 mateo092  ~ / Desktop / projects  laravel new api-auth

   _                               _
  | |                             | |
  | |     __ _ _ __ __ ___   _____| |
  | |    / _` | '__/ _` \ \ / / _ \ |
  | |___| (_| | | | (_| |\ V /  __/ |
  |______\__,_|_|  \__,_| \_/ \___|_|


 Would you like to install a starter kit? ────────────────────┐
 No starter kit                                               
 └──────────────────────────────────────────────────────────────┘

 Which testing framework do you prefer? ──────────────────────┐
 PHPUnit                                                      
 └──────────────────────────────────────────────────────────────┘

 Would you like to initialize a Git repository? ──────────────┐
 Yes                                                          
 └──────────────────────────────────────────────────────────────┘

 Which database will your application use? ───────────────────┐
 MySQL                                                        
 └──────────────────────────────────────────────────────────────┘

Una vez completada la instalación, accedemos al directorio del proyecto:

cd api-auth

Con esto, nuestro proyecto base en Laravel está listo para continuar con la configuración..

Configuración variables de entorno

En el archivo .env, ubicado en la raíz del proyecto, almacena las variables de entorno esenciales para la configuración de Laravel. Aquí definiremos las credenciales de la base de datos y la configuración del servidor de correo.

Configuración de la base de datos

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=api_auth
DB_USERNAME=root
DB_PASSWORD= 

Laravel necesita conectarse a una base de datos para almacenar usuarios y tokens de autenticación. Ajustamos las siguientes variables según nuestra configuración local: 

  • DB_CONNECTION: Define el motor de base de datos, en este caso mysql.
  • DB_HOST y DB_PORT: Especifican la dirección y el puerto de la base de datos.
  • DB_DATABASE: Nombre de la base de datos del proyecto.
  • DB_USERNAME y DB_PASSWORD: Credenciales para acceder a la base de datos.

Configuración del servidor de correo

MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=example@gmail.com
MAIL_PASSWORD=a*************
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=example@gmail.com
MAIL_FROM_NAME="${APP_NAME}"

Para gestionar el envío de correos electrónicos (como confirmaciones de cuenta y restablecimiento de contraseña), configuramos un servicio SMTP:

  • MAIL_MAILER: Define el protocolo de envío de correos (smtp).
  • MAIL_HOST, MAIL_PORT y MAIL_ENCRYPTION: Configuran el servidor SMTP de Gmail.
  • MAIL_USERNAMEMAIL_PASSWORD: Son las credenciales del correo que enviará los mensajes.
  • MAIL_FROM_ADDRESSMAIL_FROM_NAME: Especifican el remitente de los correos.

Si usamos Gmail, debamos habilitar una contraseña de aplicación para evitar errores de autenticación.

Configuración de Sanctum y migración de tablas

Laravel Sanctum es un paquete ligero para la autenticación de APIs con tokens. Para instalarlo, ejecutamos el siguiente comando:

php artisan install:api

Este comando instala Sanctum, configura las rutas de autenticación y genera una migración para la tabla de tokens. Al finalizar, Laravel preguntará si deseas ejecutar las migraciones pendientes y, si la base de datos aún no existe, ofrecerá crearla automáticamente. En ambos casos, responde yes para continuar.

 mateo092  main  / Desktop / projects / api-auth  php artisan install:api
./composer.json has been updated
Running composer update laravel/sanctum
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking laravel/sanctum (v4.0.8)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing laravel/sanctum (v4.0.8): Extracting archive
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
  INFO  Published API routes file.  


 One new database migration has been published. Would you like to run all pending database migrations? (yes/no) [yes]:
 > yes

   WARN  The database 'api_auth' does not exist on the 'mysql' connection.  

 Would you like to create it? ────────────────────────────────┐
 Yes                                                          
 └──────────────────────────────────────────────────────────────┘

   INFO  Preparing database.  

  Creating migration table ....................................................... 18.49ms DONE

   INFO  Running migrations.  

  0001_01_01_000000_create_users_table ........................................... 42.29ms DONE
  0001_01_01_000001_create_cache_table ........................................... 11.21ms DONE
  0001_01_01_000002_create_jobs_table ............................................ 30.22ms DONE
  2025_02_13_225621_create_personal_access_tokens_table .......................... 19.33ms DONE


   INFO  API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model. 

Para finalizar la configuración de Sanctum, agregamos el trait HasApiTokens al modelo User, lo que permitirá la generación y gestión de tokens de autenticación.

Configuración del modelo User

<?php

namespace App\Models;

 
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, HasApiTokens;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
   //more code ...

Además de agregar el trait HasApiTokens para la gestión de tokens de autenticación, también es necesario habilitar la verificación de correos electrónicos.

Para ello, en el archivo app/Models/User.php, debemos descomentar e implementar la interfaz MustVerifyEmail:

Configuración de enlaces personalizados para restablecimiento de contraseña y verificación de correo

Laravel, por defecto, utiliza URLs firmadas para la confirmación de correos electrónicos, lo que significa que solo pueden ser accedidas desde el mismo dispositivo que envió la solicitud. Sin embargo, en algunos casos, esta restricción puede ser poco flexible, por ejemplo, si el usuario abre el enlace en otro dispositivo.

Para evitar este problema y permitir que los enlaces sean accesibles desde cualquier dispositivo, personalizamos la generación de URLs en el AppServiceProvider.

Modificación en AppServiceProvider.php

<?php

namespace App\Providers;

use App\Models\User;

use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {

        ResetPassword::createUrlUsing(function (User $user, string $token) {

            return config('app.frontend_url') . '/reset-password?token=' .  $token . '&email=' . $user->email;
        });

        VerifyEmail::createUrlUsing(function (User $user) {

            $token = Str::random(60);

            Cache::put('email_verification_' . $token, $user->id, now()->addHour());

            return config('app.frontend_url') . '/verify-email?token=' . $token;
        });
    }
}

En el método boot() , sobrescribimos la forma en que Laravel genera los enlaces de restablecimiento de contraseña y verificación de correo:

Explicación de los cambios

  1. Restablecimiento de contraseña (ResetPassword::createUrlUsing):

    • Laravel genera un enlace que incluye el token de restablecimiento y el correo electrónico del usuario.
    • Esto permite que el frontend tenga control sobre la UI y maneje la lógica de restablecimiento de contraseña de manera más flexible.
  2. Verificación de correo electrónico (VerifyEmail::createUrlUsing):

    • En lugar de usar las URLs firmadas predeterminadas de Laravel, generamos un token aleatorio con Str::random(60).
    • Guardamos este token en caché junto con el user_id durante  1 horas (now()->addHour()), lo que permite validar la cuenta sin depender del dispositivo de origen.

¿Por qué hacer esta implementación?

  • Mayor flexibilidad: Los enlaces de verificación de correo pueden abrirse desde cualquier dispositivo, no solo desde el que hizo la solicitud.
  • Mejor integración con el frontend: Como se usa config('app.frontend_url'), los usuarios serán dirigidos directamente a la aplicación frontend, donde se puede manejar la lógica de verificación o restablecimiento.
  • Seguridad: Al almacenar el token en caché en el backend, evitamos exponer información sensible en la URL.

Con esta configuración, Laravel podrá manejar la autenticación y recuperación de cuentas de forma más flexible y eficiente.

Arquitectura del Proyecto

├── Http
│   ├── Controllers
│   │   ├── AuthController.php
│   │   ├── Controller.php
│   │   ├── EmailVerificationController.php
│   │   └── UserController.php
│   ├── Requests
│   │   ├── ForgotPasswordRequest.php
│   │   ├── LoginRequest.php
│   │   ├── RegisterRequest.php
│   │   └── ResetPasswordRequest.php
│   └── Resources
│       └── UserResource.php
├── Models
│   └── User.php
├── Providers
│   └── AppServiceProvider.php
└── Services
    ├── AuthService.php
    └── EmailVerificationService.php

7 directories, 13 files

Laravel ya nos proporciona una estructura de archivos bien organizada, así que trabajaremos sobre ella con algunas pequeñas modificaciones. En este caso, agregamos una carpeta Services/ para centralizar la lógica de autenticación y verificación de correos. A continuación, explicamos qué hace cada parte del proyecto:

📂 Http/

  • Controllers/: Contiene los controladores que manejan las solicitudes HTTP y coordinan la ejecución de los servicios.
    • AuthController.php: Gestiona la autenticación de usuarios (registro, inicio de sesión y cierre de sesión).
    • EmailVerificationController.php: Controla la verificación de correo electrónico.
    • UserController.php: Maneja operaciones relacionadas con el usuario autenticado.
  • Requests/: Define las reglas de validación para las solicitudes.
    • RegisterRequest.php: Valida los datos de registro.
    • LoginRequest.php: Valida las credenciales de inicio de sesión.
    • ForgotPasswordRequest.php: Valida la solicitud de restablecimiento de contraseña.
    • ResetPasswordRequest.php: Valida la actualización de la nueva contraseña.
  • Resources/: Contiene transformadores de datos para formatear las respuestas.
    • UserResource.php: Se encarga de definir qué información del usuario se devuelve en la API.

📂 Models/

  • User.php: Define el modelo de usuario y sus relaciones con otras entidades.

📂 Providers/

  • AppServiceProvider.php: Configura la generación de enlaces para restablecer contraseña y verificar correo.

📂 Services/

  • AuthService.php: Contiene la lógica de autenticación y gestión de tokens.
  • EmailVerificationService.php: Maneja la lógica de verificación de correo electrónico.

Implementación de la Lógica de Negocio

Empezaremos desarrollando los servicios, ya que encapsulan la lógica principal de la API. Luego, avanzaremos con los controladores para manejar las solicitudes y respuestas.

AuthService

Este servicio maneja toda la lógica de autenticación. Veamos cada método.

Método Register

public function register(array $data): array
{
    $user = User::create([
        'name'      => $data['name'],
        'email'     => $data['email'],
        'password'  => $data['password']
    ]);

    $token = $user->createToken('auth_token')->plainTextToken;

    event(new Registered($user));

    return [
        'user'  => $user,
        'token' => $token,
    ];
}

Este método se encarga de registrar a un nuevo usuario en la aplicación. Primero, crea un usuario en la base de datos utilizando los datos proporcionados, aprovechando que Laravel hashea automáticamente la contraseña gracias a la configuración en el modelo. Luego, genera un token de autenticación para el usuario con Sanctum, lo que le permitirá autenticarse en futuras solicitudes. Después, lanza el evento Registered, el cual Laravel utiliza para manejar procesos relacionados con el registro, como el envío de correos de verificación. Finalmente, devuelve el usuario recién creado junto con su token de autenticación.

Método login

public function login(array $data): array
{
    if (!Auth::attempt($data)) {
        throw ValidationException::withMessages([
            'email' => ['Las credenciales no coinciden con nuestros registros.'],
        ]);
    }

    $user = User::where('email', $data['email'])->first();

    $token = $user->createToken('auth_token')->plainTextToken;

    return [
        'user' => $user,
        'token' => $token,
    ];
}

Este método maneja el inicio de sesión del usuario. Primero, utiliza Auth::attempt para verificar si las credenciales ingresadas son correctas. Si no coinciden, lanza una ValidationException con un mensaje de error. Si las credenciales son válidas, obtiene el usuario desde la base de datos utilizando su correo electrónico. Luego, genera un nuevo token de autenticación con Sanctum y lo asocia al usuario. Finalmente, devuelve el usuario autenticado junto con el token, permitiéndole realizar solicitudes autenticadas en la API.

Método logout

public function logout(int $userId): void
{
    $user = User::find($userId);

    $user->tokens()->delete();
}

Este método gestiona el cierre de sesión del usuario. Primero, busca al usuario en la base de datos utilizando su ID. Luego, accede a la relación de tokens del usuario y los elimina con delete(), revocando así todos sus tokens de autenticación activos. Esto garantiza que el usuario quede completamente desautenticado y no pueda realizar más solicitudes protegidas hasta que vuelva a iniciar sesión.

Método forgotPassword

public function forgotPassword(array $data): void
{
    $status = Password::sendResetLink($data);

    if ($status === Password::INVALID_USER) {
        throw ValidationException::withMessages([
            'email' => ['El usuario no existe.'],
        ]);
    }

    if ($status === Password::RESET_THROTTLED) {
        throw ValidationException::withMessages([
            'email' => ['Demasiados intentos. Intenta de nuevo en un minuto.'],
        ]);
    }

    if ($status !== Password::RESET_LINK_SENT) {
        throw new Exception('Error sending email');
    }
}

Este método permite a un usuario solicitar un enlace para restablecer su contraseña. Utiliza Password::sendResetLink() para enviar el correo con las instrucciones de recuperación. Luego, maneja tres posibles errores: si el usuario no existe, si ha excedido el número de intentos permitidos o si hubo un fallo inesperado al enviar el correo. En cada caso, se lanza una excepción adecuada para informar al usuario del problema.

Método resetPassword

public function resetPassword(array $data): void 
{
    $status = Password:: reset(
        $data,
        function (User $user, string $password) {

            $user -> tokens() -> delete ();

            $user -> forceFill([
                'password' => Hash:: make($password)
            ]) -> setRememberToken(Str:: random(60));

            $user -> save();

            //event(new PasswordReset($user));

            Log:: info("Contraseña restablecida para el usuario: {$user->email}");
        }
    );

    if ($status === Password::RESET_THROTTLED) {
        throw ValidationException:: withMessages([
            'email' => ['Demasiados intentos. Intenta de nuevo en un minuto.'],
        ]);
    }

    if ($status === Password::INVALID_TOKEN) {
        throw ValidationException:: withMessages(['token' => 'El token de es inválido.']);
    }

    if ($status === Password::INVALID_USER) {
        throw ValidationException:: withMessages([
            'email' => ['El usuario no existe.'],
        ]);
    }

    if ($status !== Password::PASSWORD_RESET) {
        throw new Exception('No se pudo restablecer la contraseña. Intenta de nuevo.');
    }
}

Este método permite a un usuario restablecer su contraseña utilizando un token de recuperación. Primero, llama a Password::reset(), que valida los datos y actualiza la contraseña del usuario si todo es correcto. Antes de guardar la nueva contraseña, se eliminan los tokens activos y se genera un nuevo token de sesión. Si la operación falla, se manejan diferentes errores como intentos excesivos, un token inválido o un usuario inexistente. Finalmente, si la actualización no se realiza con éxito, se lanza una excepción general.

Servicio EmailVerificationService

Este servicio se encarga de la gestión de la verificación de correo electrónico para los usuarios. Su principal función es enviar correos de confirmación para validar la identidad del usuario.

Método sendEmail

public function sendEmail(int $userId): void
{
    $user = User::find($userId);

    if (!$user) {
        throw ValidationException::withMessages(['email' => ['El usuario no existe.']]);
    }

    if ($user->hasVerifiedEmail()) {
        throw ValidationException::withMessages(['email' => ['El correo electrónico ya está verificado.']]);
    }

    $user->sendEmailVerificationNotification();
}

Este método envía un correo de verificación a un usuario específico. Primero, busca al usuario por su ID. Si no existe, lanza una excepción. Luego, verifica si el usuario ya tiene su correo validado, en cuyo caso también se genera un error. Si el usuario aún no ha verificado su correo, se envía la notificación correspondiente para completar el proceso de validación.

Método verifyEmail

public function verifyEmail(string $token): void
{
    $userId = Cache::pull('email_verification_' . $token);

    if (!$userId) {
        throw ValidationException::withMessages(['token' => ['El token de verificación es inválido.']]);
    }

    $user = User::find($userId);

    if (!$user->hasVerifiedEmail()) {
        $user->markEmailAsVerified();
    }
}

Este método verifica el correo electrónico de un usuario utilizando un token de verificación. Primero, recupera el ID del usuario desde la caché usando el token proporcionado. Si el token es inválido o ha expirado, lanza una excepción. Luego, busca al usuario en la base de datos y, si su correo aún no está verificado, lo marca como verificado.

Controladores

Los controladores en esta aplicación actúan como intermediarios entre las solicitudes del usuario y la lógica de negocio, delegando la mayor parte del procesamiento a los servicios correspondientes. Para mantener el tutorial conciso, explicaremos únicamente el AuthController, ya que su estructura y funcionamiento son representativos de otros controladores en el sistema.

AuthController

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ForgotPasswordRequest;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\ResetPasswordRequest;
use App\Http\Resources\UserResource;
use App\Services\AuthService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    public function __construct(
        protected AuthService $authService
    ) {}

    public function login(LoginRequest $request): JsonResponse
    {
        $data = $request->validated();

        $response = $this->authService->login($data);

        return response()->json(['data' =>[
            'user' => new UserResource($response['user']),
            'token' => $response['token']
        ]]);
    }

    public function register(RegisterRequest $request): JsonResponse
    {
        $data = $request->validated();

        $response = $this->authService->register($data);

        return response()->json(['data' => [
            'user' => new UserResource($response['user']),
            'token' => $response['token']
        ]], 201);
    }

    public function logout(Request $request): JsonResponse
    {
        $userId = $request->user()->id;

        $this->authService->logout($userId);

        return response()->json(['message' => 'logged out successfully'], 200);
    }

    public function forgotPassword(ForgotPasswordRequest $request): JsonResponse
    {
        
        $this->authService->forgotPassword($request->validated());

        return response()->json(['message' => 'reset link sent on your email']);
    }

    public function resetPassword(ResetPasswordRequest $request): JsonResponse
    {
        $data = $request->validated();

        $this->authService->resetPassword($data);

        return response()->json(['message' => 'password reset successfully']);
    }

    
}

Este controlador maneja las solicitudes relacionadas con la autenticación, validando los datos a través de Form Requests y delegando la lógica de negocio al servicio AuthService. De esta manera, se mantiene un código más limpio y modular. Sus métodos incluyen el registro de usuarios, inicio y cierre de sesión, así como la recuperación y restablecimiento de contraseñas. Cada acción sigue un flujo donde se recibe la solicitud validada, se pasa la información al servicio correspondiente y se devuelve una respuesta JSON al cliente, asegurando una comunicación clara y estructurada.

Definición de rutas 

<?php

use App\Http\Controllers\AuthController;
use App\Http\Controllers\EmailVerificationController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;


Route::prefix('v1')->group(function () {

    Route::middleware('throttle:5,1')->group(function () {

        Route::post('/login',             [AuthController::class, 'login']);
        Route::post('/register',          [AuthController::class, 'register']);
        Route::post('/forgot-password',   [AuthController::class, 'forgotPassword']);
        Route::post('/reset-password',    [AuthController::class, 'resetPassword']);
        Route::post('/verify-email',      [EmailVerificationController::class, 'verifyEmail']);
    });

    Route::middleware(['auth:sanctum'])->group(function () {

        Route::get('/user',                                 UserController::class);
        Route::post('/resend-email-verification',     [EmailVerificationController::class, 'sendEmail']);

        Route::delete('/logout',      [AuthController::class, 'logout']);
    });
});

Con los controladores definidos, ahora configuramos los endpoints en el archivo api.php. En esta sección:

  • Las rutas públicas están protegidas con el middleware throttle:5,1, que limita a 5 solicitudes por minuto para evitar abusos.
  • Las rutas protegidas requieren autenticación mediante auth:sanctum, impidiendo el acceso a usuarios no autenticados.

Esto asegura que solo los usuarios autenticados puedan acceder a información sensible, mientras que las rutas de autenticación están protegidas contra intentos de fuerza bruta.

Ejecución del proyecto y prueba de endpoints

Antes de interactuar con la API, es necesario asegurarse de que el servidor de Laravel esté en funcionamiento

php artisan serve

Esto iniciará el servidor en http://127.0.0.1:8000. Ahora.

Probando los Endpoints

Para verificar el funcionamiento de la API, se pueden realizar peticiones con Postman

Screenshot_2025-02-18_10-45-04.png

Con esta prueba, la API de autenticación con Laravel Sanctum queda lista para su integración en otros sistemas. En la documentación puedes encontrar más información sobre los endpoints, su estructura y parámetros, e incluso probarlos directamente desde la interfaz.