Skip to content
Logo Theodo

The Guide I Wish I Had to Implement a Jwt Auth in Symfony

Loïc Chau8 min read

Symfony and JWT logos

Symfony is a great framework with very exhaustive documentation; too exhaustive sometimes. As I started my project I got lost too many times in the documentation… This is the article I wish I had to guide me through the implementation of authentication with JWTs and refresh tokens.

It is a step-by-step guide with explanations to guide you through the implementation of a perfect JWT authentication.

For the explanation to be as precise as possible I’ll provide you with links to an example repo that implements a JWT authentication in Symfony following the instructions described in this article. You can find the final implementation in this repository.

Packages that will be used

Outline

  1. Setting up the JWT authentication
  2. Setting up the refresh tokens
  3. Next steps

Recap on what JWT authentication is

A JWT authentication grants its users authorizations based on an access token in the JSON Web Token (JWT) format. This access token contains all information necessary for authorization in opposition to sessions which typically only store the user’s id. JWTs are most often paired with refresh tokens for security and UX purposes.

Setting up the JWT authentication

We will install the lexik/jwt-authentication-bundle bundle as per the instructions of the README.

Install the lib with composer

composer require lexik/jwt-authentication-bundle

Thanks to Symfony Flex, most files will be created for you when you run the composer command.

Generate the private and public keys

php bin/console lexik:jwt:generate-keypair

Configure the bundle

lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 300 # 5min

when@dev:
    lexik_jwt_authentication:
        token_ttl: 43200 # 12h

The secret_key, public_key and pass_phrase config has already been generated by the Symfony Flex recipe. Do not forget to change the pass_phrase in production !

By default the time to live (TTL) of the access token is of one hour which is very long. Since we will be using refresh tokens you should lower the TTL to 5 minutes to be secure.

Update security.yaml

We set up:

# config/packages/security.yaml
security:
    enable_authenticator_manager: true

    # ...

    # This provider part should have been generated when installing the Security bundle
    providers:
        app_user_provider:
            entity:
                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: ~

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

Note on the firewalls

The api firewall declaration does not interfere with /api/login because it is declared after the login firewall.

Note that we have defined all our firewalls to be stateless. This means that we do not want to use the default session authentication provided by Symfony since it would be redundant with the JWTs.

Note on the access control roles

Unfortunately, there is no list with all roles that you can use in the access_control roles property. They are individually referenced under corresponding use cases in the Access control section of the documentation.

Finally, declare the login route in the routes.yaml config file

# config/routes.yaml
# Add the following config to your file :
api_login_check:
    path: /api/login_check

Use your access token

  1. Make a POST request on /api/login_check with one of your user’s credentials in the body {email: xxx, password: xxx} to retrieve an access token
  2. Set the value Bearer MY_JSON_WEB_TOKEN in the Authorization Header of your HTTP calls to access authenticated routes

Conclusion of the JWT setup

You now have a login route on /api/login_check that returns a JWT token providing users access to authenticated parts of your app!

Setting up the refresh tokens

We now have very secure access tokens with a short time to live. The issue is that it would become very quickly annoying for users to have to relogin every 5 minutes to get a new access token… Refresh tokens are longer lived and will allow the user to get a new access token without going through login, thus balancing security and user experience.

To add this feature to our Symfony application we will install and set up the gesdinet/jwt-refresh-token-bundle.

Install the bundle

composer require gesdinet/jwt-refresh-token-bundle

Press y when prompted if you want to execute the associated recipe, this will automatically setup most of the bundle.

Update security.yaml

security:
		# ...
    firewalls:
        # ...
        refresh_token:
            pattern: ^/api/token/refresh
            stateless: true
            refresh_jwt:
                # The corresponding route has been declared by the recipe
                check_path: /api/token/refresh

        # Make sure the refresh token firewall is above the api firewall !!
        # Else the requests will match the api firewall first
        api:
            # ...

    # ...
    access_control:
        # ...
        # enable public access to token refresh route for users logged
        # out because their token expired
        - { path: ^/api/(login|token/refresh), roles: PUBLIC_ACCESS } 
        # ...
# ...

Configure the refresh token bundle

gesdinet_jwt_refresh_token:
    refresh_token_class: App\Entity\RefreshToken # Scaffolded by the bundle recipe
    ttl: 7200 # 2h in seconds
    single_use: true

    # Use cookies for the refresh token
    cookie:
        enabled: true
        remove_token_from_body: true
        # Cookie parameters
        http_only: true
        same_site: strict
        secure: true
        path: /api/token
        domain: null

The five cookie attributes we just defined are an exhaustive list of the cookie options exposed by the bundle.

Conclusion of the Refresh token setup

Upon login, a refresh token cookie will now be sent to your browser. It is up to you to send a request to /api/token/refresh with the refresh token cookie to get a new access token. This route returns the exact same payload as the login one.

Next steps

Congratulations! You now have a JWT and refresh token authentication in your Symfony app 🥳

There are two next steps you should follow to leverage the full potential of JWTs :

Liked this article?