> Symfony 5 >

Course Overview

Login to bookmark this course

Symfony 5 Security: Authenticators

Boost your Symfony flexibility with this course on streamlined security, featuring CSRF protection and an API token authentication system.

  • 4787 students
  • EN/ES Captions
  • EN/ES Script
  • Certificate of Completion

Your Guides

About this course

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.21.6
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}

It's security time! Symfony 5.3 comes with a reimagined version of its security system and I ❤️it! Yes, it's still super flexible & dependable. But the "guts" have been streamlined and simplified, making it easier to get your job done and giving you readable code if you need to dive into the core.

In this course, we'll go from an introduction into Symfony security into a full-blown application with users, permissions, custom voters and multiple ways to authenticate:

  • Generating your User class with make:user
  • Security & Firewall Fundamentals
  • Creating a custom login form with an authenticator
  • Passport object & Badges
  • CSRF protection
  • API token authentication system
  • User Providers (why you need them, but don't care)
  • Password Hashing
  • Logging out!
  • Protecting entire URLs with access_control(s)
  • IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, PUBLIC_ACCESS
  • Checking access with roles! ROLE_USER
  • Denying access in a controller
  • Voters & Complex Permissions
  • Role hierarchies
  • Impersonation (switch_user)
  • Login throttling
  • Automatic Login (after Registration)
  • Hooking into security with events!
  • Two Factor Auth (2FA)!

So let's make:user & make:auth our way to... make:profit... or at least to a great security system!

Next courses in the Symfony 5: The Fundamentals section of the Symfony 5 Track!

67 Comments

Sort By
Login or Register to join the conversation

Maybe You can cover google recaptcha v3?

1 | Reply |

Hey Sasa,

Thank you for your interest in SymfonyCasts tutorials!

Yeah, that's a good idea! And probably this course will fit this topic well. I'll add this to our topics list about Security in order to cover it in this tutorial, tough I'm not 100% sure if it will be included or no, but we will try our best.

Thanks for this idea! And if you could tell us a bit more what exactly is complex for you in this topic, or where exactly you're stuck, or what isn't covered by the official docs - that would be awesome and helps us to consider if it should be included in this tutorial or maybe deserves its own mini tutorial.

Cheers!

| Reply |

Hi Victor,

Thank you for interesting,

I create my security by watching videos from Symfony cast. And now I have problem to implement server side receptcha. There is some bundles but they use symfony forms for rendering login form, and they use Recaptcha3Type as a custom form type field, but I have basic HTML form, and I am confused. Also I founded bundle https://github.com/google/r..., but I don't know where and how to implement server side code.

Also maybe You can consider to cover integration with Oauth2, I find some instruction for implementing: https://dev.to/_mertsimsek/... - but it is much better when You explains it 😊.

Dont get me wrong, I understand that the goal is to present the possibilities of a security bundle, but for someone who does not have a lot of experience, or just want to make something quick and small in Symfony, everything get litle complicated and it takes too much time.

I think that there should be some simple, fast and reusable solution that will cover everything that is needed for authentication (Registration with email validation, reset pass, Log in, Oauth2, receptcha....)

Also I must say that I am very happy that I am accidentally found reset-password-bundle, and verify-email-bundle, and I think that's the right way to simplify and speed things up for Authentication

Best regard

| Reply |

Hey Sasa!

Thank you for providing more info from your side! It's really helpful when things come to planning a new course :) I won't promise everything your mentioned will be covered in this course, but I'll do my best to deliver this to the course authors, and probably they will fit some topics into it.

Also, good catch on those reset-password-bundle and verify-email-bundle :) Yeah, we would also like to cover them in our future screencasts, those bundles kinda new, and it would help to reveal them to more devs this way.

About oauth, you could also check another our bundle: https://github.com/knpunive... that helps with integration - it also has docs with examples how to configure it in your project.

I hope this helps!

Cheers!

| Reply |
It O. avatar It O. 3 years ago

Hi guys, as always i want to know release date.... 😬

1 | Reply |

Hey It O.!

This is the *next* tutorial - so it's right after Turbo. My guess would be about 3 weeks - it won't be significantly delayed because it's definitely happening after Turbo (I'm mostly not sure because I don't know yet exactly how long Turbo will go).

So, soon! Cjeers!

| Reply |

Awesome, can't wait to transform the current security system.

| Reply |

Thank's Ryan 👍

| Reply |
Bender avatar Bender 22 days ago

Hi there, I'm using Symfony to add some functionalities to a Moodle site. I've created a custom user provider and a custom authenticator. I've encountered a problem of loading assets and web debug toolbar. Here is the authenticator code:

<?php

namespace App\Security;

// config.php will set up $_SESSION
require_once(__DIR__ . '/../../../config.php');
require_login();

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class MoodleAuthenticator extends AbstractAuthenticator
{
    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request): ?bool
    {
        return $request->getPathInfo() === '/parent';
    }

    public function authenticate(Request $request): Passport
    {
        $username = $_SESSION['USER']->username;

        return new SelfValidatingPassport(new UserBadge($username));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // dd('success', $_SESSION);
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        // dd('failure', $exception);
        return null;
    }
}

When I access moodle.test/foo/admin (route generated with EasyAdmin), I'll be redirected to a login page first, after login, everything loads normally, the debug toolbar show "n/a" in the security section.
If I access moodle.test/foo/parent, I'll also be redirected to a login page, which is expected, but after submitted the credential, the content of the corresponding template will be loaded, but not the assets (css, js), nor is the debug toolbar in the bottom, and a refresh of the page get me a login page. Here is the ParentController code:

class ParentController extends AbstractController
{
    #[Route('/parent', name: 'app_parent')]
    public function index(): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED');
        return $this->render('parent/index.html.twig', [
            'controller_name' => 'ParentController',
        ]);
    }
}

Using browser's developer tool, I found that when accessing moodle.test/foo/parent, all requests to assets and debug toolbar have been redirected to the moodle login page and each request is setting up a new MoodleSession cookie. I feel like this can be solved by changing what's in supports() but I failed to figure out how, can someone shed some light on it? Any help is much appreciated!

Here is the network requests when accessing moodle.test/foo/admin:

Request URLMethodStatus CodeLocationSet-Cookie (Response header)Cookie (Request header)
http://moodle.test/foo/adminGET303https://moodle.test/login/index.phpdbntb3hl1cjs3f3leonh364950
http://moodle.test/login/index.phpGET200dbntb3hl1cjs3f3leonh364950
http://moodle.test/login/index.phpPOST303https://moodle.test/login/index.php?testsession=25gdnt90eba7nmdi7l2k17cdsgmdbntb3hl1cjs3f3leonh364950
http://moodle.test/login/index.php?testsession=2GET303https://moodle.test/foo/admin5gdnt90eba7nmdi7l2k17cdsgm
http://moodle.test/foo/adminGET2005gdnt90eba7nmdi7l2k17cdsgm
http://moodle.test/foo/bundles/.../app..xxxx.cssGET2005gdnt90eba7nmdi7l2k17cdsgm
http://moodle.test/foo/bundles/.../app..xxxx.jsGET2005gdnt90eba7nmdi7l2k17cdsgm
http://moodle.test/foo/_wdt/012daaGET2005gdnt90eba7nmdi7l2k17cdsgm

Here is the network requests when accessing moodle.test/foo/parent:

Request URLMethodStatus CodeLocationSet-Cookie (Response header)Cookie (Request header)
http://moodle.test/foo/parentGET303https://moodle.test/login/index.phprh0al1o4pmfnpuuuemkbjftsmk
http://moodle.test/login/index.phpGET200rh0al1o4pmfnpuuuemkbjftsmk
http://moodle.test/login/index.phpPOST303https://moodle.test/login/index.php?testsession=242mptspeutr70ag0hecdjr53ktrh0al1o4pmfnpuuuemkbjftsmk
http://moodle.test/login/index.php?testsession=2GET303https://moodle.test/foo/parent42mptspeutr70ag0hecdjr53kt
http://moodle.test/foo/parentGET200bo1voqkgbsks2a9p3b8rgiki5l42mptspeutr70ag0hecdjr53kt
http://moodle.test/foo/assets/style/app..xxxx.cssGET303https://moodle.test/login/index.phpjndsb2p21qjd1ucme651p5innsbo1voqkgbsks2a9p3b8rgiki5l
http://moodle.test/foo/assets/vendor/quill..xxxx.cssGET303https://moodle.test/login/index.php5untu9lqn76aar7v4uilcjjmdmbo1voqkgbsks2a9p3b8rgiki5l
| Reply |

Hey Bender,

Except the main firewall, do you have this dev one in your config/packages/security.yaml file?

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

And it should be above your main firewall. Does it help? It should allow loading your assets and WDT even for not logged in requests.

But I suppose when you logged in - you should be able to open those assets, no? Otherwise, please double check that links to the assets are correct, probably files are not available by the links you're using.

I hope this helps!

Cheers!

| Reply |

Hi Victor,

I do have the same dev one, looks like the problem is caused by the new MoodleSession cookie when redirect back to http://moodle.test/foo/parent in line 5 (or 5th request when accessing moodle.test/foo/parent). If all works correctly, all following requests should use the one (42mptspeutr70ag0hecdjr53kt) set by the POST request (the 3rd request). It might have something to do with how Moodle decides to set or destroy this MoodleSession cookie. Thanks anyway!

| Reply |

Hey Bender,

it might be so, yeah. Glad you figured out the problem :)

Cheers!

| Reply |

Hi Victor,

Sorry to bother again. I still haven't figure out why a redirect to moodle.test/qsishenzhen/parent will get a response header with Set-Cookie, hope you can point me to a direction that's closer to get a solution.

I've updated framework.yaml (according to the Session documentation) in hope that Symfony wouldn't overwrite $_SESSION once it's set by config.php from Moodle:

framework:
    session:
        storage_factory_id: session.storage.factory.php_bridge
        handler_id: ~
        cookie_secure: auto
        cookie_samesite: lax

Here is my updated MoodleAuthenticator code:

<?php

namespace App\Security;

// 'config.php' sets up $_SESSION
require_once(__DIR__ . '/../../../config.php');
// require_login();

...

class MoodleAuthenticator extends AbstractAuthenticator
{
    public function supports(Request $request): ?bool
    {
        return $request->getPathInfo() === '/parent';
    }

    public function authenticate(Request $request): Passport
    {
        if (!isset($_SESSION['USER']) || $_SESSION['USER']->id === 0) {
            // dd('in authenticate(), $_SESSION[\'USER\'] not corect', $_SESSION);
            // This Moodle function checks that the current user is logged in and has the required privileges
            require_login();
        }

        $username = $_SESSION['USER']->username;
        // dd('in authenticate(), $_SESSION[\'USER\'] correct', $_SESSION);

        return new SelfValidatingPassport(new UserBadge($username));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // dd('in onAuthenticationSuccess()', $_SESSION);
        return null;
    }

...
}

And ParentController:

namespace App\Controller;

...

class ParentController extends AbstractController
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
    }

    #[Route('/parent', name: 'app_parent')]
    public function index(): Response
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED');
        $moodleSession = $this->requestStack->getCurrentRequest()->cookies->get('MoodleSession');
        return $this->render('parent/index.html.twig', [
            'controller_name' => 'ParentController',
            'session' => $_SESSION,
            'moodleSession' => $moodleSession,
        ]);
    }
}

In a newly opened private tab, accessing moodle.test/qsishenzhen/parent will redirect me to the Moodle login page:

Request URLMethodStatus CodeLocation (in Response Headers)Set-Cookie (in Reponse Headers)Cookie (in Request Headers)
http://moodle.test/qsishenzhen/parentGET303 See Otherhttp://moodle.test/login/index.phpMOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=cjp2mrmdbe3lqqqa8u95hdn5lr
http://moodle.test/login/index.phpGET200 OKMOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=cjp2mrmdbe3lqqqa8u95hdn5lr

After submit username and password:

#Request URLMethodStatus CodeLocation (in Response Headers)Set-Cookie (in Reponse Headers)Cookie (in Request Headers)
1http://moodle.test/login/index.phpPOST303http://moodle.test/login/index.php?testsession=2MoodleSession=mdnp02a6oa53uqnrs9fr25bdu2; path=/, MOODLEID1=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/, MOODLEID1=%25B4%258D%25D7%2594%25DD; expires=Wed, 04-Dec-2024 11:50:33 GMT; Max-Age=5184000; path=/MOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=6sapvp0hsinoirktb6ae7aigh6
2http://moodle.test/login/index.php?testsession=2GET303http://moodle.test/qsishenzhen/parentMoodleSession=mdnp02a6oa53uqnrs9fr25bdu2; MOODLEID1_=%25B4%258D%25D7%2594%25DD
3http://moodle.test/qsishenzhen/parentGET200MoodleSession=eqv14r0i7qh0ae79nu35mrpag9; path=/; httponly; samesite=laxMoodleSession=mdnp02a6oa53uqnrs9fr25bdu2; MOODLEID1_=%25B4%258D%25D7%2594%25DD
4http://moodle.test/qsishenzhen/assets/styles/app-713d9828d7058036a44786836080554c.cssGET200MoodleSession=5g1ah7q4qqokqrnjq1viiog625; path=/MOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=eqv14r0i7qh0ae79nu35mrpag9
5http://moodle.test/qsishenzhen/assets/vendor/quill/dist/quill.snow-4da2c5a5fa53f7b23528685a046ece46.cssGET200MoodleSession=cgss87r9dtsf1v6mvgn2jab1ta; path=/MOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=eqv14r0i7qh0ae79nu35mrpag9
...and the remaining requets for assets fileswill have different Set-Cookie for MoodleSession valuessame values, MoodleSession is the one set by 3rd request, MOODLEID1_=%25B4%258D%25D7%2594%25DD; MoodleSession=eqv14r0i7qh0ae79nu35mrpag9

The page (moodle.test/qsishenzhen/parent) loads fine, with all the assets files loaded and WDT shows the authenticated user. But a refresh will redirect me to the Moodle login page, probably because of the new value for MoodleSesion cookie.

Access to moodle.test/qsishenzhen/admin is all okay except no authenticated user, refresh won't redirect.

My understanding of Symfony authenticator is that every request will go through authenticator first, so whether or not supports() returns true, the scripts in config.php will got executed. But why a redirect to moodle.test/qsishenzhen/parent would get a Set-Cookie header while a redirect to moodle.test/qsishenzhen/admin wouldn't (not with current code, but when I put require_login() right below require_once('config.php'))?

Again, any help is much appreciated!

| Reply |

Hey Bender,

Unfortunately, we don't have the bandwidth to help our users with their custom projects. I can try to give you some tips, probably they will help. But probably you will need to do more debugging work on it. First of all, I would recommend you to use symfony serve command to run your website locally on the default 127.0.0.1:8000 address. Because in case you use a real web server like Nginx/Apache - probably you need some config tweaks to make it work correctly, I'm not sure. Ideally follow the way that is shown in the course, i.e. use Symfony built-in web server.

In the support() method of your custom authenticator you may want to check for the specific route name instead of the path, e.g. you can get the route name from $request->attributes->get('_route') and check if it matches your login route name to decide if your authenticator supports this request or no. Also, if you use this authenticator for traditional login form - you can also check that the request is the POST request with $request->isMethod('POST');, something like this:

return 'security_login' === $request->attributes->get('_route')
        && $request->isMethod('POST');

Also, make sure you have installed HTTPS certificates to make HTTPS work locally for you. See symfony server:ca:install command for this in case you haven't installed it. Or try to uninstall it and install again.

Also, you can play with different config values for

    cookie_secure:        auto # One of true; false; "auto"
    cookie_httponly:      true
    cookie_samesite:      lax # One of null; "lax"; "strict"; "none"

You may also want to enable redirects intercepting to see from what page you're redirecting, see: https://symfony.com/doc/current/reference/configuration/web_profiler.html#intercept-redirects - sometimes it might be helpful to understand the problem with redirecting.

I hope that helps and thank you for understanding!

Cheers!

| Reply |
Kasal avatar Kasal 8 months ago

Hello there,
I am struggling with this : csrf tiken validation issue

| Reply |

Hey Kasal,

You can turn off the CSRF validation if that causes issues in your case or if you just don't need that.

Cheers!

1 | Reply |

Hello Victor,

Thanks for your response!

I have resolved it and added the solution in the link.

Cheers!

| Reply |

Hey Kasal,

Great, glad to hear you nailed it!

Cheers!

1 | Reply |
Ash-M avatar Ash-M 9 months ago edited

Something that has been looming over my head is the lack of guidance I have for testing the actual implementation of a particular component. In this guide you mention testing on a number of pages in the form of physically navigating to pages to see if certain actions behave correctly. For components which only involve configuration, that seems sufficient: you're configuring the application, so it follows the application should be run to test it. Inevitably you could translate this manual behavior into some methods provided by symfony/http-client (static::createClient() in a test), although it would be nice to structure the evolution of the application in a form of a test that just keeps expanding to fit the needs of the requirements throughout the tutorial.

That being said, this is the perfect component to illustrate my question. This component is modular in the respect that you can write your own classes (authenticators, listeners, voters, etc) to orchestrate behavior. Symfony already writes tests that ensure you can accomplish a certain feat this way (like the AbstractAuthenticatorTest), but are application tests really the only thing necessary when adding an Authenticator with custom behavior?

On the flip side, the simplicity of the design Symfony offers is sort of defeated by numerous layers of testing. So where do you draw the line?

  • Do you test that your UserRepository works?
  • Do you test that you can instantiate a custom authenticator, fake the details and see if $authenticator->authenticate() returns an expected result?
  • Do you unit test your authenticator and just check your implemented methods?

If you follow a TDD format, you might start with something like:

    public function setUp() {
        $this->token = 'abcdef';
        $this->user = new Security\User($email, $password, $token);
        $this->userRepo = new Fake\UserRepository();
        $this->userRepo->add($user);
        $this->authenticator = new Real\Custom\Authenticator($userRepo);
    }
    
    public function testSuccess() {
        $request = new Request();
        $request->setHeader('X-AUTH-TOKEN', $this->token);
    
        $passport = $this->authenticator->authenticate($request);
        assert($passport->getBadge(), new UserBadge($this->user->getIdentifier()));
    }

then you'd be like, "aw, I need a Security\User. php bin/console make:user Security\User ..." and it would drive you to make your repository and what not. This particular example is unintuitive in the sense that I had to forsee what the Authenticator contract dictates I return, and then check that. So I had to think to myself: "how does the symfony component determine that a user is authenticated?"

From a BDD perspective, a test like this doesn't really offer insight into any particular goal. You can say the goal is to "login", but there are 2 problems with that:

  1. you often want to describe to what end a feature is implemented; ie "why login?"
  2. the only real way to test that one can "login" is via an application test which distracts from the description of the feature itself

In that case you might write your feature like:

    Background:
        Given a user ...
        And authenticator ...
        And token ...
        And comment service ...

    Scenario: cannot comment without badge
        When the user tries to comment
        Then the comment will not be posted

    Scenario: user obtains a badge
        When the user submits token ....
        Then the user will obtain a badge

    Scenario: comment with a badge
        Given the user submits token ...
        And the user obtains a badge
        When the user tries to comment
        Then the comment will be posted

Although, I am sure this round BDD peg I just jammed into this square symfony-security hole is still sticking out quite a bit, you get the idea.

A famous philosopher Some person once said: "Testing is all about confidence."

If we feel really confident that the backend works as expected, then we don't have to retest all the features we tested through the frontend. In my first example, we've driven a lot of functionality, so it makes sense that we have a number of tests including this one and unit tests for the user, repository, and authenticator, real repository tests, smoke tests, and so on. But the next time around, for just a new authenticator, we'll use basically the same test as my example; you just replace Real\Custom\Authenticator with Some\Other\Authenticator. In this case, do we really even need to duplicate the test? Are passing unit tests enough?

The bottom line is that a lot of these tutorials are very good at describing how to implement a bundle (which is very well written!), but I generally don't see a lot about how to test your implementation of that bundle. I'd really appreciate it if you could provide some guidance on that.

| Reply |

Hey @Ash-M!

I have wondered about covering some basic testing throughout the series... just to help enable people who are interested. I might still do that for the Symfony 7 tutorials...

For authenticators specifically, these are such an "integration" layer: they are almost configuration that is tying many different parts of the system together. So yes, I generally use functional / e2e testing for these. And part of the reason that works is that the logic inside the authenticators is quite simple: there is one general code path and any real logic is isolated to other services (for example, query logic is isolated to the repository, which you could choose to test). I don't see value in a unit test: it feels meaningless to, for example, assert that the authenticate method returns a Passport with certain badges. If the logic in authenticate() were super complex... and I needed to check of some specific badge is returned in some scenario... MAYBE I would unit test it then. But I would first try to isolate that logic into a different service so I could unit test that.

Anyway, these are just personal opinions: a lot of thought and philosophy can go into testing, and I don't really think about it too deeply, tbh. But I hope hearing my thoughts (for what they're worth) is helpful :).

Cheers!

| Reply |

Hey @weaverryan,

Thanks for the prompt reply. It is very helpful to have any input on the topic, so thank you!

The tutorials are well written and more digestible than the official docs by offering a sample application to follow. It makes the information more organized, like once you get to a certain point, some of these feature become relevant. You don't get that guidance from the docs. However, both describe how to use a component and testing is never really addressed other than in isolated segments for how to test your application code.

To that end, do you have any ideas on what your integration tests might look like or opinions on the samples I described in my comment? What did you think about TDD and the through process of:

  • What does this component do?
    Authenticates a user
  • How does it do that?
    It returns a passport with a badge if authenticated.
  • Okay, write a scenario where a request with the (correct/incorrect) credentials (yields/doesn't yield) a passport

Or does that sound like a goal accomplished by an E2E test? Any example is fine since once you paint a path for this we can really apply it to any of the tuts.

Thanks again!

| Reply |

Hey @Ash-M!

The tutorials are well written and more digestible than the official docs by offering a sample application to follow

Thank you! That's the goal - and we can get to be more opinionated here, which helps :)

To that end, do you have any ideas on what your integration tests might look like or opinions on the samples I described in my comment?

Sure! You mentioned:

From a BDD perspective, a test like this doesn't really offer insight into any particular goal. You can say the goal is to "login", but there are 2 problems with that:

You might have a point, but actually, we do write tests specifically about logging in. On the Feature description, we use something like:

Feature: Login
  In order to smoothly use the site
  As a non-logged in visitor,
  I need to login and continue to the correct page

And then we have scenarios that specifically just logging in and its different cases. That allows us in scenarios for others features to just say:

  Background:
    Given I am authenticated

What did you think about TDD and the through process of:

If I understand correctly, you're talking about testing the authenticator class directly (via a unit test or perhaps an integration test if that makes more sense). I think doing that is fine, but I probably wouldn't. I would definitely have an E2E test (like described above with the "Login" feature). Most of the time, your authenticator will not be too complex, so testing the 1, 2 or 3 different "flows" in an E2E test is what I'd do. If your authenticator is more complex than that - with various different badges based on some complex logic, then I'd test it. But generally, the logic in authenticators is small enough that it doesn't "scare me" - which is my personal check for if I should unit test something. But of course logging in is super important, hence the E2E to make sure it's all working.

Cheers!

| Reply |

Thanks Ryan for taking the time to reply, and thanks for your opinionated insight! I think opinionated advice is great so long as people understand that it's opinionated and there are many ways to skin cats. It offers a much needed premise when teaching advanced things quickly.

Thanks again.

| Reply |
Sebastian-O avatar Sebastian-O 1 year ago

Hey guys, surely this course shows the best way even for sf6.4/sf7-projects – or do you expect something that won't work with sf6.4/sf7?

| Reply |

Hey @Sebastian-O!

Yup, this still works great with 6.4/7 and shows the best way to do things. It was recorded with Symfony 5.3, but there have not been major changes since then (a credit to the new security system!). We will upgrade this for Symfony 7, but it's still good to go. But if you have any issues or spot anything that looks weird or different, you can ask about it in the comments :)

Cheers!

1 | Reply |
Johann avatar Johann 1 year ago

Hello,
can i use symfonycasts/verify-email-bundle with Symfony 6.3?
I installed it in a 6.3 project, but files that were generated are quite different to the files here in this tutorial or github or in the symfony documentation.
i can't find where the verify URL is generated and no email is send.
maybe there is another bundle for registration confirmation?
thank you for your help.
JD

| Reply |

Hey @Johann!

Sorry for the slow reply! Yes, it should work just fine, hmm.

but files that were generated are quite different to the files here in this tutorial or github or in the symfony documentation

This is something we will need to check into - I totally see what you're referring to though!

i can't find where the verify URL is generated and no email is send.

When I followed the docs just now, I end up with some code that looks like this:

A) RegistrationController::register() - inside the if ($form->isSubmitted() && $form->isValid()) {, I have:

$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
    (new TemplatedEmail())
        ->from(new Address('mailer@example.com', 'AcmeMailBot'))
        ->to($user->getEmail())
        ->subject('Please Confirm your Email')
        ->htmlTemplate('registration/confirmation_email.html.twig')
);

B) In src/Security/EmailVerifier.php, we generate a new helper class to actually send the confirmation email. Its sendEmailConfirmation() contains the code that creates the signedUrl and other variables for confirmation + actually sends the email with $this->mailer->send().

Do you see similar code? Does this help point in the right direction? Emails are always tricky because it's possible everything works... but then a misconfiguration in the email system causes the email to not actually be delivered. However, you can always go to /_profiler and find the POST request to /register and click to see the profiler for that page. There, should be able to click the Emails tab on the left. If an email was sent during the successful registration form submit, you should see it here.

Let me know if that helps!

Cheers!

| Reply |

Hi Ryan,
thanks for the hints. Everything was my bad :-(
for whatever reason i have no debug-toolbar on the 'lost pw request-page'. (i have to investigate this). so i couldn't see the qeued emails because of the forgotten (i forgot) 'Symfony\Component\Mailer\Messenger\SendEmailMessage: async' config. i commented it out, and everything works fine!
thank you

| Reply |

Woo! I'm just happy it worked! And anyway, this poked me to update the verify bundle docs :)

Cheers!

| Reply |
mofogasy avatar mofogasy 2 years ago edited

Hello, the database configuration is a pain.

with xampp, when I make:migration I get

` Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB serve

r version for the right syntax to use near 'INDEX idx_9474526c1e27f6bf TO IDX_DADD4A251E27F6BF' at line 1`

then I retry make:migation and I get: Column already exists: 1060 Duplicate column name 'status'

what's going on here ?

| Reply |

Hey Grenouille,

We're using MySQL server in this course but looks like you have MariaDB installed. Though MySQL and MariaDB almost identical, they may have some differences, and looks like one of it you revealed with that error I'd recommend you to use MySQL instead if you have it installed, otherwise, since it's a learning project and does not contain any important data - you can skip migrations and just run:

$ bin/console doctrine:schema:update --force

It will sync the DB schema with your mapping configuration. You may lose some important data in the DB this way, but if those data are dummy data and you don't care about it - that's totally OK. And you can always reload fixtures again if needed.

I hope this helps!

P.S. But if you you'r working on a real project and will try to deploy changes to production - it seems like you need to regenerate migrations for your specific DB version.

Cheers!

| Reply |
Default user avatar GianlucaF 2 years ago edited

Hi,
is there a way to use CustomCredential with hashed password? I thought this could work but apparently, it doesn't:

`
....
new CustomCredentials(function($credentials, User $user) {

            return $this->passwordHasher->hashPassword($user, $credentials) === $user->getPassword();
        }, $password)`

I'm hashing the given plain password and I'm trying to check the hash using the password hasher.
But it does not work. What is wrong? ( I know I should use PasswordCredential ... )

| Reply |

Yes, only one chapter to go! 🎉🎉🎉 And off course looking forward for new courses! Is there a place on this site where we can follow which courses are being released in the near future? I know the next one will be EasyAdmin. But i'm really curious for the other *top secret* plans that you have.

| Reply |

Hey Lex,

Yeah, it's super closed to be released in full already :) Yeah, we do have such a page! Take a look at this link: https://symfonycasts.com/co... - this will list upcoming tutorials, but nothing much there yet except the EastAdmin that you're right should be next, and it should happen very soon already ;)

Thank you for your patience!

Cheers!

| Reply |

What about Cache? I can't see any course about Symfony Cache.

| Reply |

Hey @Solvex

You're right we do not have a dedicated course on cache, but it's because it's just a small thing that we explain in other tutorials. I'll leave you here a chapter that you can watch about caching stuff
https://symfonycasts.com/sc...

Cheers!

| Reply |

I don't think cache is simple. Contracts, PSR-6, Marshalling, CachePoolItem, TagAware... In fact, it is very difficult to use, especially the tagged cache. Perhaps a small course of 5-10 lessons will fill in the gaps when using the cache.

| Reply |

That's a pretty good idea... :)

1 | Reply |
Evgeny avatar Evgeny 2 years ago edited

@there Hello! Will the course be completed at all?

| Reply |

Hey Solvex,

Sure! SymfonyCasts doesn't have any courses that were not finished after they had started releasing ;) It will be continued after Christmas / New Year holidays, sorry for any inconvenience and thanks for your patience!

Cheers!

| Reply |

Thanks!
We have been looking forward to this course for so long!
Merry Christmas and a Happy New Year!

| Reply |

Hey Solvex,

Merry Christmas and a Happy New Year to you too! :) Sorry for holiday delays!

Cheers!

| Reply |

Спасибо!
Очень эти праздники не вовремя.

| Reply |

Hello SymfonyCasts team!
Great job you are doing - i like you tutorials.
Could you please add logout after timeout (logout unactive user) capability to this tutorial?

| Reply |

Hey @Andris!

Thank you for the nice words :)

> Could you please add logout after timeout (logout unactive user) capability to this tutorial?

That simplest way to implement this is with a session timeout: https://symfony.com/doc/cur...

However, another approach would be to set a "lastActiveTime" property on your User object. I would do that by adding a listener to the RequestEvent (previously called kernel.request): it runs at the start of every request. In there, I would check the logged-in-user's "lastActiveTime" against the current time. If it is too long, then invalidate the session. If it is NOT too long, then update lastActiveTime to the current time.

If you have any questions on this, let me know :). I probably won't talk about it in the tutorial, but I'd be happy to answer questions here.

Cheers!

| Reply |

weaverryan
Thank you for reply!
I will do it like you suggest.
Thanks for the solution.

| Reply |
Gustavo D. avatar Gustavo D. 3 years ago

When this will be released?

| Reply |

Hey Gustavo D.

There is not an oficial release date yet but this course should happen after "Doctrine Relations" (https://symfonycasts.com/sc... so, it should not take too long :)

Cheers!

| Reply |

wondering if there are any plans to use PSR-15-compatible HTTP middleware for authentication.

| Reply |

Hey azeem!

That's not something we're going to cover, unfortunately. PSR-15 is built on top of PSR-7, which was designed (unfortunately) to be incompatible with Symfony's request/response objects (to be more fair, Symfony could, with some work, change their classes to implement PSR-7, but they don't want to implement it because they/we don't like the dev experience of PSR-7). And so, PSR-15 is awkward to work with in Symfony. There is a bridge that allows it to work - https://symfony.com/doc/cur... - but it's not a main use-case in the Symfony world :).

Cheers!

| Reply |

Delete comment?

Share this comment

astronaut with balloons in space

"Houston: no signs of life"
Start the conversation!