How to Add JWT Login to a Symfony Project
Tags: symfony, php, jwt
Published: 2023-11-21 14:06:06

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:

```bash
symfony new jwt-test
```

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:

```bash
composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
composer require lexik/jwt-authentication-bundle
```

## 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:

```bash
php bin/console make:user
```

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:

```bash
php bin/console doctrine:database:create
php bin/console make:migration
doctrine:migrations:migrate
```

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:

```bash
php bin/console ecurity:hash-password
```

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:

```yaml
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: ~
```

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:

```yaml
access_control:
    - { path: ^/api/login, roles: PUBLIC_ACCESS }
    - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
```

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:

```diff
@@ -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 }
 ```

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

```bash
php bin/console lexik:jwt:generate-keypair
```

## 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:

```yaml
api_login_check:
    path: /api/login_check
```

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.

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


## 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:

```bash
php bin/console make:controller
```

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
<?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',
        ]);
    }
}
```

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.

```bash
curl --request POST \
  --url http://localhost:8000/api/home \
  --header 'Authorization: Bearer REPLACE_YOUR_TOKEN' \
  --header 'Content-Type: application/json'
```

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