Log In To A Symfony API With JWTs (LexikJWTAuthenticationBundle)


In this video we will install and configure LexikJWTAuthenticationBundle to enable JWT (JSON Web Tokens) authentication for our Symfony REST API.

Throughout this video, and the rest of the series you will hear me talking about "jots", which is how you pronounce JWT... apparently! :)

Configuring this bundle is remarkably straightforward, especially when compared to the amount of functionality it provides for so little configuration. Perhaps the biggest hurdle will be the requirement for OpenSSL being installed. If you don't have OpenSSL, on Ubuntu it is as simple as:

sudo apt-get install openssl

All other installation instructions can be copied directly from the installation instructions provided by LexikJWTAuthenticationBundle.

Test Code

Way back when we first started looking at the Behat feature for User, we wrote - but commented out - the following:

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |
    # And I am successfully logged in with username: "peter", and password: "testpass"

(I've removed the extra steps that aren't relevant here.)

Now we want to uncomment the line that logs our User in, otherwise the UserVoter we created in the previous video is going to deny access (correctly) as we aren't logged in:

// src/AppBundle/Security/Authorization/Voter/UserVoter.php

    public function vote(TokenInterface $token, $requestedUser, array $attributes)
    {
        // * snip *

        // get current logged in user
        $loggedInUser = $token->getUser();

        // make sure there is a user object (i.e. that the user is logged in)
        if ($loggedInUser === $requestedUser) {
            return VoterInterface::ACCESS_GRANTED;
        }

        return VoterInterface::ACCESS_DENIED;
    }

To clarify - if we aren't logged in, $token->getUser(); will either return a TokenInterface if we are authenticated, or null if not (docs for reference). Our test will see if null is equal to the User we are requesting - which will be false, so we will skip over to the return VoterInterface::ACCESS_DENIED; statement.

Hence, our test of:

  Scenario: User can GET their personal data by their unique ID
    When I send a "GET" request to "/users/u1"
    Then the response code should 200
     And the response header "Content-Type" should be equal to "application/json; charset=utf-8"
     And the response should contain json:
      """
      {
        "id": "u1",
        "email": "peter@test.com",
        "username": "peter",
        "accounts": [
          {
            "id": "a1",
            "name": "account1"
          }
        ]
      }
      """

Will switch from passing to failing as soon as we implement login. To fix this, we must implement / uncomment the log in step in our Background section.

Fortunately, the code is already written, but let's take a look at it so we aren't flying blind:

// src/AppBundle/Features/Context/RestApiContext.php

    /**
     * Adds JWT Token to Authentication header for next request
     *
     * @param string $username
     * @param string $password
     *
     * @Given /^I am successfully logged in with username: "([^"]*)", and password: "([^"]*)"$/
     */
    public function iAmSuccessfullyLoggedInWithUsernameAndPassword($username, $password)
    {
        $response = $this->client->post('login', [
            'json' => [
                'username' => $username,
                'password' => $password,
            ]
        ]);

        \PHPUnit_Framework_Assert::assertEquals(200, $response->getStatusCode());

        $responseBody = json_decode($response->getBody(), true);
        $this->addHeader('Authorization', 'Bearer ' . $responseBody['token']);
    }

Firstly we create a POST request to /login sending in our username and password data as JSON:

{"username":"peter", "password":"testpass"}

Because we configured FOSUserBundle in such a way that we can use either the email address or username (if different), both are valid as the username. Be sure to check out this video if unsure how to do this for your project.

Perhaps the one strange part of sending JSON with Guzzle is that we need to pass in the data inside a json 'option':

    [
      'json' => [
        'username' => $username,
        'password' => $password,
      ]
    ]

If unsure, be sure to check the Guzzle docs.

Whatever the outcome of this POST request is, will be stored on the $response variable.

We can then use the standard \PHPUnit_Framework_Assert asserttions to check that the response code was a 200 - OK. If this fails, the process will end here. If it doesn't end here, we can assume things went well.

Knowing this, we can decode the JSON response - passing in true to json_decode means we can make PHP convert the JSON into a PHP compatible array. This is immediately handy, as on the next line we grab the token from that array and add it as a security Authorization header, with the value of Bearer {long-string-of-fun-here}.

Because this is a background step, this will occur for every single scenario in our feature.

From Background To Foreground

With our background step ensuring we are logged in as the expected user ('peter'), we can then assume that our test should work:

  Scenario: User can GET their personal data by their unique ID
    When I send a "GET" request to "/users/u1"
    Then the response code should 200
     And so on...

The nice thing about this test is that not only does it cover whether various parts of our system are behaving as expected (Controllers, Handlers, Voters, Queries, etc), it also brings confidence to our project.

We have seen that things break when they should. If we aren't logged in then absolutely we shouldn't have access. We know that log in works because we test it on every single scenario.

The crucial parts of our system are being continually and reliably tested.

Should we make a change at any point in the future, we can say with a high degree of certainty that the system is, or is no longer, behaving as we expect.

As I've said in previous video write ups, developing in this way takes longer. But the outcome is more robust, and if you have any desire to use this system in production, you will thank yourself repeatadly for devoting the time and effort to proving your system behaves properly, and in an automated fashion.

Code For This Course

Get the code for this course.

Episodes