Configuring JWT Authentication with Symfony can be quite tricky, especially for beginners. We’ll guide you through a step-by-step tutorial getting you up to speed. We use LexikJWTAuthenticationBundle to setup JWT Auth in less than 10 mins.
Our setup for JWT Authentication with Symfony
- Symfony 3.x, 4.x, 5.x
- FosUserBundle (you may use any other user provider as well)
- LexikJWTAuthenticationBundle (used to setup JWT authentication)
If you are very new to JWT(JSON Web Tokens), it is highly recommended that you have a basic understanding of how it works. Here’s a short video that’ll give you an idea –
We will be using the LexikJWTAuthenticationBundle for configuring JWT Authentication. The steps to setup the same are enlisted below…
1. Setup LexikJWTAuthenticationBundle
Install via composer
# if composer is installed globally composer require "lexik/jwt-authentication-bundle" # or you can use php archive of composer php composer.phar require "lexik/jwt-authentication-bundle"
- For Symfony 2.x – Symfony 3.3 :
// Register bundle into app/AppKernel.php: public function registerBundles() { return array( // ... new Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle(), ); }
-
For Symfony 3.4, Symfony 4.x, and above, Flex will have generated the bundles automatically, so no manual intervention is necessary.
2. Generate the SSH Public/Private keys
We create a temporary folder config/jwt
to store the public and private keys. Execute the following in the Terminal
–
# create a folder $ mkdir -p config/jwt # For Symfony3+, no need of the -p option # generate the private key and store it in temporary folder # Provide a strong passphrase when asked and note it. $ openssl genrsa -out config/jwt/private.pem -aes256 4096
While generating the private, you will be asked for a passphrase. Enter a strong passphrase and note it somewhere as we will need it later to update the configuration. As an example, we’ll use ThisIsThePassPhrase
as our passphrase.
# generate the public key using the private key $ openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem # You will be asked to verify the passphrase you provided earlier.
The private(private.pem
) and public(public.pem
) keys should now be generated in the config/jwt
directory.
3. Configure the JWT Public and Private keys
Get the Public and private keys in base64 format
We now obtain the base64 encoded public/private keys by executing the following in the Terminal
.
# Prints the private/public keys in base64 format cat config/jwt/private.pem | base64 | tr -d '\n' // S09obkNhWjZnaDUVTRFWQ4R0J4MkJMQUtLR0lZNEsxdEQxc.... cat config/jwt/public.pem | base64 | tr -d '\n' // FQTRRRUdMVTRFWQ4R0J4MkJtS09obkNhW....
Update your .env file
Copy the private/public base64 encoded SSH keys obtained in the previous step and update the values of JWT_SECRET_KEY and JWT_PUBLIC_KEY respectively in the .env
file. Also update the JWT_PASSPHRASE to the same passphrase that was provided while generating the keys.
### .env file ### ###> lexik/jwt-authentication-bundle ### JWT_SECRET_KEY=S09obkNhWjZnaDUVTRFWQ4R0J4MkJMQUt # Base64 encoded Private Key JWT_PUBLIC_KEY=FQTRRRUdMVTRFWQ4R0J4MkJtS09obkNhW # Base64 encoded Public Key JWT_PASSPHRASE=ThisIsThePassPhrase # The same Passphrase you provided earlier
Once this is done, the config/jwt/*
folder can be deleted as its not needed anymore. But if you plan to leave it there, remember to not commit it to GIT.
Update the lexik_jwt_authentication config
For Symfony 3.4, 4.x, a configuration file will be generated automatically at config/packages/lexik_jwt_authentication.yaml
. Update it as shown below. (For Symfony 3.3 and below, you can add the following in security.yaml
)
### config/packages/lexik_jwt_authentication.yaml file ### lexik_jwt_authentication: secret_key: '%env(base64:JWT_SECRET_KEY)%' # required for token creation public_key: '%env(base64:JWT_PUBLIC_KEY)%' # required for token verification pass_phrase: '%env(JWT_PASSPHRASE)%' # required for token creation, usage of an environment variable is recommended token_ttl: 7200 # default is 3600 seconds
We now define the route that should be used for generating a valid JWT.
4. Update Route
Define the api_login_check
path in your routes.yaml
. This route is the endpoint which accepts the user credentials and returns the token(JWT) if authenticated successfully. An error will be returned if the credentials are incorrect/missing.
# config/routes.yaml api_login_check: path: /api/login_check
5. Update Firewall
There are 2 common ways in which you can authenticate your user – json_login
or form_login
.
We will see how to use json_login. Configure your config/packages/security.yaml
.
# security.yaml security: # ... firewalls: login: pattern: ^/api/login stateless: true anonymous: true json_login: # or form_login provider: fos_userbundle #or your custom user provider check_path: /api/login_check #same as the configured route success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure require_previous_session: false api: pattern: ^/api # protected path stateless: true guard: authenticators: - lexik_jwt_authentication.jwt_token_authenticator access_control: - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
6. Obtaining the token
You can test getting the token with a simple curl command in your Terminal
like this (adapt host and port):
# test json_login curl -X POST -H "Content-Type: application/json" http://localhost/api/login_check -d '{"_username":"johndoe","_password":"test"}' # test form_login curl -X POST http://localhost:3000/data -d "_username=johndoe&_password=test" # you should receive something like this as a response: { "token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY" }
Store the Token
Once you get the JWT, store it on the client side. You could either use Html5 Web Storage(LocalStorage/SessionStorage) or Cookies. Each has its advantage and disadvantage. Here’s a resource that might be helpful in deciding.
Whatever you decide, ensure that you always send your JWT over https and never http. The JWT is reusable until its ttl
has expired (3600 seconds by default).
As an example, we’ll store the token in LocalStorage
–
// app.js let token = ''; // get token from response localStorage.setItem('jwt', token); // store token in localStorage
7. Verify/Use the token
To use the token, you need to only pass the JWT with your request to the protected firewall. You can pass it either as an authorization header or as a query parameter.
A. Send JWT as an “Authorization” header
The authorization header mode is enabled by default : Authorization: Bearer {token}
Apache does not support the Bearer authorization scheme. Hence, you will have to explicitly enable Authorization. To enable it, update your .htaccess
file as follows:
# .htaccess or /etc/apache2/sites-available/000-default.conf SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
An alternate way is to add the same line as above to the virtual hosts config of apache (/etc/apache2/sites-available/000-default.conf
)
While making Ajax requests, you can configure your Client library to add the JWT to every request. Here are a few examples.
// get token (From Web Storage/Cookie) let token = localStorage.getItem('jwt-token'); // Axios axios.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${token}` return config; }); // jQuery, $.ajaxSetup({ beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', `Bearer ${token}`); } }); // Native javascript XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send; var newSend = function(vData) { this.setRequestHeader('Authorization', 'Bearer ' + token); this.realSend(vData); }; XMLHttpRequest.prototype.send = newSend;
Now each time you make a request to the protected section (viz /api
according to what we defined in the firewall) and pass a valid JWT along with the request, access will be granted. For missing/incorrect/expired JWT, an error will be returned. You could also send the JWT token as a query parameter.
B. Send JWT as a query parameter
Sending JWT as a query parameter is disabled by default. To enable it, update the lexik_jwt_authentication configuration at config/packages/lexik_jwt_authentication.yaml
# config/packages/lexik_jwt_authentication.yaml lexik_jwt_authentication: public_key: '...' token_ttl: 10800 # ... token_extractors: query_parameter: enabled: true name: bearer
Now you can pass your token as a query parameter to the guarded route (/api
as defined in the firewall) with the name “bearer” and the value being that of the token. For a valid JWT, access will be granted, otherwise an error will be returned.
8. Logout User
To logout a user, it is sufficient to delete the token from the Web Storage/Cookie. No other manipulation is necessary.
That’s it! You have now setup JWT Authentication with Symfony successfully. Example of a Website using JWT Auth – https://gospelmusic.io
So far, We discussed how to obtain a JWT, to validate/use it and to delete it. When you further develop your App, the following scenarios might be helpful…
Configuring a Route as both Secure and Unsecure
Sometimes, you might wanna allow a certain route to be accessible even without authentication. To achieve that, we write our own JWTAuthenticator class as follows:
// src/Security/JWTAuthenticator.php namespace App\Security; use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface; use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; // use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; // For Symfony 4.4 and above final class JWTAuthenticator extends JWTTokenAuthenticator { private $firewallMap; public function __construct( JWTTokenManagerInterface $jwtManager, EventDispatcherInterface $dispatcher, TokenExtractorInterface $tokenExtractor, // TokenStorage $tokenStorage, // For Symfony 4.4 and above FirewallMap $firewallMap ) { parent::__construct($jwtManager, $dispatcher, $tokenExtractor); // For Symfony 4.4 and above, use the next line instead of the above one // parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $tokenStorage); $this->firewallMap = $firewallMap; } /* For Symfony 3.x and below */ public function getCredentials(Request $request) { try { return parent::getCredentials($request); } catch (AuthenticationException $e) { $firewall = $this->firewallMap->getFirewallConfig($request); // if anonymous is allowed, do not throw error if ($firewall->allowsAnonymous()) { return; } throw $e; } } /* For Symfony 4.x and above */ public function supports(Request $request) { try { return parent::supports($request) && parent::getCredentials($request); } catch (AuthenticationException $e) { $firewall = $this->firewallMap->getFirewallConfig($request); // if anonymous is allowed, skip authenticator if ($firewall->allowsAnonymous()) { return false; } throw $e; } } }
Register this class as a service by adding the following to your services.yaml
file-
app.jwt_authenticator: #autowire: false # uncomment if you had autowire enabled. autoconfigure: false public: false parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator class: App\Security\JWTAuthenticator arguments: ['@security.firewall.map']
And then update the firewall in security.yaml
to use the newly registered service
api: pattern: ^/api stateless: true guard: authenticators: - app.jwt_authenticator
That’s it. Now even if you provide an invalid token, the request will not fail since you allow anonymous access too!
Creating a Rest API
Here’s a great resource that describes on how you could create a REST API using Symfony 4, LexikJWTAuthBundle and NelmioCorsBundle.
Hope that helps 🙂 !
Why, not commit?
You should not commit to GIT because anyone who has access to your repository will also be able to access your public and private keys that are used to generate your secure JWT’s.
What is the point of `pattern: /api/login`, why it’s different than `check_path` and not just `/api/login_check`?
The
/api/login
path is actually authenticated anonymously and is used to only display a user login form. When you submit the form, its target is the path/api/login_check
which is used to verify your login and return a JWT if the login was successful. The route “check_path” is similar to/api/login_check
excepting that it is specifically used for json_login (i.e. when you want the form data is submitted as json-encoded data)