DEV Community

Daniyal Javani
Daniyal Javani

Posted on • Updated on

How to Add JWT Login to a Symfony Project

In this tutorial, I will show you how to implement a simple JWT authentication system for your Symfony project. JWT stands for JSON Web Token, which is a standard for securely transmitting information between parties as a JSON object. JWT can be used to authenticate users and authorize access to protected resources, such as APIs.

Prerequisites

To follow this tutorial, you will need:

  • PHP 8.0 or higher
  • Composer
  • Symfony CLI
  • A database server (MySQL, PostgreSQL, etc.)
  • A tool to test your API requests (Postman, Insomnia, etc.)

Creating a New Symfony Project

First, let's create a new Symfony project using the Symfony CLI. Open a terminal and run the following command:

symfony new jwt-test
Enter fullscreen mode Exit fullscreen mode

This will create a new Symfony project in the jwt-test directory.

Installing the Required Packages

Next, we need to install some packages that we will use for this tutorial. We will use the Doctrine ORM to interact with the database, the MakerBundle to generate code, and the LexikJWTAuthenticationBundle to handle the JWT authentication.

To install these packages, run the following commands:

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
composer require lexik/jwt-authentication-bundle
Enter fullscreen mode Exit fullscreen mode

Creating the User Entity

Now we need to create a User entity that will represent our users in the database. We will use the MakerBundle to generate the code for us. Run the following command:

php bin/console make:user
Enter fullscreen mode Exit fullscreen mode

This will ask you some questions about the User entity, such as the class name, the properties, and the password hashing algorithm. You can accept the default values or customize them as you wish. I'll be keeping things simple for this tutorial and sticking with the default settings.

This will generate the following files:

  • src/Entity/User.php: The User entity class
  • src/Repository/UserRepository.php: The User repository class

Configure your database and run the following commands to create the users table:

php bin/console doctrine:database:create
php bin/console make:migration
doctrine:migrations:migrate
Enter fullscreen mode Exit fullscreen mode

The final step in the database process is to create a user within the database. You need to first hash your password using this command:

php bin/console ecurity:hash-password
Enter fullscreen mode Exit fullscreen mode

Copy the "Password hash" and paste it into the password column of the user. Set the roles column as [].

Configuring the Security

Next, we need to configure the security system to use the User entity and the JWT authentication. We will make some changes to the config/packages/security.yaml file.

We need to define two firewalls: one for the login endpoint, and one for the API endpoints. The login firewall will use the json_login authenticator, which will allow us to send the username and password as a JSON object and receive a JWT token in response. The API firewall will use the jwt authenticator, which will validate the JWT token and grant access to the protected resources.

To define the firewalls, add the following lines under the firewalls key:

firewalls:
    login:
        pattern: ^/api/login
        stateless: true
        json_login:
            check_path: /api/login_check
            success_handler: lexik_jwt_authentication.handler.authentication_success
            failure_handler: lexik_jwt_authentication.handler.authentication_failure
    api:
        pattern:   ^/api
        stateless: true
        jwt: ~
Enter fullscreen mode Exit fullscreen mode

The pattern option defines the URL pattern that matches the firewall. The stateless option indicates that the firewall does not use sessions or cookies. The check_path option defines the URL that will handle the login request. The success_handler and failure_handler options define the services that will handle the login success and failure events. The jwt option enables the JWT authenticator.

Finally, we need to define some access control rules to restrict access to the endpoints based on the user roles. We will use the PUBLIC_ACCESS role to allow anyone to access the login endpoint, and the IS_AUTHENTICATED_FULLY role to require a valid JWT token to access the API endpoints.

To define the access control rules, add the following lines under the access_control key:

access_control:
    - { path: ^/api/login, roles: PUBLIC_ACCESS }
    - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
Enter fullscreen mode Exit fullscreen mode

The path option defines the URL pattern that matches the rule. The roles option defines the required roles to access the path.

Here are the final changes that have been made in the config/packages/security.yaml file:

@@ -10,6 +12,17 @@ security:
                 class: App\Entity\User
                 property: email
     firewalls:
+        login:
+            pattern: ^/api/login
+            stateless: true
+            json_login:
+                check_path: /api/login_check
+                success_handler: lexik_jwt_authentication.handler.authentication_success
+                failure_handler: lexik_jwt_authentication.handler.authentication_failure
+        api:
+            pattern:   ^/api
+            stateless: true
+            jwt: ~
         dev:
             pattern: ^/(_(profiler|wdt)|css|images|js)/
             security: false
@@ -17,17 +30,9 @@ security:
             lazy: true
             provider: app_user_provider

     access_control:
-        # - { path: ^/admin, roles: ROLE_ADMIN }
-        # - { path: ^/profile, roles: ROLE_USER }
+        - { path: ^/api/login, roles: PUBLIC_ACCESS }
+        - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
Enter fullscreen mode Exit fullscreen mode

To create JWT tokens, it is necessary to generate key pairs:

php bin/console lexik:jwt:generate-keypair
Enter fullscreen mode Exit fullscreen mode

Configuring the Routes

Next, we need to configure the routes for the login and the API endpoints. We will use the config/routes.yaml file to define the routes.

First, we need to define a route for the login endpoint, which will be handled by the api_login_check controller. This controller is provided by the LexikJWTAuthenticationBundle and will generate and return the JWT token. To define the route, add the following lines to the config/routes.yaml file:

api_login_check:
    path: /api/login_check
Enter fullscreen mode Exit fullscreen mode

The path option defines the URL that matches the route. The api_login_check controller is automatically registered by the bundle, so we don't need to specify the controller name.

Next, we need to define a route for the API endpoint, which will be handled by our custom controller. We will create this controller in the next step. To define the route, we will use the #[Route] attribute on the controller class.

Make a request to the login endpoint to obtain a JWT token.

curl --request POST \
  --url http://localhost:8000/api/login_check \
  --header 'Content-Type: application/json' \
  --data '{
    "username": "REPLACE_YOUR_EMAIL",
    "password": "REPLACE_YOUR_PASSWORD"
  }'
Enter fullscreen mode Exit fullscreen mode

Creating the API Controller

Now we need to create a controller that will handle the API requests. We will use the MakerBundle to generate the code for us. Run the following command:

php bin/console make:controller
Enter fullscreen mode Exit fullscreen mode

This will ask you the name of the controller class. You can choose any name you want, but for this tutorial, I will use HomeController. This will generate the following file:

  • src/Controller/HomeController.php: The HomeController class

We will use the TokenStorageInterface service to get the logged-in user and display their email.

To modify the controller class, replace the content of the src/Controller/HomeController.php file with the following code:

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class HomeController extends AbstractController
{
    #[Route('api/home', name: 'app_home')]
    public function home(TokenStorageInterface $tokenStorage): JsonResponse
    {
        $token = $tokenStorage->getToken();

        $user = $token->getUser();

        return $this->json([
            'message' => sprintf('Welcome to your new controller %s!', $user->getEmail()),
            'path' => 'src/Controller/HomeController.php',
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The #[Route] attribute defines the route for the home action. The TokenStorageInterface service is injected as a parameter to the home method. The getToken method returns the current authentication token. The getUser method returns the actual user!

Make a request to the secured endpoint using the obtained JWT token to get the logged-in user's email.

curl --request POST \
  --url http://localhost:8000/api/home \
  --header 'Authorization: Bearer REPLACE_YOUR_TOKEN' \
  --header 'Content-Type: application/json'
Enter fullscreen mode Exit fullscreen mode

Congratulations! You've successfully added JWT authentication to your Symfony 6 project. Enjoy the secure authentication mechanism for your APIs.

Top comments (2)

Collapse
 
skorolev-developer profile image
Sergey

Your forgot about command lexik:jwt:generate-keypair

Collapse
 
daniyaljavani profile image
Daniyal Javani • Edited

Thank you, my friend. Yes, you are absolutely right. I have just resolved the issue.