En este tutorial vamos a ver cómo crear un login sin contraseña con Laravel y Livewire. El usuario introducirá su email, recibirá un código de seis cifras y, al escribirlo, accederá a su cuenta. La primera vez que lo haga, la cuenta se creará automáticamente.

Es la versión con Livewire del tutorial en el que vimos cómo crear un login sin contraseña en Laravel. La lógica del código es la misma, pero en lugar de controladores y formularios de Blade que recargan la página, usaremos un único componente de Livewire que pasa del primer paso al segundo sin recargar nada.

Introducción

Vamos a crear una sencilla aplicación con Laravel y Livewire en la que todo el login vivirá en un único componente. Al igual que en el tutorial anterior, protegeremos el envío de los códigos con Cloudflare Turnstile usando el paquete Laracaptcha, que se integra directamente con Livewire.

Antes de continuar, es recomendable que tengas ciertos conocimientos previos de PHP y HTML. Para ello puedes consultar las siguientes guías:

También te recomiendo que leas antes el tutorial del login sin contraseña en Laravel, ya que reutilizaremos la clase que genera y comprueba los códigos, así como el correo que los envía, y aquí no volveré a explicarlos en detalle.

Puedes consultar el código de la aplicación que vamos a crear en GitHub. Este es el aspecto que tendrá el formulario de acceso:

Formulario de login sin contraseña con Livewire y Cloudflare Turnstile

Instalación de Laravel y Livewire

Antes de comenzar, tendrás que asegurarte de que tienes composer instalado en tu sistema, así como PHP 8.3 o superior. Si no tienes PHP instalado en tu sistema, consulta el tutorial de instalación de PHP en Windows.

Crea un proyecto con Laravel

Para crear un proyecto con Laravel, abre una ventana de línea de comandos y sigue estos pasos:

  1. Sitúate en el directorio en el que quieres crear el proyecto y ejecuta el comando create-project:

    composer create-project laravel/laravel login-livewire
    
  2. Seguidamente, accede al proyecto usando el comando cd:

    cd login-livewire
    
  3. Con esto ya habrás creado el proyecto.

Instala Livewire y Laracaptcha

A continuación, instala Livewire y Laracaptcha con el siguiente comando:

composer require livewire/livewire edulazaro/laracaptcha

Usaremos Livewire 4, que es la versión actual. Si quieres conocer otras opciones de instalación, consulta la documentación de instalación de Livewire. En la documentación de Laracaptcha encontrarás todas las opciones del paquete.

Configura Turnstile y el correo

La configuración del archivo .env es la misma que en el tutorial anterior. Agrega las claves de prueba de Cloudflare, que siempre dan la verificación por buena, y asegúrate de que los correos se escriben en el log:

CAPTCHA_DRIVER=turnstile
TURNSTILE_KEY=1x00000000000000000000AA
TURNSTILE_SECRET=1x0000000000000000000000000000000AA

MAIL_MAILER=log

Configuración inicial del proyecto

Antes de crear el componente vamos a preparar todo lo que necesita: la clase del código, el correo, el layout y las rutas.

Reutiliza la clase del código y el correo

Estas piezas no dependen de Livewire, así que son exactamente las mismas que en el tutorial anterior:

  • La clase App\Auth\EmailCode: genera el código, guarda su hash en la caché durante diez minutos, admite cinco intentos y limita los códigos por dirección. Puedes copiarla del repositorio en GitHub.
  • El mailable App\Mail\LoginCode: envía el código, que incluye también en el asunto.
  • La migración que permite que la columna password sea nula, ya que estas cuentas no tienen contraseña.

Si quieres entender cómo funcionan, consulta el apartado en el que explico cómo crear la clase del código en el tutorial anterior.

Crea el layout

Livewire renderiza los componentes de página dentro del layout layouts::app, que se corresponde con el archivo resources/views/layouts/app.blade.php. Créalo con este contenido:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? 'Entrar' }} · {{ config('app.name') }}</title>
    <link rel="stylesheet" href="/login.css">
</head>
<body>
    <main class="card">
        {{ $slot }}
    </main>
</body>
</html>

No es necesario incluir los scripts de Livewire, ya que la versión 4 los agrega automáticamente a las páginas que contienen componentes.

Crea las rutas

En Livewire 4, los componentes de página se registran directamente como rutas con el método Route::livewire. Edita el archivo routes/web.php y agrega estas rutas:

Route::redirect('/', '/panel');

Route::livewire('/login', 'pages::login')->middleware(['guest', 'throttle:30,1'])->name('login');
Route::livewire('/panel', 'pages::panel')->middleware('auth')->name('dashboard');

La ruta login mostrará el componente del login y la ruta dashboard un sencillo panel para los usuarios que hayan iniciado sesión. Puedes consultar más detalles acerca de este tipo de rutas en la documentación de las páginas de Livewire.

Ten en cuenta que el middleware throttle solo limita las veces que se carga la página. Las acciones del componente se envían a la ruta interna de Livewire, que no pasa por estos middlewares, por lo que los límites de envío de códigos tendrán que estar dentro del propio componente.

Implementación del login

Ya tenemos todo lo necesario, así que vamos a crear el componente del login.

Crea el componente

Ejecuta el siguiente comando para crear el componente:

php artisan make:livewire pages::login

Esto creará el archivo resources/views/pages/⚡login.blade.php. En Livewire 4, los componentes se crean por defecto en un único archivo que contiene tanto la clase PHP como la vista. El símbolo ⚡ del nombre sirve para distinguirlos de las vistas normales; si lo prefieres sin él, puedes usar la opción --emoji=false.

El componente tendrá tres propiedades: el paso en el que se encuentra el usuario, su email y el código. Además, usará el trait WithCaptcha de Laracaptcha, que agrega la propiedad $captcha y los métodos que comprueban el captcha:

new #[Title('Entrar')] class extends Component
{
    use WithCaptcha;

    private const CODES_PER_ORIGIN = [3600 => 6, 86400 => 20];

    public string $step = 'email';

    public string $email = '';

    public string $code = '';
};

La constante CODES_PER_ORIGIN limita los códigos que puede pedir una misma máquina: seis por hora y veinte al día, sumando todas las direcciones. Si quieres saber más acerca de las propiedades, consulta la documentación de las propiedades de Livewire.

Envía el código

En la parte de la vista, el primer paso muestra el formulario con el email y el widget de Turnstile. El widget se vincula a la propiedad $captcha con wire:model, ya que Livewire envía las propiedades del componente y no los campos del formulario:

<form wire:submit="sendCode">
    <label for="email">Email</label>
    <input id="email" type="email" wire:model="email" required autofocus autocomplete="username">
    @error('email') <p class="error">{{ $message }}</p> @enderror

    @unless ($this->captchaPassed('login'))
        <x-laracaptcha::widget wire:model="captcha" language="es" />
        @error('captcha') <p class="error">{{ $message }}</p> @enderror
    @endunless

    <button type="submit">Enviarme el código</button>
</form>

Al enviar el formulario se ejecuta el método sendCode:

public function sendCode(): void
{
    $this->email = Str::lower(trim($this->email));

    $this->validate(
        ['email' => ['required', 'string', 'email', 'max:255']],
        ['email.required' => 'Escribe tu email.', 'email.email' => 'Ese email no es válido.', 'email.max' => 'Ese email es demasiado largo.'],
    );

    $this->verifyCaptchaOnce('login');

    $this->issue();

    $this->step = 'code';
    $this->code = '';
}

Fíjate en que normalizamos el email antes de validarlo. Livewire no pasa por el middleware de Laravel que elimina los espacios de los campos, así que un email pegado con un espacio al final no sería válido.

El método verifyCaptchaOnce comprueba el captcha y guarda en la sesión que el usuario lo ha superado. De este modo, si se equivoca de email y vuelve a empezar, no tendrá que resolverlo de nuevo, y por eso la vista solo muestra el widget mientras captchaPassed('login') sea falso. Si la comprobación falla, se produce un error de validación en el campo captcha.

Protege los métodos públicos

El envío lo hace el método privado issue, que también usaremos para reenviar el código. Lo primero que hace es comprobar que la sesión ha superado el captcha:

private function issue(): void
{
    if (! $this->captchaPassed('login')) {
        $this->step = 'email';

        throw ValidationException::withMessages(['captcha' => 'Completa la verificación de seguridad.']);
    }

    // ...límites por dirección y por máquina, y envío del código
}

Esta comprobación es importante. Cualquier método público de un componente de Livewire se puede ejecutar desde el navegador, y sus propiedades se pueden modificar. Sin ella, alguien podría escribir cualquier email en la propiedad $email y llamar directamente al método resend, que reenvía el código sin pasar por el captcha. Tienes más información en la documentación de seguridad de Livewire.

El resto del método es igual que en el tutorial anterior: comprueba los límites de la clase EmailCode y los de la máquina, y envía el código. Puedes consultar el componente completo en GitHub.

Verifica el código

Tras enviar el código, el componente pasa al segundo paso sin recargar la página:

Formulario para introducir el código con Livewire

Para procesarlo, agrega el método verify:

public function verify(): void
{
    $this->validate(
        ['code' => ['required', 'digits:6']],
        ['code.required' => 'Escribe el código.', 'code.digits' => 'El código tiene 6 cifras.'],
    );

    $codes = EmailCode::for($this->email);

    $error = match ($codes->check($this->code)) {
        EmailCode::OK => null,
        EmailCode::WRONG => 'Ese no es el código. Te quedan '.$codes->triesLeft().' intentos.',
        EmailCode::BURNT => 'Demasiados intentos con ese código. Pide uno nuevo.',
        EmailCode::GONE => 'El código ha caducado. Pide uno nuevo.',
        EmailCode::LOCKED => 'Demasiados códigos incorrectos hoy para este email. Vuelve a intentarlo mañana.',
    };

    if ($error !== null) {
        throw ValidationException::withMessages(['code' => $error]);
    }

    $user = User::firstOrCreate(['email' => $this->email], ['name' => Str::before($this->email, '@')]);

    if ($user->email_verified_at === null) {
        $user->forceFill(['email_verified_at' => now()])->save();
    }

    Auth::login($user, remember: true);
    session()->regenerate();

    $this->redirectIntended(route('dashboard'));
}

Al igual que en el tutorial anterior, la cuenta se crea cuando el usuario introduce el código correcto y se marca como verificada, ya que el código que ha llegado a su email ya demuestra que la dirección es suya. La diferencia es que aquí los errores se lanzan con ValidationException, que Livewire muestra junto al campo con la directiva @error.

Para volver al primer paso, el método useAnother reinicia el componente:

public function useAnother(): void
{
    $this->reset('step', 'code');
    $this->resetValidation();
}

Si quieres saber más acerca de los métodos que se invocan desde la vista, consulta la documentación de las acciones de Livewire.

Crea el panel

Finalmente, crea el componente del panel con el comando php artisan make:livewire pages::panel, que mostrará el usuario y un botón para cerrar la sesión:

new #[Title('Panel')] class extends Component
{
    public function logout(): void
    {
        Auth::logout();

        session()->invalidate();
        session()->regenerateToken();

        $this->redirectRoute('login');
    }
};

Tests

Livewire permite probar los componentes con Livewire::test, sin necesidad de un navegador. Al igual que antes, usaremos Mail::fake() y Captcha::fake() para no enviar correos ni hacer peticiones a Cloudflare:

public function test_calling_resend_directly_without_the_captcha_sends_nothing(): void
{
    Livewire::test('pages::login')
        ->set('email', 'victima@example.com')
        ->set('step', 'code')
        ->call('resend')
        ->assertHasErrors('captcha')
        ->assertSet('step', 'email');

    Mail::assertNothingSent();
}

Este test comprueba justamente lo que vimos en el apartado de seguridad: alguien que modifica las propiedades y llama directamente al método resend no consigue enviar ningún código. En el repositorio encontrarás diez tests que cubren todo el flujo. Puedes consultar más información en la documentación de los tests de Livewire.

Para ejecutarlos, usa este comando:

php artisan test

Finalizando

Con esto ya habrás creado un login sin contraseña con Laravel y Livewire. Antes de usarlo en producción, ten en cuenta los mismos puntos que en el tutorial anterior: un servidor SMTP real, tus propias claves de Turnstile y los registros SPF configurados en el dominio desde el que envías los correos.

Si en lugar de Livewire usas Inertia, o prefieres la versión con Blade, aquí tienes el resto de la serie:

Recuerda que siempre puedes consultar el código completo de la aplicación en GitHub. Si quieres usar Laracaptcha en otros formularios, consulta la documentación de Laracaptcha o su repositorio en GitHub.

Esto ha sido todo. Espero que este tutorial te haya resultado útil.