Buy

Registering the Authenticator (Part 2)

The authenticator class is done - well done enough to see it working. Next, we need to register it as a service. Open up app/config/services.yml to add it: call it jwt_token_authenticator. Set its class to AppBundle\Security\JwtTokenAuthenticator:

39 lines app/config/services.yml
... lines 1 - 5
services:
... lines 7 - 35
jwt_token_authenticator:
class: AppBundle\Security\JwtTokenAuthenticator
autowire: true

And instead of adding an arguments key: here's your permission to be lazy! Set autowire to true to make Symfony guess the arguments for us.

Finally, copy the service name and head into security.yml. Under the firewall, add a guard key, add authenticators below that and paste the service name:

32 lines app/config/security.yml
security:
... lines 2 - 8
firewalls:
main:
... lines 11 - 20
guard:
authenticators:
- 'jwt_token_authenticator'
... lines 24 - 32

As soon as you do that, Symfony will call getCredentials() on the authenticator on every request. If we send a request that has an Authorization header, it should work its magic.

Let's try it! Run our original testPOSTProgrammerWorks() test: this is sending a valid JSON web token.

./vendor/bin/phpunit --filter testPOSTProgrammerWorks

And this time... it passes!

Hold on, that's pretty amazing! The authenticator automatically decodes the token and authenticates the user. By the time ProgrammerController is executed, our user is logged in. In fact, there's one other spot we can finally fix.

Down on line 37, we originally had to make it look like every programmer was being created by weaverryan:

193 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 18
class ProgrammerController extends BaseController
{
... lines 21 - 24
public function newAction(Request $request)
{
... lines 27 - 36
$programmer->setUser($this->findUserByUsername('weaverryan'));
... lines 38 - 50
}
... lines 52 - 191
}

Without authentication, we didn't know who was actually making the API requests, and since every Programmer needs an owner, this hack was born.

Replace this with $this->getUser():

193 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 18
class ProgrammerController extends BaseController
{
... lines 21 - 24
public function newAction(Request $request)
{
... lines 27 - 36
$programmer->setUser($this->getUser());
... lines 38 - 50
}
... lines 52 - 191
}

That's it.

Our controller doesn't know or care how we were authenticated: it just cares that $this->getUser() returns the correct user object.

Run the test again.

./vendor/bin/phpunit --filter testPOSTProgrammerWorks

It still passes! Welcome to our beautiful JWT authentication system. Now, time to lock down every endpoint: I don't want other users messing with my code battlers.

Leave a comment!

  • 2017-11-07 weaverryan

    Hey einue!

    Sorry for the late reply - for some reason Disqus put your comment in Spam :(.

    Check out this issue: https://stackoverflow.com/q... - it is likely your problem!

    Cheers!

  • 2017-11-01 Victor Bocharsky

    Hey einue ,

    I believe you can get access to all those headers via Symfony Request object, i.e:


    dump($request->headers->all());

    I mean, you can avoid apache_request_headers() function call and that extra check with function_exists(). Isn't that data set? I thought it should be parsed with Symfony Request.

    Cheers!

  • 2017-10-27 einue

    It seems like the authorization header is saved in the apache_request_headers.

    If I add this Code


    if (!$request->headers->has('Authorization') && function_exists('apache_request_headers')) {
    $all = apache_request_headers();
    if (isset($all['Authorization'])) {
    $request->headers->set('Authorization', $all['Authorization']);
    }
    }

    at the beginning of getCredentials, it works with the authorization key. But that's in fact not a really nice solution... Now i implement an eventListener to fix that.

  • 2017-10-27 einue

    Hi Victor Bocharsky , thanks for the fast repsonse. I followed the video tutorial. I also changed the status code in the newAction. Then i get the error: Failed asserting that 200 matches expected 201.

    If I change the 'Authorization' key in getCredentials to 'Test' and also in the testPOSTProgrammerWorks-Method. It works fine.
    It seems like the Authorization-Header doesn't work. I use a vagrant box with apache.

  • 2017-10-27 Victor Bocharsky

    Hey einue ,

    The code in start/ and finish/ is different for this course. Also, status code should be 201 (not 200) as we have in "symfony-rest4/finish/src/AppBundle/Controller/ApiProgrammerController" file -> "newAction()" method -> and see 201 status code on line 45: "$response = $this->createApiResponse($programmer, 201);".

    Do you watch videos or are you following this tutorial by course scripts and code blocks only? Because, right in this video I see we return 201 in newAction().

    I'd be glad to help you to figure out this problem.

    Cheers!

  • 2017-10-27 einue

    Hy, can you update the finish folder in the download project?
    I think the TokenControllerTest is missing and the ProgrammerControllerTest is not up to date.

    I followed your tutorial and I still get the 200 instead of the 201 status code for testPOSTProgrammerWorks.
    I put an echo json_encode($request); in the getCredentials Function. And by running the test, i got an empty request:
    {"attributes":{},"request":{},"query":{},"server":{},"files":{},"cookies":{},"headers":{}}

    I don't know, whats going wrong?

  • 2017-10-17 Diego Aguiar

    Hey @Greg!

    haha, it's ok to ask, someone else might find it useful :)
    and yes, if you have two different ways of accessing to your app (web, API), it's recommended to configure two firewalls

    Cheers!

  • 2017-10-17 Greg

    Hi,

    It's me again ;)

    I have a little question about guard if I want to use guard for the api and for the web.

    I suppose I need 2 classes one for the web and one for the API but in security.yml, do I just declare the 2 services under the authenticators key ?

    Thanks again for all.

    Edit: One more time I really should to watch all the videos before I open my mouth ;)
    2 firewalls are the solution.
    isn't it?

    Cheers!

  • 2017-08-14 weaverryan

    Yes! Code would be very helpful :). In general, the 401 happens when you try to access a secured endpoint, and there is NO authentication information on the request. Have you added code to your start() method yet? If so, if you modify the response in that method (e.g. change it to a 402), does that change your response to a 402 or is it still a 401? Typically, the start() method is responsible for returning the 401, but I want to be sure.

    To debug, I would look hard at your getCredentials() method. My guess is that this method is returning null, even when there is a JWT in the request. If I'm correct, then because you are returning null, the authenticator is being skipped, and so you ultimately receive the 401.

    Cheers!

  • 2017-08-11 Diego Aguiar

    Hey Rajesh Patel

    Could you show us your code? So we can help you debugging

    Cheers!

  • 2017-08-10 Rajesh Patel

    i have done all the code according to tutorial but its always return 401 Unauthorized.I am sending authorization header with JWT tokens but its not working can anyone help me please?

  • 2017-07-28 Diego Aguiar

    Hey Ayham Hasan!

    In that case you could inject the "TokenStorage" into your Subscriber class (By using dependency injection), and then, retrieve the User from it. Actually you would be "almost" duplicating all the logic that Symfony's BaseController does

    This is the right namespace of the TokenStorage:
    Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;


    // in somewhere from your subscriber class

    $user = $this->tokenStorage->getToken();

    if (!is_object($user = $token->getUser())) {
    // user is not logged in
    }

    Cheers!

  • 2017-07-28 Ayham Hasan

    Hi, I have one question, I would like to call $this->getUser() not in Controller like in LocaleSubscriber class, how could I do that??

  • 2016-12-06 Victor Bocharsky

    Hey Zuhayer,

    When you have more than one service which extends the same class - you need to stop using "autowire: true" and set your dependencies manually. It's a normal behavior - system just can't determine by itself what service to inject. So you have to take this work on yourself. It's rare, but sometimes it happens like in your example with SonataAdminBundle.

    Cheers!

  • 2016-12-06 Zuhayer Tahir

    When I configure autowire i get an error

    jwt_token_authenticator:
    class: AppBundle\Security\JwtTokenAuthenticator
    autowire: true
    Unable to autowire argument of type "Doctrine\ORM\EntityManager" for the service "jwt_token_authenticator". Multiple services exist for this class (doctrine.orm.default_entity_manager, sonata.admin.entity_manager).

    How to resolve this error, or is this normal behavior when using SonataAdminBundle?
    ------------
    Currently I am manually configuring and it works:

    arguments: ['@lexik_jwt_authentication.encoder', '@doctrine.orm.entity_manager', '@api.response_factory']

  • 2016-09-07 Victor Bocharsky

    Hey Rakib,

    It's weird. Could you please show your current content of security.yml file? At least security.firewalls section. You could show it in comment here or better use GitHub Gist for that.

    Cheers!

  • 2016-09-06 Rakib Ahmed Shovon

    [Symfony\Component\Config\Definition\Exception\InvalidConfigurationException]
    Unrecognized option "guard" under "security.firewalls.main"
    why Unrecognized ? cant move forward :-(
    did as you said . help pls