Blog


Autenticación JWT en Symfony

AUTENTICACIÓN JWT EN SYMFONY

10 / 02 / 2023 Symfony

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.



ARTÍCULOS RELACIONADOS