Buy Access to Course
07.

JWT Guard Authenticator (Part 1)

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

To create our token authentication system, we'll use Guard.

Guard is part of Symfony's core security system and makes setting up custom auth so easy it's actually fun.

Creating the Authenticator

In AppBundle, create a new Security directory. Inside add a new class: JwtTokenAuthenticator:

// ... lines 1 - 2
namespace AppBundle\Security;
// ... lines 4 - 11
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 16 - 61
}

Every authenticator starts the same way: extend AbstractGuardAuthenticator. Now, all we need to do is fill in the logic for some abstract methods. To get us started quickly, go to the "Code"->"Generate" menu - command+N on a Mac - and select "Implement Methods". Select the ones under Guard:

// ... lines 1 - 7
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
// ... lines 12 - 13
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
public function getCredentials(Request $request)
{
// ... lines 18 - 30
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// TODO: Implement getUser() method.
}
public function checkCredentials($credentials, UserInterface $user)
{
// TODO: Implement checkCredentials() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// TODO: Implement onAuthenticationFailure() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function supportsRememberMe()
{
// TODO: Implement supportsRememberMe() method.
}
// ... lines 57 - 61
}

Tip

Version 2 of LexikJWTAuthenticationBundle comes with an authenticator that's based off of the one we're about to build. Feel free to use it instead of building your own... once you learn how it works.

Now, do that one more time and also select the start() method. That'll put start() on the bottom, which will be more natural:

// ... lines 1 - 5
use Symfony\Component\HttpFoundation\Request;
// ... lines 7 - 8
use Symfony\Component\Security\Core\Exception\AuthenticationException;
// ... lines 10 - 13
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 16 - 57
public function start(Request $request, AuthenticationException $authException = null)
{
// TODO: Implement start() method.
}
}

If this is your first Guard authenticator... welcome to party! The process is easy: we'll walk through each method and just fill in the logic. But if you want to know more - check out the Symfony security course.

getCredentials()

First: getCredentials(). Our job is to read the Authorization header and return the token - if any - that's being passed. To help with this, we can use an object from the JWT bundle we installed earlier: $extractor = new AuthorizationHeaderTokenExtractor(). Pass it Bearer - the prefix we're expecting before the actual token - and Authorization, the header to look on:

// ... lines 1 - 13
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
public function getCredentials(Request $request)
{
$extractor = new AuthorizationHeaderTokenExtractor(
'Bearer',
'Authorization'
);
// ... lines 22 - 30
}
// ... lines 32 - 61
}

Grab the token with $token = $extractor–>extract() and pass it the $request:

// ... lines 1 - 17
$extractor = new AuthorizationHeaderTokenExtractor(
'Bearer',
'Authorization'
);
$token = $extractor->extract($request);
// ... lines 24 - 63

If there is no token, return null:

// ... lines 1 - 17
$extractor = new AuthorizationHeaderTokenExtractor(
'Bearer',
'Authorization'
);
$token = $extractor->extract($request);
if (!$token) {
return;
}
// ... lines 28 - 63

This will cause authentication to stop. Not fail, just stop trying to authenticate the user via this method.

If there is a token, return it!

// ... lines 1 - 17
$extractor = new AuthorizationHeaderTokenExtractor(
'Bearer',
'Authorization'
);
$token = $extractor->extract($request);
if (!$token) {
return;
}
return $token;
// ... lines 30 - 63

getUser()

Next, Symfony will call getUser() and pass this token string as the $credentials argument. Our job here is to use that token to find the user it relates to.

And this is where JSON web tokens really shine. Because if we simply decode the token, it will contain the username. Then, we can just look it up in the database.

To do this, we'll need two services. On top of the class, add a __construct() method so we can inject these. First, we need the lexik encoder service. Go back to your terminal and run:

./bin/console debug:container lexik

Select the lexik_jwt_authentication.encoder service. Ah, this is just an alias for the first service - lexik_jwt_authentication.jwt_encoder. And this is an instance of JWTEncoder. Back in the authenticator, use this as the type-hint. Or wait, since it looks like there's an interface this probably implements, you can use JWTEncoderInterface instead. Give this one more argument: EntityManager $em:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 19 - 21
public function __construct(JWTEncoderInterface $jwtEncoder, EntityManager $em)
{
// ... lines 24 - 25
}
// ... lines 27 - 82
}

I'll use a shortcut - option+enter on a Mac - to initialize these fields:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
private $jwtEncoder;
private $em;
public function __construct(JWTEncoderInterface $jwtEncoder, EntityManager $em)
{
$this->jwtEncoder = $jwtEncoder;
$this->em = $em;
}
// ... lines 27 - 82
}

This created the two properties and set them for me. Nice!

Head back down to getUser(). First: decode the token. To do that, $data = $this–>jwtEncoder->decode() and pass it $credentials - that's our token string:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 19 - 43
public function getUser($credentials, UserProviderInterface $userProvider)
{
$data = $this->jwtEncoder->decode($credentials);
// ... lines 47 - 56
}
// ... lines 58 - 82
}

That's it! $data is now an array of whatever information we originally put into the token. Fundamentally, this works just like a normal json_decode, except that the library is also checking to make sure that the contents of our token weren't changed. It does this by using our private key.

Tip

The private key is only used in the case of symmetric signing. In the much more common case of asymmetric signing, the private key is used to sign the token, and the public key is used to verify the signature.

This guarantees that nobody has changed the username to some other username because they're a jerk. Encryption is amazing.

It also checks the token's expiration: our tokens last 1 hour because that's what we setup in config.yml:

77 lines | app/config/config.yml
// ... lines 1 - 72
lexik_jwt_authentication:
// ... lines 74 - 76
token_ttl: 3600

So, if ($data === false), then we know that there's a problem with the token. If there is, throw a new CustomUserMessageAuthenticationException() with Invalid token:

// ... lines 1 - 45
$data = $this->jwtEncoder->decode($credentials);
if ($data === false) {
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
// ... lines 51 - 84

Tip

In version 2 of the bundle, you should instead use a try-catch around this line:

use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
// ...

public function getUser($credentials, UserProviderInterface $userProvider)
{
    try {
        $data = $this->jwtEncoder->decode($credentials);
    } catch (JWTDecodeFailureException $e) {
        // if you want to, use can use $e->getReason() to find out which of the 3 possible things went wrong
        // and tweak the message accordingly
        // https://github.com/lexik/LexikJWTAuthenticationBundle/blob/05e15967f4dab94c8a75b275692d928a2fbf6d18/Exception/JWTDecodeFailureException.php

        throw new CustomUserMessageAuthenticationException('Invalid Token');
    }

    // ...
}

We'll talk about what that does in a second.

But if everything is good, get the username with $username = $data['username']:

// ... lines 1 - 45
$data = $this->jwtEncoder->decode($credentials);
if ($data === false) {
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
$username = $data['username'];
// ... lines 53 - 84

Then, query for and return the user with return $this–>em–>getRepository('AppBundle:User')–>findOneBy() passing username set to $username:

// ... lines 1 - 45
$data = $this->jwtEncoder->decode($credentials);
if ($data === false) {
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
$username = $data['username'];
return $this->em
->getRepository('AppBundle:User')
->findOneBy(['username' => $username]);
// ... lines 57 - 84

checkCredentials()

If the user is not found, this will return null and authentication will fail. But if a user is found, then Symfony finally calls checkCredentials(). Just return true:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 19 - 58
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
// ... lines 63 - 82
}

There's no password or anything else we need to check at this point.

And that's it for the important stuff!

Skip Everything Else (for now)

Skip onAuthenticationFailure() for now. And for onAuthenticationSuccess(), purposefully do nothing:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 19 - 63
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// do nothing - let the controller be called
}
// ... lines 73 - 82
}

We want the authenticated request to continue to the controller so we can do our normal work.

In supportsRememberMe() - this doesn't apply to us - so return false:

// ... lines 1 - 16
class JwtTokenAuthenticator extends AbstractGuardAuthenticator
{
// ... lines 19 - 73
public function supportsRememberMe()
{
return false;
}
// ... lines 78 - 82
}

And keep start() blank for another minute. With just getCredentials() and getUser() filled in, our authenticator is ready to go. Let's hook it up!