Curso de Laravel
Autenticación en Laravel: registro, inicio de sesión y rutas protegidas
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Hasta ahora nuestro CRUD está abierto: cualquiera que conozca la dirección puede entrar y borrar pacientes.
Hoy le ponemos puerta.
La autenticación es de esas cosas que no conviene escribir a mano. No porque sea difícil, sino porque los detalles que se escapan —el cifrado de la contraseña, la regeneración de la sesión, el límite de intentos— son justo los que causan problemas serios.
¡Empecemos!
Autenticación no es lo mismo que autorización
Dos palabras que se confunden y conviene separar desde el principio:
- Autenticación es saber quién eres. Iniciar sesión.
- Autorización es saber qué puedes hacer. Que un recepcionista no pueda borrar historiales.
Hoy vemos la primera. La segunda es la lección siguiente.
Lo que ya viene incluido
Un proyecto nuevo de Laravel trae más de lo que parece:
- El modelo
Usercon su migración - La tabla
password_reset_tokens - Toda la maquinaria de sesiones
- La fachada
Authy el middlewareauth
Lo único que falta son las pantallas: el formulario de registro, el de inicio de sesión y sus controladores.
Opción 1: un starter kit
La forma más rápida es empezar el proyecto con uno de los kits oficiales:
laravel new sistema-clinica
El instalador pregunta si quieres un starter kit y con qué tecnología de frontend. Cualquiera de ellos deja resuelto el registro, el inicio de sesión, la verificación de correo, la recuperación de contraseña y la edición del perfil.
Si vas a construir un sistema de gestión, usa un kit. No hay mérito en reescribir un formulario de inicio de sesión, y el que traen ya está bien hecho.
Lo que sí conviene es leer el código que genera. Está pensado para leerse, y explica cómo funciona todo.
Opción 2: hacerlo a mano
Vale la pena entender el mecanismo, aunque después uses un kit. Y en proyectos que ya empezaron sin autenticación, muchas veces es lo más simple.
Las rutas
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegistroController;
Route::middleware('guest')->group(function () {
Route::get('/registro', [RegistroController::class, 'mostrar'])->name('registro');
Route::post('/registro', [RegistroController::class, 'registrar']);
Route::get('/login', [LoginController::class, 'mostrar'])->name('login');
Route::post('/login', [LoginController::class, 'entrar']);
});
Route::post('/logout', [LoginController::class, 'salir'])
->middleware('auth')
->name('logout');
El middleware guest impide que alguien ya autenticado vuelva al formulario de inicio de sesión.
Que la ruta se llame login no es casual. Cuando alguien sin sesión intenta entrar a una zona protegida, Laravel redirige a la ruta con ese nombre exacto.
El registro
public function mostrar()
{
return view('auth.registro');
}
public function registrar(Request $request)
{
$datos = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
]);
$usuario = User::create($datos);
Auth::login($usuario);
$request->session()->regenerate();
return redirect()->route('pacientes.index')
->with('exito', 'Cuenta creada. Bienvenido, ' . $usuario->name);
}
Tres detalles que importan:
confirmed exige que exista un campo password_confirmation con el mismo valor. Solo hay que añadirlo al formulario:
<x-campo nombre="password" etiqueta="Contraseña" tipo="password" requerido />
<x-campo nombre="password_confirmation" etiqueta="Repetir contraseña" tipo="password" requerido />
Password::defaults() aplica la política de contraseñas del proyecto, que se define una sola vez en AppServiceProvider:
use Illuminate\Validation\Rules\Password;
Password::defaults(fn () => Password::min(8)->letters()->numbers()->uncompromised());
Ese uncompromised() comprueba si la contraseña aparece en filtraciones conocidas y la rechaza si es así. Cuesta una línea y evita las contraseñas que están en todos los diccionarios.
La contraseña no se cifra a mano. El modelo User ya la declara así:
protected function casts(): array
{
return [
'password' => 'hashed',
];
}
Con esa conversión, cualquier valor asignado a password se cifra automáticamente. Si haces Hash::make() además de eso, la ciframos dos veces y el inicio de sesión no funciona nunca. Es un error frecuente y desconcertante, porque no da ningún mensaje: simplemente las credenciales «no coinciden».
El inicio de sesión
public function entrar(Request $request)
{
$credenciales = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credenciales, $request->boolean('recordarme'))) {
return back()
->withInput($request->only('email'))
->withErrors(['email' => __('auth.failed')]);
}
$request->session()->regenerate();
return redirect()->intended(route('pacientes.index'));
}
Aquí hay cuatro cosas que merecen explicación.
Auth::attempt() busca el usuario por correo, compara la contraseña cifrada y, si todo cuadra, inicia la sesión. Devuelve true o false.
session()->regenerate() no es opcional. Genera un identificador de sesión nuevo tras el inicio de sesión. Sin eso, el sistema queda expuesto a un ataque de fijación de sesión, donde alguien fuerza al usuario a usar una sesión que el atacante ya conoce. Es una línea, y omitirla es un fallo de seguridad real.
redirect()->intended() devuelve al usuario a la página que intentaba abrir antes de que lo mandáramos a iniciar sesión. Si venía directo, usa la ruta que le pasas. Es un detalle pequeño con mucho efecto: quien pulsa un enlace a una cita concreta termina en esa cita y no en el inicio.
El mensaje de error es genérico a propósito. __('auth.failed') dice que las credenciales no coinciden, sin aclarar si falló el correo o la contraseña. Distinguirlos permitiría averiguar qué correos están registrados en el sistema.
Cerrar sesión
public function salir(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
Las tres líneas hacen falta: cerrar la sesión, invalidar los datos guardados y renovar el token CSRF.
Y en la vista, un formulario, nunca un enlace —por lo mismo que vimos al eliminar registros:
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit">Cerrar sesión</button>
</form>
Limitar los intentos
Sin esto, alguien puede probar contraseñas indefinidamente:
Route::post('/login', [LoginController::class, 'entrar'])
->middleware('throttle:5,1');
Cinco intentos por minuto. Para algo más fino, limitando por correo y no por dirección IP:
use Illuminate\Support\Facades\RateLimiter;
$clave = strtolower($request->email) . '|' . $request->ip();
if (RateLimiter::tooManyAttempts($clave, 5)) {
$segundos = RateLimiter::availableIn($clave);
return back()->withErrors([
'email' => __('auth.throttle', ['seconds' => $segundos]),
]);
}
if (! Auth::attempt($credenciales)) {
RateLimiter::hit($clave, 60);
return back()->withErrors(['email' => __('auth.failed')]);
}
RateLimiter::clear($clave);
Los mensajes salen traducidos si seguiste la lección anterior.
Proteger las rutas
Con la autenticación lista, cerrar el sistema es una línea:
Route::middleware('auth')->group(function () {
Route::resource('pacientes', PacienteController::class);
Route::resource('citas', CitaController::class);
Route::resource('doctores', DoctorController::class);
});
Cualquier intento de entrar sin sesión redirige al inicio de sesión, y al autenticarse el usuario vuelve a donde iba.
También puede aplicarse desde el controlador:
class PacienteController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth',
new Middleware('verified', only: ['create', 'store']),
];
}
}
Prefiero declararlo en las rutas. Se ve de un vistazo qué está protegido y qué no, sin abrir cada controlador.
Usar el usuario autenticado
Auth::user(); // el modelo completo
Auth::id(); // solo el id
Auth::check(); // ¿hay sesión?
Auth::guest(); // ¿no hay sesión?
auth()->user(); // la función auxiliar, equivalente
$request->user(); // desde el controlador
En Blade:
@auth
<span>Hola, {{ auth()->user()->name }}</span>
@endauth
@guest
<a href="{{ route('login') }}">Iniciar sesión</a>
@endguest
Y en el controlador, para registrar quién hizo qué:
$cita = Cita::create([
...$request->validated(),
'creado_por' => auth()->id(),
]);
Que es lo que ya adelantamos en la lección de crear registros: ese dato lo pone el servidor, nunca el formulario.
Mostrar solo lo del usuario
Un patrón muy frecuente en sistemas con varios usuarios:
public function index()
{
$citas = auth()->user()
->citas()
->with('paciente')
->latest('fecha')
->paginate(15);
return view('citas.index', compact('citas'));
}
Partir desde auth()->user() en lugar de Cita::all() hace que sea imposible mostrar datos de otro usuario por descuido. Es una costumbre que evita bastantes fugas de información.
Verificación de correo
Si quieres que los usuarios confirmen su correo, el modelo debe declararlo:
class User extends Authenticatable implements MustVerifyEmail
{
// ...
}
Y las rutas se protegen con un middleware más:
Route::middleware(['auth', 'verified'])->group(function () {
// ...
});
Laravel envía el correo automáticamente al registrarse. Requiere tener el envío configurado, que es la lección de correos.
¿Hace falta? En un sistema interno de una clínica, donde las cuentas las crea un administrador, no. En una aplicación con registro abierto, sí.
Recuperación de contraseña
Laravel trae toda la lógica hecha: genera un token, lo guarda en password_reset_tokens, envía el correo y valida el enlace. Solo hay que conectar las pantallas.
// Enviar el enlace
$estado = Password::sendResetLink($request->only('email'));
// Restablecer
$estado = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $usuario, string $password) {
$usuario->forceFill(['password' => $password])->save();
}
);
Los starter kits ya lo incluyen. Si lo haces a mano, la documentación oficial tiene el ejemplo completo.
El campo remember_token
Ese segundo parámetro de Auth::attempt() activa la sesión persistente:
Auth::attempt($credenciales, true);
Guarda una cookie de larga duración y el usuario sigue autenticado aunque cierre el navegador. Requiere la columna remember_token en la tabla, que ya viene en la migración por defecto.
Cuidado con activarlo en equipos compartidos. En sistemas con datos sensibles, muchas veces es mejor no ofrecer la opción.
Errores comunes
- Cifrar la contraseña con
Hash::make()cuando el modelo ya la declara comohashed. - Olvidar
session()->regenerate()tras el inicio de sesión. - Un enlace para cerrar sesión en vez de un formulario POST.
- Mensajes de error que distinguen entre correo inexistente y contraseña incorrecta.
- No limitar los intentos.
- Consultar
Modelo::all()en vez de partir desdeauth()->user(). - Nombrar la ruta de otra forma que no sea
login.
Para cerrar
La autenticación en Laravel es casi toda infraestructura ya resuelta: tú pones las pantallas y el framework pone la seguridad.
Lo que no se puede saltar: regenerate() tras el inicio de sesión, límite de intentos, y no cifrar la contraseña dos veces.
Con esto sabemos quién entra. En la siguiente lección decidimos qué puede hacer cada uno.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp