JWT Guard Authenticator (Part 1)
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTo 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
:
// ... 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!
Hey, If we want to perform traditional validation of username( character count and valid email) and password (character count) before authentication is performed.
The users are in database and we don't want to hit the database before this validation gives a go. How the solution gonna be !