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
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
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
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
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
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: ~
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 }
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 }
To create JWT tokens, it is necessary to generate key pairs:
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:
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.
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:
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
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.
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.
Top comments (2)
Your forgot about command lexik:jwt:generate-keypair
Thank you, my friend. Yes, you are absolutely right. I have just resolved the issue.