Buy

JWT Guard Authenticator (Part 1)

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:

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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
}

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

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

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

If there is no token, return null:

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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!

63 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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. 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:

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

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

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

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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:

84 lines src/AppBundle/Security/JwtTokenAuthenticator.php
... 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!

Leave a comment!

  • 2016-07-01 Roukmoute

    If you have this error like:

    PHP Fatal error: Class AppBundle\Security\JwtTokenAuthenticator contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface::start) in src/AppBundle/Security/JwtTokenAuthenticator.php on line 16

    It's only because, GuardAuthenticatorInterface extends with another AuthenticationEntryPointInterface, so just implement the missing method called `start` (\Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface::start)

    If you don't care about this, just do a `return;`

    For more informations, reads commentary about this method:

    This is called when an anonymous request accesses a resource that requires authentication, when the user is not authenticated at all.
    It returns generally a new Response.
    It's for helping the user to connect to the website (so redirect to a form or use a header with an API for example)

    Bye!