Buy
Buy

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

Login Subscribe

When the request sends us a valid API token, our authenticator code is working! At least all the way to checkCredentials(). But before we finish that, I want to see what happens if a client sends us a bad key. So let's see... the last number in the token is six. Let's add a space: that will be enough to mess things up.

Hit send again. Woh! It redirects us to /login? I did not see that coming.

Sometimes the hardest part of security is figuring out what's happening when something unexpected occurs. So, let's figure out exactly what's going on here.

When authentication fails, this onAuthenticationFailure() method is called:

... lines 1 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 15 - 54
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// todo
}
... lines 59 - 73
}

Our job is to return a Response that should be sent back to the client. Right now... we're doing nothing.! So, instead of sending an error back to the user, the request continues like normal to the controller. But, the request is still anonymous. So when it hits our security check in AccountController, Symfony activates the "entry point", which redirects the user to /login.

onAuthenticationError()

But... that's not what we want at all! If an API client sends a bad API token, we need to tell them! Bad API client! Let's return a new JsonResponse() with a message key that describes what went wrong. Earlier, I mentioned that whenever authentication fails - for any reason - it's because, internally, some sort of AuthenticationException is thrown. That's important because this exception is passed to us as an argument:

... lines 1 - 12
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 15 - 54
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
... line 57
}
... lines 59 - 73
}

And it has a method - getMessageKey() - that holds a message about what went wrong. Set the status code to 401:

... lines 1 - 5
use Symfony\Component\HttpFoundation\JsonResponse;
... lines 7 - 13
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 16 - 55
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new JsonResponse([
'message' => $exception->getMessageKey()
], 401);
}
... lines 62 - 76
}

Custom Error Messages with CustomUserMessageAuthenticationException

Let's try it again! Send the request. Yes! A 401 Unauthorized response. But, oh. That message isn't right at all!

Username could not be found?

This is because Symfony creates a different error message based on where authentication fails inside your authenticator. If you fail to return a User from getUser(), you get this "Username could not be found" error.

For our login form, we render this exact messageKey field in the template. But we also pass it through the translator:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 34
</form>
{% endblock %}

That allowed us to translate that into a better message:

"Username could not be found.": "Oh no! It doesn't look like that email exists!"

We could do the same here: inject the translator service into ApiTokenAuthenticator and translate the message key. But... hmmm, the message still wouldn't be right - it would use the "It doesn't look like that email exists!" message from the translation file.

No problem: there is a second way to control error messages in an authenticator, and it's super flexible. At any point in your authenticator, you can throw a new CustomUserMessageAuthenticationException() that will cause authentication to fail and accepts any custom error message you want, like, "Invalid API Token":

... lines 1 - 9
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
... lines 11 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 38
public function getUser($credentials, UserProviderInterface $userProvider)
{
... lines 41 - 44
if (!$token) {
throw new CustomUserMessageAuthenticationException(
'Invalid API Token'
);
}
... lines 50 - 51
}
... lines 53 - 79
}

That's it! This exception will be passed to onAuthenticationFailure() and its getMessageKey() method will return that message.

Go back to Postman to try it: send! We got it! So much better!

Checking Token Expiration

Oh, while we're talking about tokens failing, we should definitely check to make sure the token hasn't expired. Inside ApiToken, we created this nice expiresAt property:

... lines 1 - 9
class ApiToken
{
... lines 12 - 23
/**
* @ORM\Column(type="datetime")
*/
private $expiresAt;
... lines 28 - 60
}

Go down to the bottom of the class and add a new helper function: isExpired() that returns a bool. Return $this->getExpiresAt() is less than or equal to new \DateTime():

... lines 1 - 9
class ApiToken
{
... lines 12 - 61
public function isExpired(): bool
{
return $this->getExpiresAt() <= new \DateTime();
}
}

Nice! Back in ApiTokenAuthenticator, in getUser(), if $token->isExpired(), then throw new CustomUserMessageAuthenticationException() with Token Expired:

... lines 1 - 9
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
... lines 11 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 38
public function getUser($credentials, UserProviderInterface $userProvider)
{
... lines 41 - 44
if (!$token) {
throw new CustomUserMessageAuthenticationException(
'Invalid API Token'
);
}
if ($token->isExpired()) {
throw new CustomUserMessageAuthenticationException(
'Token expired'
);
}
... lines 56 - 57
}
... lines 59 - 85
}

We're killin' it! Oh, but, why are we putting this code here and not in checkCredentials()? Answer: no reason! These two methods are called one after the other and you can really put any code inside either of these methods. Actually, I chose getUser() just because we have access to the $token object there.

Head back to Postman. Let's remove that extra space so our API token is valid once again. Send! Success! Now, go back to the ApiToken class and, temporarily, return true from isExpired() so we can see the error:

class ApiToken
{
    // ...

    public function isExpired(): bool
    {
        return true;
        return $this->getExpiresAt() <= new \DateTime();
    }
}

And... send it again! Got it! Token Expired. Remove that dummy code.

onAuthenticationSuccess()

At this point... we're basically done! In checkCredentials(), there is no password to check. And so, it's perfectly ok for us to return true:

... lines 1 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 59
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
... lines 64 - 85
}

Finally, in onAuthenticationSuccess(), hmm. What should we do when authentication is successful? With a login form, we redirect the user after success. But with an API token system we, well, want to do... nothing! Yep! We want to allow the request to continue so that it can hit the controller and return the JSON response:

... lines 1 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 71
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// allow the authentication to continue
}
... lines 76 - 85
}

start() & supportsRememberMe()

So what about start()? Because we chose LoginFormAuthenticator as the entry_point, this will never be called. To prove it, I'll throw an exception that says:

Not used: entry_point from other authenticator is used:

... lines 1 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 76
public function start(Request $request, AuthenticationException $authException = null)
{
throw new \Exception('Not used: entry_point from other authentication is used');
}
... lines 81 - 85
}

And, finally, supportsRememberMe(). Return false:

... lines 1 - 14
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
... lines 17 - 81
public function supportsRememberMe()
{
return false;
}
}

If you return true from this method, it just means that the "remember me" system is activated and looking for that _remember_me checkbox to be checked. Because that makes absolutely no sense for an API, just turn it off.

That's it! Find a stranger to high-five! Cheers your coffee with a co-worker! And find Postman! Brace yourself... send! Yes! It executes our controller and we are definitely authenticated because we see the info for spacebar9@example.com.

People - we now have two valid ways to authenticate in our system! The super cool thing is that, inside of our controller, we don't care which method is used! We just say $this->getUser()... never caring whether the user was authenticated via the login form or with an API token.

Next: let's set up a registration form and learn how we can manually authenticate the user after success.

Leave a comment!

  • 2019-01-28 Diego Aguiar

    Hey Amy

    I don't know if it is a good or bad practice but if you want to refresh the token life, probably after a succesful API call is the best moment. You would only need to update the "expireAt" field of such token.

    Cheers!

  • 2019-01-25 Amy

    I was interrupted while working on the tutorial and came back after an hour, which meant all my tokens had expired. No biggie, just loaded the fixtures again. BUT, I got to wondering, how easy would it be to call the renewExpiresAt function whenever the user's session is modified? For example, every time they load a page? (I would also probably renew it after every successful API call and shorten the life of the token)

  • 2018-11-07 Jorge Sapena

    You are right Diego Aguiar , in fact, I started to do my tests with the Symfony's client, but later I adjusted those test using the Guzzle client. Now I have too much tests with this client so I don't want to spend more time change it. But it's good to know it for the future, thanks!

    Cheers!

  • 2018-11-06 Diego Aguiar

    BTW, you can use Symfony's client to make your http requests instead of Guzzle. I'm recommending it because it hits the test environment by default without having to create a special front controller (index_text.php as Ryan suggested)

    Docs: https://symfony.com/doc/cur...

    Cheers!

  • 2018-11-06 Jorge Sapena

    Hi weaverryan !

    Thanks for your answer. Makes sense, I did all the suggested steps, but as you said, I'm using Guzzle and I was able to verify that the repositories where using my dev database instead of the tests one. I'll try that solution.

    Thank you very much in advance.

    Cheers!

  • 2018-11-06 weaverryan

    Hey Jorge Sapena!

    Yea, as Diego mentioned, testing got a little strange in Symfony 4. It will be fixed in Symfony 4.2, with a small new feature and a change to the recipe. There are two issues:

    1) The .env file is not read in the test environment. And so, all the environment variables need to be duplicated in phpunit.xml.dist. Diego's suggestion above to override the doctrine.dbal.url config setting via that special doctrine.yaml file in the test environment takes care of this part (though we will have a more robust solution soon).

    2) Even after you've fixed the above, when you're running your tests, you need to make sure that you're actually in the test environment! That should be handled thanks to the APP_ENV environment variable in your phpunit.xml.dist file. However, if you are making real web requests from your test (e.g. via Guzzle), then those requests will execute your public/index.php file, which, just like when you load it in a browser, will load in the "dev" environment. That's the second piece that needs to be made more smooth. One easy way to fix this is to create an index_test.php file that is a duplicate of index.php, except that it defaults to the "test" environment.

    Let me know if this helps! I think part (2) might be your issue - but I could also be totally wrong ;).

    Cheers!

  • 2018-11-06 weaverryan

    Hey Shaun!

    Hmm. Well, I suppose like everything, it depends :). In this app, Symfony *is* our front-end and our back-end - some pages are HTML pages (a frontend) and some pages are API endpoints (a back-end). For the front-end part, Symfony *does* need to know where to redirect the user to when authentication fails. But, when Symfony is being treated like an API, then if authentication fails, it should simply return a 401 JSON response back to the API client. It would then be up to the frontend (e.g. JavaScript) to figure out what to do.

    That's a long way of saying: if you are really building a frontend in JavaScript, then it should be capable of understanding the 401 JSON response sent back from Symfony and then it should redirect the user (or open some login dialog) itself.

    Let me know if that answers your question!

    Cheers!

  • 2018-11-06 Jorge Sapena

    I'm using Php Unit Bridge:

    https://symfony.com/doc/cur...

    That's the way to run it. It shouldn't be the problem.

  • 2018-11-05 Shaun

    If there is an authentication failure, should the Symfony app be responsible for redirecting the user to the login page? Or should this be done by the front end receiving the authentication failure response?

  • 2018-11-05 Diego Aguiar

    Is there a reason to run "simple-phpunit"? Try running "phpunit" bin


    ./vendor/bin/phpunit
  • 2018-11-02 Jorge Sapena

    I'm running my tests with "./vendor/bin/simple-phpunit". The thing is that the authentication of the endpoints I'm testing, uses a JwtGuardAuthenticator class, that is getting the user using the repository. I've verified that it's querying the dev DB instead of my tests DB.

    I've checked everything but still with the same problem.

  • 2018-10-31 Diego Aguiar

    Hmm, that's odd. How are you testing it? Can you double check that you are actually running on "test" environment?

  • 2018-10-31 Jorge Sapena

    Hi Diego Aguiar , thanks for your answer.

    I already did what you suggested, but it still takes the wrong DB URL, still using dev DB instead of test DB, really weird!

    Cheers!

  • 2018-10-30 Diego Aguiar

    Hey Jorge Sapena

    Yeah, testing in Symfony4 got a bit weirder... but anyways, you only have to tweak your Doctrine config for your test environment, like this:


    // config/packages/test/doctrine.yaml

    doctrine:
    dbal:
    url: '%env(resolve:DATABASE_URL)%_test'

    Cheers!

  • 2018-10-30 Jorge Sapena

    Hi weaverryan, congratulations for the course, it's really useful.

    By the way, I'm using this approach on one app with Symfony 4, everything works as expected. But when I test it with phpunit, on the getUser method of my JwtGuardAuthenticator service, it's quering the "dev" DB instead of my test database.

    I followed the steps described here (https://symfony.com/doc/cur..., and I have a phpunit.xml file with the DATABASE_URL configured. However the EntityManager is connecting to the wrong database.

    Any idea about what could be wrong?

    Thanks!

  • 2018-10-29 weaverryan

    Hey Yahya A. Erturan!

    Sorry for the slow delay! And... congrats! I'm 99% sure that you found a Symfony bug :). And it took me quite some time to track it down. Here is the issue report: https://github.com/symfony/...

    The tl;dr; is this: there is a bug that causes the container cache to be rebuilt too often, when you're using the debug:config command. It doesn't affect anything else in the system. Basically, under the *right* situation, debug:config will break. But, nothing else in the system should be affected.

    I hope this helps! Oh, also, at least when I was testing it, I would get the error the first time I ran debug:config, but not afterwards (basically it would only occur if the command had to be the container). I know you got it working, but I have a feeling that this might come back at some point - and I want you to know what's going on.

    Cheers!

  • 2018-10-25 Diego Aguiar

    Hey Yahya A. Erturan

    That's a known deprecation that will be fix (if it's not fixed yed) soon. Check this thread: https://github.com/symfony/...

    Cheers!

  • 2018-10-25 Yahya A. Erturan

    I am getting following depreciation message. How can we get rid of it? Because it is a little bit annoying shown in all pages (authenticcated ones) :)


    User Deprecated: Doctrine\Common\ClassLoader is deprecated.
    Hide context Hide trace
    [▼
    "exception" => ErrorException {#453 ▶}
    ]
    {▼
    /var/www/symfony/vendor/doctrine/common/lib/Doctrine/Common/ClassLoader.php:7 {▶}
    /var/www/symfony/vendor/symfony/debug/DebugClassLoader.php:143 {▶}
    Symfony\Component\Debug\DebugClassLoader->loadClass() {}
    /var/www/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php:1028 {▶}
    /var/www/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php:266 {▶}
    /var/www/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php:245 {▶}
    /var/www/symfony/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php:305 {▶}
    /var/www/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php:78 {▶}
    /var/www/symfony/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php:183 {▶}
    /var/www/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:283 {▶}
    /var/www/symfony/vendor/doctrine/doctrine-bundle/Repository/ServiceEntityRepository.php:31 {▶}
    /var/www/symfony/src/Repository/UserRepository.php:19 {▼
    › {
    › parent::__construct($registry, User::class);
    › }
    }
  • 2018-10-24 Yahya A. Erturan

    weaverryan

    rm -Rf vendor/*; composer install did the work. Now I can run ./bin/console debug:config doctrine in ease. Regarding my app, it is 100% copied from finish folder of Symfony Security track.

  • 2018-10-23 Marcin B

    Sorry, I should have been more clear. I had this problem with security.request_matcher.***** in my business project, not in this tutorial.
    However, I was curious and I've just executed bin/console debug:config doctrine in the root of this tutorial and here is what I've experienced:
    Step 1: Yes, I did get: "In GuardAuthenticationFactory.php line 100: The guard authentication provider cannot use the "App\Security\LoginFormAuthenticator" entry_point because another entry point is already configured by another provider! Either remove the other provider or move the entry_point configuration as a root key under your firewall (i.e. at the same level as "guard")."
    And yes, I did get problem with security.request_matcher.***** when moved to the "guard" level but it doesnt matter right now.
    Step 2: Opened GuardAuthenticationFactory and put `dd($defaultEntryPointId, $config['entry_point'])` after `if ($defaultEntryPointId) {`, executed debug:config again and got "App\Security\LoginFormAuthenticator" TWICE!!!
    Step 3: Executed debug:config - 1, 2, 3 ... n times and IT WORKS!
    Stop 4: rm -rf var/cache/*
    Step 5: Deleted line with `dd` and executed debug:config --- BLOW!!! Error from Step 1.

    But, but, but. After the steps I had written out above I randomly run debug:config doctrine three times in a row and I did get the config at the third time and the every following one!!!

    Answering your question: when I remove both: cache and vendor, then composer install I do get this error with another entry point but only on the first comman run, then I'm good :)

    Ryan, I will upload my repo to github and send you a link via email on ryan@knpuniversity.com. Give me a moment.

    P.S. I would change the Exception message so it includes the name of this "another entry point". It is more explicit and a developer can see that both entry points are the same ones (like in out case both are App\Security\LoginFormAuthenticator)

  • 2018-10-23 weaverryan

    Hey guys!

    Hmm. I am frustratingly stumped! I've used your composer.lock file so that I have the EXACT dependencies as you have, and have used the EXACT code from the tutorial up to this point. No matter what I do (I've tried making many different mistakes/typos) I cannot get the same error that you're getting. And, as I mentioned before, I can't even see how this is possible! But, if 2 people have gotten the error, then it's definitely looks like it could be legit.

    Two last questions:

    1) If you rm -rf vendor/* and composer install, same result?
    2) Would it be possible to push your entire app to GitHub so I could reproduce the error myself? I thought composer.lock would be enough, but apparently not. I think it may be something somewhat unrelated that is causing the weird issue.

    Cheers!

  • 2018-10-23 Marcin B

    I had the same problem. Right now I can't remember for sure what was the cause but I think my dependencies (security bundle???) were not up to date. Try that.

  • 2018-10-22 Yahya A. Erturan

    Clearing cache rm -rf var/cache/* did not change the status.

    Here is the gist contains 3 files:

    composer.lock, symfony.lock and a custom file named .history - as my own log of the composer installations during the project:

    https://gist.github.com/yah...

    That bad behaviour prevent me to use debug:config for all :(

  • 2018-10-22 weaverryan

    Hey Yahya A. Erturan!

    Hmm, very interesting. Basically, what you're seeing should not happen :). So yes, it seems like a bug... in Symfony or some weird setup issue. Basically, that ".security.request_matcher.zfHj2lW" service is an internal service that's created to support your firewall (https://github.com/symfony/... and https://github.com/symfony/.... It's consumed directly by the internal security.firewall.map service. This is all low-level plumbing that's handled automatically for you by the container.

    So, you have hit a Symfony bug... or something similar indeed :). Here are some things we can do:

    A) Manually clear your cache to see if it fixes things: rm -rf var/cache/*

    B) Post your `composer.lock` file (a good option is to https://gist.github.com/). Basically, we can't reproduce this error. And, if it IS a symfony bug, it is probably only something that happens on a very specific version. If you post your composer.lock file, we can try to reproduce it using that.

    Sorry you hit this! This type of thing is very strange and rare. But, unless I'm looking at the code wrong, I can't think of anything that you're doing in your code that would cause this - this looks like bad Symfony behavior.

    Cheers!

  • 2018-10-22 Diego Aguiar

    It would be something like this:


    ...
    class SomeService
    {
    use TranslatorTrait;

    public function __construct(TranslatorInterface $translator)
    {
    $this->setTranslator($translator);
    }
    ...
    }


    ...
    trait TranslatorTrait
    {
    private $translator;

    protected function translate($message)
    {
    ...
    }

    protected function setTranslator(TranslatorInterface $translator)
    {
    $this->translator = $translator;
    }
    }

    Cheers!

  • 2018-10-22 Yahya A. Erturan

    weaverryan Can you a little bit elaborate the first solution please?

  • 2018-10-22 Yahya A. Erturan

    Dear Ryan,

    This is latest security.yaml of the latest tutorial for Symfony Security in SyymfonyCasts.


    security:
    encoders:
    App\Entity\User:
    algorithm: bcrypt

    role_hierarchy:
    ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE, ROLE_ALLOWED_TO_SWITCH]

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
    entity:
    class: App\Entity\User
    property: email
    # used to reload user from session & other features (e.g. switch_user)

    firewalls:
    dev:
    pattern: ^/(_(profiler|wdt)|css|images|js)/
    security: false
    main:
    anonymous: true

    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator
    - App\Security\ApiTokenAuthenticator

    # redirect anonymous users to the login page
    entry_point: App\Security\LoginFormAuthenticator

    logout:
    path: app_logout

    remember_me:
    secret: '%kernel.secret%'
    lifetime: 2592000 # 30 days in seconds

    switch_user: true

    # activate different ways to authenticate

    # http_basic: true
    # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate

    # form_login: true
    # https://symfony.com/doc/current/security/form_login_setup.html

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
    # but, definitely allow /login to be accessible anonymously
    #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    # require the user to fully login to change password
    #- { path: ^/change-password, roles: IS_AUTHENTICATED_FULLY }
    # if you wanted to force EVERY URL to be protected
    #- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }

    # - { path: ^/admin, roles: ROLE_ADMIN }
    # - { path: ^/profile, roles: ROLE_USER }

    This will give you an error saying that a problem with entry point. It will ask you indent entry_point to guard level. If you indent as requested, it will give you the previous error I wrote about security.firewall.map.

    And this is the yaml I alternately wrote without entry point.


    security:
    encoders:
    App\Entity\User:
    algorithm: bcrypt

    role_hierarchy:
    ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE, ROLE_ALLOWED_TO_SWITCH]

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
    entity:
    class: App\Entity\User
    property: email
    # used to reload user from session & other features (e.g. switch_user)

    firewalls:
    dev:
    pattern: ^/(_(profiler|wdt)|css|images|js)/
    security: false
    api:
    pattern: ^/api/
    guard:
    authenticators:
    - App\Security\ApiTokenAuthenticator
    main:
    anonymous: true
    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator
    # redirects anonymous users to the login page

    logout:
    path: app_logout

    remember_me:
    secret: '%kernel.secret%'
    lifetime: 2592000 # 30 days in seconds

    switch_user: true

    # activate different ways to authenticate

    # http_basic: true
    # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate

    # form_login: true
    # https://symfony.com/doc/current/security/form_login_setup.html

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
    # but, definitely allow /login to be accessible anonymously
    #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    # require the user to fully login to change password
    #- { path: ^/change-password, roles: IS_AUTHENTICATED_FULLY }
    # if you wanted to force EVERY URL to be protected
    #- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }

    # - { path: ^/admin, roles: ROLE_ADMIN }
    # - { path: ^/profile, roles: ROLE_USER }

    Still gives same error.

    Hope this helps.

  • 2018-10-22 weaverryan

    Hey Yahya A. Erturan!

    Ah! So, there are 2 options here:

    1) (the cleaner, but less fancy solution): add the shortcut methods in your trait (e.g. private function translate()</code),>$this->translator property (maybe throw an exception that this is needed if it's not there). This means you still need to do the work of creating a constructor and setting the property... which is actually a good thing - because that's good clean coding.

    2) Use this tricky: https://symfonycasts.com/sc... - but I don't *love* this solution for "required" dependencies: objects that you actually need to make your code work.

    Cheers!

  • 2018-10-22 weaverryan

    Hey Yahya A. Erturan!

    Hmm, yea, it could be. That is, at the very least, a very unhelpful error :). Can you post the exact security.yaml config that you have that gives you the error? You mentioned that the security.yaml you printed on the bottom also causes the error, but I would love to see the original one.

    Thanks!

  • 2018-10-22 Victor Bocharsky

    Hey Ivan,

    Thank you for your help!

    Cheers!

  • 2018-10-22 Victor Bocharsky

    Hey Yahya,

    Good question! You can execute "bin/console debug:autowiring" and find a proper translation service to know what type hint you need to use to get it. So, as you can see if you ran the command, you can type hint with "Symfony\Component\Translation\TranslatorInterface" in your ApiTokenAuthenticator's constructor. So, first of all, translate your message first and then create a custom exception with already translated message. Basically, the same as Ivan suggested to you below :)

    Cheers!

  • 2018-10-22 Yahya A. Erturan

    Thank you Ivan. It seems all about finding the correct interface as gate to service. Maybe one more question. If we create a trait, which we can use in several services, how we do it in a Trait? Because traits has no constructor?

  • 2018-10-22 Yahya A. Erturan

    I might found a bug. In your security.yaml with two authenticator and and one entry point (even if you indent entry_point correctly), if you want to run

    php bin/console debug:config doctrine

    or any other service, you will get below message:


    In CheckExceptionOnInvalidReferenceBehaviorPass.php line 31:

    The service "security.firewall.map" has a dependency on a non-existent service ".security.request_matcher.zfHj2lW".

    Even if you change your security.yaml to:


    api:
    pattern: ^/api/
    guard:
    authenticators:
    - App\Security\ApiTokenAuthenticator
    main:
    anonymous: true
    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator

    You will still get exact same error.

    How to fix this?

  • 2018-10-21 Ivan Puntiy

    > how to access translator in a Service?

    Using translator: https://symfony.com/doc/cur...

    Dependency Injection: https://symfonycasts.com/sc...

    DI for services in Symfony 4: https://symfonycasts.com/sc...

    And putting it all together:

    use Symfony\Component\Translation\TranslatorInterface;

    class SomeService
    {
    private $translator;

    public __construct(TranslatorInterface $translator)
    {
    $this->translator = $translator;
    }

    public someAction()
    {
    $this->translator->trans('some text');
    }
    }

    > How we can make Custom exceptions messages translated

    The simplest way I see, is just translate the text before you pass it to the exception's constructor.

  • 2018-10-20 Yahya A. Erturan

    Excellent tutorial. Thank you. One question (with two leaf :)) - How we can make Custom exceptions messages translated, and in generic how to access translator in a Service?

  • 2018-10-18 Diego Aguiar

    Hey @Marcin

    Sometimes it take us a couple of days to release the code blocks, sorry about that!

  • 2018-10-18 Marcin

    Am I missing code blocks or the videos always come few days earlier?

  • 2018-10-05 Diego Aguiar

    Awesome job Scott Collier, thanks for sharing your solution :)

    I hope you find the job you are looking for. Cheers!

  • 2018-10-05 Scott Collier

    I appreciate your help.

    Your response was very helpful and pointed me in the right direction.

    I only have a self-imposed deadline as this is a personal project for freelancing jobs. I am also looking for a new job (hopefully staying in Europe) . So, brushing up on some things.

    Anyways, here is my solution in case it may help someone else.

    security.yaml


    main:
    anonymous: true
    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator
    - App\Security\ApiTokenAuthenticator

    entry_point: App\Security\ApiTokenAuthenticator

    logout:
    success_handler: App\Service\LogoutSuccessService

    remember_me:
    secret: '%kernel.secret%'
    lifetime: 2592000 # 30 days in seconds

    In ApiTokenAuthenticator


    public function start(Request $request, AuthenticationException $authException = null)
    {
    if('json' === $request->getContentType()) {
    $data = array(
    "@context" => "/contexts/Error",
    "@type" => "Error",
    "hydra:title" => "An error occurred",
    "hydra:description" => "Authentication Required.",
    "hydra:status" => Response::HTTP_UNAUTHORIZED
    );

    return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    } else {
    return new RedirectResponse($this->router->generate('form_login'));
    }
    }

    To be able to log out, API should get a JsonResponse and website should be redirected to login page. To do this, I had to do the following:

    LogoutSuccessService


    class LogoutSuccessService implements LogoutSuccessHandlerInterface
    {

    private $router;

    public function __construct(RouterInterface $router)
    {
    $this->router = $router;
    }

    /**
    * Creates a Response object to send upon a successful logout.
    *
    * @param Request $request
    *
    * @return JsonResponse|RedirectResponse never null
    */
    public function onLogoutSuccess(Request $request)
    {
    if(null === $request->getContentType()) {
    return new RedirectResponse($this->router->generate('form_login'));
    } else {
    $data = array(
    "@context" => "/contexts/Message",
    "@type" => "Message",
    "hydra:title" => "Success",
    "hydra:description" => "You have successfully been logged out.",
    "hydra:status" => Response::HTTP_ACCEPTED
    );

    return new JsonResponse($data, Response::HTTP_ACCEPTED);
    }
    }
    }

    This is reference by the logout success_handler in secrity.yaml.

    I haven't had time to test this with Behat or PHPUnit. But, seems to work when testing using PostMan and using a browser.

  • 2018-10-05 weaverryan

    Hey Scott Collier!

    Sorry for the slow reply. But yea, I'd be happy to help you with that :). So, when you don't send the Bearer token, it means that both of your authenticators do nothing (i.e. supports() returns false). This means that your request continues anonymously, then it hits some security check. At this point, Symfony calls the "entry point" for the firewall. In practice, that means it calls the "start" method for one of your two authenticators. You choose WHICH start() method (which entry point) should be used via the entry_point config under "guard" in security.yaml.

    So, change the entry_point config to point to the ApiTokenAuthenticator class instead. Then, add some code in its start() method to return a JsonResponse.

    The only downside is that if your site also supports normal users, if they try to access any page anonymously, they'll see a JsonResponse :). For the best of both worlds, in the start() method of your ApiTokenAuthenticator, you will need to (somehow) determine if the current request is an API request (then return JSON) or a web request (then redirect to the login page). How? Well, if all your URLs start with /api, that's one thing you could check for. Or, if your API exists only to power your own JavaScript, you could check if $request->isXmlHttpRequest(). It depends on your app :).

    I hope this helps & reaches you in time!

    Cheers!

  • 2018-10-03 Scott Collier

    I understand this section isn't quite finished yet. But, I need this for a project I am working so I am working my way through it.

    I downloaded the code to make sure that everything was the same in security.yaml, SecurityController, ApiTokenAuthenticator, LoginFormAuthenticator, etc.

    Everything works except if you send a request with Postman and do not include a Bearer Token (e.g. no auth header, basic auth, etc.).

    If you do not include a Bearer Token, the response recieved is the login page code. What I would like to do is return a JsonResponse.

    Any ideas about how to do this? I am not getting it to work.

    I don't think it matters. But, just in case, I am doing this in a new project using API Platform.

    Thanks in advance!