How to implement Argon2 into Symfony JWT authentication?
In this article, you’ll find an explanation of how the Argon2 password hashing algorithm can be implemented into a Symfony-based application. The algorithm is used to encrypt passwords and store them in a safe place. We will show you a practical example of processing it, step by step.
Table of contents
In order to start the process, you should install the JWT package (called jwt-authentication-bundle) to your application. Basic implementation of the authentication is well described in the Symfony documentation: https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html
After successful JWT installation, you can get started with Argon2 implementation.
Step 1: Preparing for Symfony JWT authentication - security config
According to the documentation, we must set up the firewall logic and access control. Firewall nodes define the logic by which our endpoints have access to the protected resource. Here we might create rules with routes that should be guarded with our JWT authentication system. You can create as many rules as you want for a given regular expression of a route. Access control is responsible for defining types of protection. You might define routes that are publicly accessible or fully restricted. Remember that order matters.
You should, in the first place, set paths that would be exceptional from the JWT authentication or public APIs to the most restricted and detailed ones. Be aware of changing the order in already existing apps, because that might really start to be a problem. Additionally, we would need to add a provider node that would read the user from the DB that is currently reviewing in the security package. Additionally, we would need to set our custom password hasher or any other that we would like.
security:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
id: 'App\Hasher\Argon2idHasher'
providers:
user_provider:
entity:
class: App\Entity\User
property: username
Step 2: Argon2idHasher - our custom password hasher
A password hasher is responsible for verifying our password. In JWT authentication, only the verify method is used to compare incoming passwords with the one in the DB. This is what our hasher looks like:
class Argon2idHasher implements PasswordHasherInterface
{
public function hash(string $plainPassword): string
{
return $plainPassword;
}
public function verify(string $hashedPassword, string $plainPassword): bool
{
$hashedPassword =
PasswordTransformer::transformFromPassword($hashedPassword);
return password_verify($plainPassword, $hashedPassword);
}
public function needsRehash(string $hashedPassword): bool
{
return false;
}
}
As I mentioned before in one of my other articles, I would recommend storing passwords in the DB without Argon2id hash options in the password. In my opinion, it’s quite important to do so. This is why we are using the ‘PasswordTransformer’ class to create the default Argon2 hash.
ref: How to improve user password security with Argon2?
class PasswordTransformer
{
private const SCHEMA = [
0 => '',
1 => 'argon2id',
2 => 'v=19',
3 => 'options',
4 => 'salt',
5 => 'password',
];
public static function transformToHashPassword(string $password): string
{
$hash = password_hash($password, PASSWORD_ARGON2ID, AuthenticatorOptionEncrypter::getOptions());
$hash = explode('$', $hash);
return $hash[4]."$".$hash[5];
}
public static function transformFromPassword(string $password): string
{
$password = explode('$', $password);
$schema = self::SCHEMA;
$options = [];
$options['m'] = AuthenticatorOptionEncrypter::getMemoryCost();
$options['t'] = AuthenticatorOptionEncrypter::getTimeCost();
$options['p'] = AuthenticatorOptionEncrypter::getThreads();
$option = '';
foreach ($options as $key => $item) {
$option .= sprintf(
'%s=%s,',
$key,
$item,
);
}
$schema[3] = substr($option, 0, -1);
$schema[4] = $password[0];
$schema[5] = $password[1];
return implode('$', $schema);
}
}
Step 3: Symfony JWT authentication - password options
Create a simple class that will contain your password hasher options. Here you can customize the parameters however you like.
class AuthenticationPasswordOptions
{
private const MEMORY_COST = 1<<17;
private const TIME_COST = 5;
private const THREADS = 6;
public static function getMemoryCost(): int
{
return self::MEMORY_COST;
}
public static function getTimeCost(): int
{
return self::TIME_COST;
}
public static function getThreads(): int
{
return self::THREADS;
}
public static function getOptions(): array
{
return [
'memory_cost' => self::MEMORY_COST,
'time_cost' => self::TIME_COST,
'threads' => self::THREADS,
];
}
}
Step 4: Testing the new Symfony JWT authentication
The Symfony documentation shows us how we should do it, but here is a curl for validation:
curl -X POST -H "Content-Type: application/json" http://localhost:8000/api/login_check -d '{"username":"johndoe","password":"test"}'
Response:
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NTkzNTI5ODMsIm
V4cCI6MTY1OTM1NjU4Mywicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoiam9obmR
vZSJ9.ZAV4F1-z6ErlyGt_4o82Zf9RQKs2resUO1hLxLTkSEIKD0mg9EypN616-jBX4aM3f9e
qKOk-EoEfdMUak0Me7wl0E5oREmAdh0jNcXTD-ccy68XsehzKSQCjpMQdpxNrrMVtsb-tfP8d
Y05lmExn_Z2X4SzNCG-YT4DS_9j6K7k2IEkf8mS4x8ozX5QcTN_nBnw-bHUFGtKCJqnPkDsvg
yEEJQUWAllnhMcleUyNiWIvL37o1K8DFmlrYLlrt3QLpQdMXyKOC2QjB5xKIuLSLZF9h_MBRa
sUB2kLcSW3nVlHU79auH6MFFkkoArzse5PMkKNTmudvXxfCO3HroeRhw"}
We’ve prepared an example app that contains the following working implementation in Symfony. Here is a link to the repository:
Share this article: