La autenticación JWT es un mecanismo seguro para autenticar usuarios en una aplicación web. En este artículo, vamos a mostrar cómo implementarlo en un proyecto desarrollado con Symfony, utilizando LexikJWTAuthenticationBundle.
Instalar el bundle
Para comenzar, debemos instalar el bundle lexik/jwt-authentication-bundle en nuestro proyecto Symfony. Podemos hacerlo utilizando composer:
composer require lexik/jwt-authentication-bundle
Configurar el bundle
Una vez instalado el bundle, debemos configurarlo para que funcione con nuestra aplicación. En el fichero de configuración de seguridad de Symfony (config/packages/security.yaml), debemos añadir las siguientes líneas:
security:
password_hashers:
App\Entity\Usuario:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
stateless: true
anonymous: true
json_login:
check_path: /login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
logout:
path: /logout
success_handler: lexik_jwt_authentication.handler.logout_success
En esta configuración, estamos especificando que nuestra entidad de usuarios se llama App\Entity\User y tiene una propiedad "email". También estamos especificando que queremos utilizar el guard JWT para proteger las rutas de nuestra aplicación.
Crear una clave privada y pública
Antes de poder generar y validar tokens JWT, debemos crear una clave privada y pública. Podemos hacerlo utilizando OpenSSL:
openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
En este ejemplo, estamos creando una clave privada y pública en el directorio config/jwt. Es importante guardar estas claves de forma segura y nunca compartirlas con nadie.
Generar un token JWT
Una vez configurado el bundle y creadas las claves, podemos generar tokens JWT para nuestros usuarios. Para ello, podemos crear un controlador que maneje el login de los usuarios, y utilizar el servicio lexik_jwt_authentication.encoder para generar el token.
<?php
namespace App\Controller;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class LoginController extends AbstractController
{
private $passwordEncoder;
private $jwtEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder, JWTEncoderInterface $jwtEncoder)
{
$this->passwordEncoder = $passwordEncoder;
$this->jwtEncoder = $jwtEncoder;
}
public function login(Request $request)
{
$user = $this->getDoctrine()
->getRepository(User::class)
->findOneBy(['email' => $request->get('email')]);
if (!$user) {
return new JsonResponse(['error' => 'Invalid email or password'], 401);
}
if (!$this->passwordEncoder->isPasswordValid($user, $request->get('password'))) {
return new JsonResponse(['error' => 'Invalid email or password'], 401);
}
$token = $this->jwtEncoder->encode([
'email' => $user->getEmail(),
'exp' => time() + 3600, // 1 hour expiration
]);
return new JsonResponse(['token' => $token]);
}
}
En este ejemplo, estamos creando un controlador que maneja el login de los usuarios. Primero, buscamos al usuario en la base de datos utilizando su email, y luego comprobamos si la contraseña es válida. Si todo es correcto, generamos un token JWT utilizando el servicio lexik_jwt_authentication.encoder y lo devolvemos al cliente.
Validar un token JWT
Una vez generado el token JWT, el cliente debe incluirlo en las cabeceras de las peticiones que realiza para acceder a las rutas protegidas. El bundle se encarga de validar el token y permitir o denegar el acceso al usuario.
Crear un servicio de autenticación
Además de crear un controlador para manejar el login de los usuarios, también podemos crear un servicio de autenticación para manejar la lógica de la autenticación JWT.
<?php
namespace App\Service;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class JwtAuthService
{
private $passwordEncoder;
private $jwtEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder, JWTEncoderInterface $jwtEncoder)
{
$this->passwordEncoder = $passwordEncoder;
$this->jwtEncoder = $jwtEncoder;
}
public function login(string $email, string $password): array
{
$user = $this->getDoctrine()
->getRepository(User::class)
->findOneBy(['email' => $email]);
if (!$user) {
throw new \InvalidArgumentException('Invalid email or password');
}
if (!$this->passwordEncoder->isPasswordValid($user, $password)) {
throw new \InvalidArgumentException('Invalid email or password');
}
return $this->createToken($user);
}
public function getUserFromToken(string $token): User
{
try {
$payload = $this->jwtEncoder->decode($token);
} catch (JWTDecodeFailureException $e) {
throw new \InvalidArgumentException('Invalid token');
}
$user = $this->getDoctrine()
->getRepository(User::class)
->findOneBy(['email' => $payload['email']]);
if (!$user) {
throw new \InvalidArgumentException('Invalid token');
}
return $user;
}
private function createToken(User $user): array
{
try {
$token = $this->jwtEncoder->encode([
'email' => $user->getEmail(),
'exp' => time() + 3600, // 1 hour expiration
]);
} catch (JWTEncodeFailureException $e) {
throw new \RuntimeException('Error while encoding token');
}
return [
'token' => $token,
'user' => $user,
];
}
}
Agregar la ruta de login y proteger las rutas
Finalmente, agregamos la ruta de login a nuestro archivo de rutas (config/routes.yaml) y protegemos las rutas que deseemos que requieran autenticación utilizando el guard JWT que configuramos anteriormente:
login:
path: /login
controller: App\Controller\LoginController::login
api:
resource: '.'
type: annotation
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
En este ejemplo, estamos agregando una ruta de login que utiliza el controlador que creamos anteriormente. También estamos protegiendo todas las rutas de nuestra sección "api" utilizando el guard JWT.
Con estos pasos, tendríamos una implementación básica de autenticación JWT en nuestro proyecto Symfony utilizando LexikJWTAuthenticationBundle. Es importante mencionar que este es solo un ejemplo básico, y debemos añadir más medidas de seguridad en nuestra aplicación, como por ejemplo, un sistema de refresco de tokens.