Symfony REST API (without FosRestBundle) using JWT authentication: PART 2

Hicham BEN KACHOUD
9 min readNov 25, 2020

--

In the first part ( Post 1) We explored how to implement the Rest API without using FosRestBunlde. In this post, we are going to secure the implementation by using JWT Authentication.

1- What is JWT ?

JWT (JSON Web Token) is a very popular technology that we use to transport data between interested parties (client & server). It is an encoder string that can contain unlimited amount of data, and it is technically signed. A very common use of a JWT token is as an API authentication mechanism.

It is the mechanism used by Google to let you using their APIs.

2- JWT format:

AZnbh57g-Sgu559Sgyhs.H61bSL0_Sg6-Sfravakjd.Uydiduhz76Zçsyyd-ZY-gsd

It consists of three parts which is separated by (.):

a- The header: It specifie the information about the type of JWT and the algorithm used to generate the signature.

b- The payload: It contains application specific information (like username in our example), along with additional informational of the token.

c- The signature: It is the final and last part of a JWT which is generated by combining and hashing the first two parts along with a secret key.

3- Install the JWT Bundle:

To use JWT inside a symfony project, we need to install JWTAuthenticationBundle using the following command:

composer require lexik/jwt-authentication-bundle

Install JWTAuthenticationBundle with composer

This bundle is going to make creating and validating JSON web tokens easier.

4- Generate SSH Keys:

In order to get our Secret key we need to generate a private & public key pair. The private key will be used to sign the JSON web tokens.

If someone else gets it, they’ll be able to create new JSON web tokens with whatever information they want , for example with someone else’s username to get access to their account.

There are many ways of creating keys, but we will do it using command line:

Firstly, we need to create a folder named jwt inside a config folder which is going to contain our pair keys:

mkdir config/jwt

Secondly, we are going to execute the two following commands:

openssl genrsa -out config/jwt/private.pem -aes256 4096
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem

After executing those commands we get the following results:

.env file

This above file specifies the emplacement of the pair keys

JWTAuthentication config

We now have a private.pem and a public.pem inside the folder jwt.

5- Create a User Entity:

We need to create an entity User which implements the UserInterface.

<?php
/**
* Created by PhpStorm.
* User: hicham benkachoud
* Date: 05/01/2020
* Time: 22:07
*/
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
*
@ORM\Table(name="users")
*
@ORM\Entity
*/
class User implements UserInterface
{
/**
*
@ORM\Column(type="integer")
*
@ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
@ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
*
@ORM\Column(type="string", length=255)
*/
private $password;
/**
*
@ORM\Column(type="string", length=45)
*/
private $email;
/**
* User constructor.
*
@param $username
*/
public function __construct($username)
{
$this->username = $username;
}
/**
*
@return string
*/
public function getUsername()
{
return $this->username;
}
/**
*
@param mixed $username
*/
public function setUsername($username): void
{
$this->username = $username;
}
/**
*
@return string|null
*/
public function getSalt()
{
return null;
}
/**
*
@return string|null
*/
public function getPassword()
{
return $this->password;
}
/**
*
@param $password
*/
public function setPassword($password)
{
$this->password = $password;
}
/**
*
@return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
*
@param mixed $email
*/
public function setEmail($email): void
{
$this->email = $email;
}
/**
*
@return array|string[]
*/
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
}

Let’s just create a new file called User.php inside the src/Entity folder, and after run the command:

php bin/console doctrine:schema:update — force to create our table users in database.

6- Configuration:

Lastly we need to tell Symfony’s security system about our Provider and Authenticator:

security:
encoders:
App\Entity\User:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
login:
pattern: ^/api/login
stateless: true
anonymous: 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
provider: app_user_provider
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
# activate different ways to authenticate
#
https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

We are going to explain the above file:

encoders: let’s you define how users password are encoded. After you can use the UserPasswordEncoderInterface service to do this before saving your users to the database.

providers: This section creates a “user provider” called app_user_provider, that knows how to query from your App\Entity\User entity by the username property.

firewalls: Let’s take a look at all of this part:

For the login route, we indicate that our login route must be accessible to anonymous users, we also indicate that the JWT must take care of managing the verification of user information with its own methods.
For the rest of our API everything is stateless, each request must contain authentication information, all routes that start with API will be protected by the JWT.

access_control: For each incoming request, Symfony will decide which access control to use based on the URI, the client’s IP address, the incoming host name, and the request method. It then enforces access restrictions based on the roles options:

If the user does not have the given role, then access is denied. If this value is an array of multiple roles, the user must have at least one of them.

7- Create Authentrication controller:

Let’s create an AuthController.php .

<?php
/**
* Created by PhpStorm.
* User: hicham benkachoud
* Date: 06/01/2020
* Time: 20:39
*/
namespace App\Controller;
use App\Entity\User;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class AuthController extends ApiController
{
public function register(Request $request, UserPasswordEncoderInterface $encoder)
{
$em = $this->getDoctrine()->getManager();
$request = $this->transformJsonBody($request);
$username = $request->get('username');
$password = $request->get('password');
$email = $request->get('email');
if (empty($username) || empty($password) || empty($email)){
return $this->respondValidationError("Invalid Username or Password or Email");
}
$user = new User($username);
$user->setPassword($encoder->encodePassword($user, $password));
$user->setEmail($email);
$user->setUsername($username);
$em->persist($user);
$em->flush();
return $this->respondWithSuccess(sprintf('User %s successfully created', $user->getUsername()));
}
/**
*
@param UserInterface $user
*
@param JWTTokenManagerInterface $JWTManager
*
@return JsonResponse
*/
public function getTokenUser(UserInterface $user, JWTTokenManagerInterface $JWTManager)
{
return new JsonResponse(['token' => $JWTManager->create($user)]);
}
}

Let’s create an ApiController which can help us to customise the reponse and request of our api.

<?php
/**
* Created by PhpStorm.
* User: hicham benkachoud
* Date: 06/01/2020
* Time: 20:39
*/
namespace App\Controller; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
class ApiController extends AbstractController
{
/**
*
@var integer HTTP status code - 200 (OK) by default
*/
protected $statusCode = 200;
/**
* Gets the value of statusCode.
*
*
@return integer
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* Sets the value of statusCode.
*
*
@param integer $statusCode the status code
*
*
@return self
*/
protected function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
/**
* Returns a JSON response
*
*
@param array $data
*
@param array $headers
*
*
@return JsonResponse
*/
public function response($data, $headers = [])
{
return new JsonResponse($data, $this->getStatusCode(), $headers);
}
/**
* Sets an error message and returns a JSON response
*
*
@param string $errors
*
@param $headers
*
@return JsonResponse
*/
public function respondWithErrors($errors, $headers = [])
{
$data = [
'status' => $this->getStatusCode(),
'errors' => $errors,
];
return new JsonResponse($data, $this->getStatusCode(), $headers);
}
/**
* Sets an error message and returns a JSON response
*
*
@param string $success
*
@param $headers
*
@return JsonResponse
*/
public function respondWithSuccess($success, $headers = [])
{
$data = [
'status' => $this->getStatusCode(),
'success' => $success,
];
return new JsonResponse($data, $this->getStatusCode(), $headers);
}
/**
* Returns a 401 Unauthorized http response
*
*
@param string $message
*
*
@return JsonResponse
*/
public function respondUnauthorized($message = 'Not authorized!')
{
return $this->setStatusCode(401)->respondWithErrors($message);
}
/**
* Returns a 422 Unprocessable Entity
*
*
@param string $message
*
*
@return JsonResponse
*/
public function respondValidationError($message = 'Validation errors')
{
return $this->setStatusCode(422)->respondWithErrors($message);
}
/**
* Returns a 404 Not Found
*
*
@param string $message
*
*
@return JsonResponse
*/
public function respondNotFound($message = 'Not found!')
{
return $this->setStatusCode(404)->respondWithErrors($message);
}
/**
* Returns a 201 Created
*
*
@param array $data
*
*
@return JsonResponse
*/
public function respondCreated($data = [])
{
return $this->setStatusCode(201)->response($data);
}
// this method allows us to accept JSON payloads in POST requests
// since Symfony 4 doesn’t handle that automatically:
protected function transformJsonBody(\Symfony\Component\HttpFoundation\Request $request)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
return $request;
}
$request->request->replace($data); return $request;
}
}

Let’s create the two routes concern register and login_check in the file routes.yaml

#index:
# path: /
# controller: App\Controller\DefaultController::index
register:
path: /register
controller: App\Controller\AuthController::register
methods: POST
api_login_check:
path: /api/login_check
controller: App\Controller\AuthController::getTokenUser
test:
path: /api/test
controller: App\Controller\ApiController::test

5- Test the APIs.

In the privious post, we used to list, create, update and delete post without using any authentication like the following picture (posts list).

before jwt

After installing and configuring jwt authentication, let’s see what happens when we try to attack our API.

after jwt token

The server returns an error code 401 Unauthorized because it does not identify us and specifies the error: No token was found.

In the bellow picture, we are going to register user.

create user

let’s try to authenticate:

First, we will authenticate ourselves. For this, we enter the login URL, on my local machine, api/login_check, and we send in the body of the request our JSON object which contains the username key with value the username of our admin and the password key with its password.

we checked with wrong password and we get invalid credentials as the result.

So, let’s try with the correct credentials:

get token

what it contains ? The token is signed using public and private keys.

So let’s make a request using this token. Postman integrates this notion of authentication and allows you to easily specify the token for a request in the “Authorization” tab.

We will choose the type of authentication by selecting “Bearer token”. We paste our token previously obtained via the login route in the field provided for this purpose.

success with token

So, we have recovered our list of post in json format!

The code source can be found: Here

The first part of this implementation can be found here.

That’s all!

Here you have the basics of setting up JWT token authentication on an API. To refine the functioning of this, it would be necessary to improve the token management system, especially in the event that they are expired. What is recommended in terms of security is a token with a very short lifespan but refreshed with each request made.

--

--