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 SubscribeHave you ever had a situation where you're helping someone online... and it would be so much easier if you could see what they're seeing on their screen... or, better, if you could temporarily take over and fix the problem yourself?
Yeah, just click the little paper clip icon to attach the file. It should be like near the bottom... a paper clip. What's "attaching a file"? Oh... it's um... like sending a "package"... but on the Internet.
Ah, memories. Symfony can't help teach your family how to attach files to an email. But! It can help your customer service people via a feature called impersonation. Very simply: this gives some users the superpower to temporarily log in as someone else.
Here's how. First, we need to enable the feature. In security.yaml
, under our firewall somewhere, add switch_user: true
:
security: | |
... lines 2 - 20 | |
firewalls: | |
... lines 22 - 24 | |
main: | |
... lines 26 - 46 | |
switch_user: true | |
... lines 48 - 61 |
This activates a new authenticator. So we now have our CustomAuthenticator
, form_login
, remember_me
and also switch_user
.
How does it work? Well, we can now "log in" as anyone by adding ?_switch_user=
to the URL and then an email address. Head back to the fixtures file - src/Fixtures/AppFixtures.php
- and scroll down. We have one other user whose email we know - it's abraca_user@example.com
:
... lines 1 - 15 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 20 - 51 | |
UserFactory::createOne([ | |
'email' => 'abraca_user@example.com', | |
]); | |
... lines 55 - 57 | |
} | |
} |
Copy that, paste it on the end of the URL and...
Access Denied.
Of course! We can't allow just anyone to do this. The authenticator will only allow this if we have a role called ROLE_ALLOWED_TO_SWITCH
. Let's give this to our admin users. We can do this via role_hierarchy
. Up here, ROLE_ADMIN
has ROLE_COMMENT_ADMIN
and ROLE_USER_ADMIN
. Let's also give them ROLE_ALLOWED_TO_SWITCH
:
security: | |
... lines 2 - 6 | |
role_hierarchy: | |
ROLE_ADMIN: ['ROLE_COMMENT_ADMIN', 'ROLE_USER_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'] | |
... lines 9 - 61 |
And now... whoa! We switched users! That's a different user icon! And most importantly, down on the web debug toolbar, we see abraca_user@example.com
... and it even shows who the original user is.
Behind the scenes, when we entered the email address in the URL, the switch_user
authenticator grabbed that and then leveraged our user provider to load that User
object. Remember: we have a user provider that knows how to load users from the database by querying on their email
property:
security: | |
... lines 2 - 13 | |
# 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 | |
... lines 21 - 61 |
So that's why we used email
in the URL.
To "exit" and go back to our original user, add ?_switch_user=
again with the special _exit
.
But before we do that, once a customer service person has switched to another account, we want to make sure they don't forget that they switched. So let's add a very obvious indicator to our page that we're currently "switched": let's make this header background red.
Open the base layout: templates/base.html.twig
. Up on top... find the body
and nav
... and I'll break this onto multiple lines. How can we check to see if we are currently impersonating someone? Say is_granted()
and pass this ROLE_PREVIOUS_ADMIN
. If you're impersonating someone, you will have this role.
In that case, add style="background-color: red"
... with !important
to override the nav styling:
... line 1 | |
<html> | |
... lines 3 - 14 | |
<body> | |
<nav | |
class="navbar navbar-expand-lg navbar-light bg-light px-1" | |
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }} | |
> | |
... lines 20 - 66 | |
</nav> | |
... lines 68 - 72 | |
</body> | |
</html> |
Let's see it! Refresh and... ha! That's a very obvious hint that we're impersonating.
To help the user stop impersonation, let's add a link. Go down to the dropdown menu. Once again, check if is_granted('ROLE_PREVIOUS_ADMIN')
. Copy the link below... paste... then send the user to - app_homepage
but pass an extra _switch_user
parameter set to _exit
.
If you pass something to the second argument of path()
that is not a wildcard on the route, Symfony will set it as a query parameter. So this should give us exactly what we want. For the text, say "Exit Impersonation":
... line 1 | |
<html> | |
... lines 3 - 14 | |
<body> | |
<nav | |
class="navbar navbar-expand-lg navbar-light bg-light px-1" | |
{{ is_granted('ROLE_PREVIOUS_ADMIN') ? 'style="background-color: red !important"' }} | |
> | |
<div class="container-fluid"> | |
... lines 21 - 29 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
... lines 31 - 41 | |
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} | |
<div class="dropdown"> | |
... lines 44 - 54 | |
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="user-dropdown"> | |
{% if is_granted('ROLE_PREVIOUS_ADMIN') %} | |
<li> | |
<a class="dropdown-item" href="{{ path('app_homepage', { | |
'_switch_user': '_exit' | |
}) }}">Exit Impersonation</a> | |
</li> | |
{% endif %} | |
... lines 63 - 65 | |
</ul> | |
</div> | |
{% else %} | |
... lines 69 - 70 | |
{% endif %} | |
</div> | |
</div> | |
</nav> | |
... lines 75 - 79 | |
</body> | |
</html> |
Try that! Refresh. It's obvious that we're impersonating... hit "Exit Impersonation" and... we are back as abraca_admin@example.com
. Sweet!
By the way, if you need more control over which users someone is allowed to switch to, you can listen to the SwitchUserEvent
. To prevent switching, throw an AuthenticationException
. We'll talk more about event listeners later.
Next: let's take a short break to do something totally fun, but... kind of not related to security: build a user API endpoint.
Hey Rufnex,
Could you elaborate a bit more on your question? I don't fully understand what you need. If you're talking about impersonation, any user that has the role ROLE_ALLOWED_TO_SWITCH
will be able to impersonate anybody
Cheers!
Hi MolloKhan,
in the user provider this e.g. uses property: email as identifier. I need a second field (company_id) to identify the user. Can you give me an example for that?
(select .. from users where email = '...' and company_id = 123)
Ok, in that case you'll need to load your users manually by sending a callback as second argument to the UserBadge
, and compose the userIdentifier
by concatenating the email
+ companyId
. Something like this:
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email');
$company = $request->request->get('company');
$userIdentifier = sprintf('%s:$s', $email, $company);
return new Passport(
new UserBadge($email, function($userIdentifier) {
[$email, $company] = explode(':', $userIdentifier);
$user = $this->userRepository->findOneBy(['email' => $email, 'company' => $company]);
if (!$user) {
throw new UserNotFoundException();
}
return $user;
}),
new PasswordCredentials($password),
);
}
I didn't try that code but it should give you a good idea of what to do :)
Cheers!
Thank you, thats so far clear. But what i mean is how to do this with the ?_switch_user .. for that i have to set also the company_id .. is this possible?
In my database, an email can occur more than once. This is made unique in combination via the company_id.
Ohh I get it now. Symfony, internally uses the UserProvider
to load the impersonated user object, it calls the loadUserByIdentifier()
, so, what you can do is create a custom UserProvider
implementing that method, and the $userIdentifier
argument will be (as I mentioned above) a combination of your email + companyId. Does it makes sense?
You can check this class if you want to get deeper on how your users are loaded Symfony\Component\Security\Http\Firewall\SwitchUserListener
Hey Abraham,
Here are the docs to configure in-memory users, however it's not recommended for production
https://symfony.com/doc/current/security/user_providers.html#memory-user-provider
Cheers!
Hey Symfony Cast Team. I never leave a comment, but I am at wits end. I took on the task of updating a Symfony 3.4 application recently. I have it up to 5.4.6 currently and all is working fine. Although, the impersonate user ability seems to be broken. When I use the _switch_user=username parameter, it switches me over to the user. But then as soon as I click on anything else, it boots me back to the login page with a 403 http status in the web profiler. It redirected me to the security controller::loginAction.
Here are some other things that may help you answer this.
Symfony 3.4 upgraded to 5.4. Using OAuth (HWI/OauthBundle) with Azure,
What more information would you need to help out with this issue?
Reading some of the items that were already posted in a little more depth, I am using access control.
Hey @Mathew!
Welcome to the comments - I hope we can help :).
Are you able to repeat this locally? If so, turn intercept_redirects
to true: https://github.com/symfony/recipes/blob/2d1ba2cf32556d9c01ff7bd05bb02ec9bfb44e5c/symfony/web-profiler-bundle/5.3/config/packages/web_profiler.yaml#L4
This will "stop" redirects. Basically, each time it is about to redirect you, you will instead see a page saying "you're about to be redirected" with a link to the redirect. The reason this is useful is that this page will have a web debug toolbar at the bottom. On that, you'll be able to see if you're still logged in and who you are logged in as. I'm particularly interested in exactly when you are logged out. For example, here is a potential flow that you could confirm or tell me which part is wrong:
A) You add _switch_user=username
B) This is "read" then you are redirected back to the same URL but without the _switch_user=username (because, now, in theory, it has been processed). At this moment, you ARE logged in as the impersonated user.
C) You click a link. You are suddenly redirected to /logout. On this page's web debug toolbar, you can see that you are NOT logged out.
Pay VERY careful attention to where you are or aren't redirected and who you are logged in as in each step. What flow exactly are you seeing? And, when you finally identify the page (well, it will be a redirect to /login probably) where you are mysteriously not logged in anymore, if you click into the profiler, do you see any interesting logs for this page?
Overall, my initial instinct is that you're losing authentication because it appears that your user is "changing". I can go into more detail about that... once if confirm that it is or isn't the problem. A super fast way to check if this is the issue is to:
1) Add EquatableInterface to your User clas
2) In the isEqualTo method (which the interface will force you to have), simply return true
.
If the problem goes away, then we've nailed down the cause and we can then debug (pretty easily) why this is happening and a proper solution (the above is NOT a proper solution).,
Lemme know!
Cheers!
Thank you so much for your quick reply!
I have turned on the intercept redirects (which is very helpful) and tried the impersonate process again.. So far, what is happening.
I receive the redirect notice for the following.
- Clicking login (Anon)
- Redirected to Microsoft for credentials (which never displays due to remember me (Anon)
- Redirected to our app's main dashboard (AdminUser)
- Redirected to the admin dashboard once it realizes what roles I have (AdminUser
- From there I click on a link in an impersonation GUI (AdminUser)
- Redirected to the standard non admin dashboard (BasicUser)
- Click on any link or refresh, instantly sent to the login screen with no redirect notice (Anon)
I then tried your second recommendation or implementing EquatableInterface and adding the required isEqualTo method which returns true.
IT WORKED!
I very much appreciate the work that you and your team do. While it may be my first time in the conversation tab, I use your lessons quite a bit. Your lessons are excellent. I wish I had more time to watch them all!
Hey @Mathew!
Woohoo! I'm glad we figured it out! Ok, but before you celebrate TOO much, we should get that isEqualTo() method into a condition where it works AND is "secure". Quick explanation:
A) At the end of each request, the User object is serialized into the session. If you have a _serialize() method or implement SerializableInterface, then those methods will be called to do this. Otherwise, iirc, every property is serialized.
B) At the start of the next request, that User object is deserialized from the session.
C) THEN, the security system queries (assuming your User class is an entity - on a technical level, refreshUser()
is called on your user provider) for a "fresh" user. It basically looks at the "id" of your deserialized User object and uses it to fetch a fresh one from the database.
Now, here is where things get interesting. You now have 2 User objects floating around: the deserialized one and the fresh one from the database. At this point, Symfony "compares" these to see if they are "different". And if they ARE different, you are logged out. This is what was happening to you. Why does Symfony compare the objects? It's trying to see if some sensitive data has changed in the database since the user logged in. Here is the most concrete example: suppose a bad user logs in as me on some browser. I find out, log in, and change my password. At this point, our system needs to be smart enough to deauthenticate the "bad user". This "user comparison" process does that.
Normally, to compare the User objects, Symfony uses a built-in algorithm: https://github.com/symfony/symfony/blob/ed9f973de5ed3c950cc0769557919f6ad7a60210/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php#L292-L331 (the logic for this has moved in 6.0, but is still the same).
You can see that it compares several properties on the deserialized User with the fresh User, including password, roles and user identifier. For some reason, once you switched users (and so, the "switched" user would be the one that is now deserialized), this check began to fail. By implementing EquatableInterface, you "took over" and fixed this.
This is a long way of saying that you have 2 options:
A) Figure out why the original logic was failing and fix it. Maybe you are not serializing the roles property so it's missing when you do the comparison? It's a bit weird that you have this problem when switching... but not under normal situations.
B) Keep isEqualTo() but implement your own checks. Ask yourself this about each property on User: if this value changes, should the user be deauthenticated on all other browsers? If yes, then add that check.
Cheers!
I THINK im picking up what you're putting down.. I wrote this up, let me know if this is what you were describing.
I am assuming that you meant to check the userInterface brought in to the isEqualTo method against the user object itself?
public function isEqualTo(UserInterface $user)
{
$userObjectHasRole = false;
$userInterfaceHasRole = false;
if ($this->hasRole('ROLE_USER'))
$userObjectHasRole = true;
$roles = $user->getRoles();
foreach ($roles as $role) {
if ($role == 'ROLE_USER')
$userInterfaceHasRole = true;
}
$userInterfaceActive = false;
$userObjectActive = false;
if ($user->getStatus())
$userInterfaceActive = true;
if ($this->getStatus())
$userObjectActive = true;
if ($userObjectHasRole and $userInterfaceHasRole and $userObjectActive and $userInterfaceActive)
return true;
else
return false;
}
Hey @Mathew!
Apologies for the slow reply! Yes, this is exactly what I had in mind. I would add just a few notes:
A) You could probably simplify by "exiting earlier" instead of storing things in variables. For example:
if ($this->hasRole('ROLE_USER') && !$user->hasRole('ROLE_USER') {
return false;
}
if ($this->getStatus() != $user->getStatus()) {
return false;
}
return true;
B) I would also add checks to compare the password of the user... and maybe also the email. You have to ask yourself (for example): is it feasible that the user would ever need to change their email to "regain" for a security reason and want all other browsers currently logged in to be deauthenticated? I think for "password", this is definitely a yes. For email, I'm less sure, but by default, the email address is a field that is normally "compared" in the core algorithm.
Cheers!
Hi.
Is it possible to use both(one of params) parameters "email" or "id"?
Some time for users support convenient to use "email" , another time "id".
Thank you.
Hey Ruslan
That's a good question and it's not actually documented. The anwers relies on a UserProvider. What you need to do is to add a custom user provider for your application, and implement the loadUserByIdentifier()
method, that method should be able to find users by email or by id (or whatever other field you may use to identify a user).
Cheers!
I have another question.
I'd like to define a section of the website (e.g. all the routes starting with /admin) where impersonation is ignored/skipped, so when you access those pages, you are not impersonating anyone and you are just logged in with your original user, but when you are visiting any other url, you keep impersonating.
So, for example. Say I have an admin user admin@example.com with ROLE_ADMIN, and a regular user user@example.com with no role (other than ROLE_USER). I log in as admin@example.com, then I switch user (i.e. start impersonating) to user@example.com. When I visit any non-admin url, i.e. any url that does not start with /admin, I am impersonating: I see everything as if logged in as user@, except for the red navbar that reminds me that I am impersonating. But when I go to /admin/whatever, I am not impersonating, so instead of getting an access denied error, I can simply access the page and I am logged in as my real user, admin@.
How can I do that? Can I attach a listener to some event where I can prevent impersonation from happening, but not by denying access, but rather cause the impersonation to be "skipped"?
Typical use case: I am an admin, I go to the admin panel, manage a user, I want to check something, so I start impersonating the user, then I go back to the admin panel and fix/edit something about that user's profile, then go back to the impersonation to check that everything is as expected from his point of view, and so on back and forth, without having to exit and enter impersonation over and over again (and I can even keep two separate browser tabs one with the admin panel and the other with the impersonated user - but that's irrelevant).
VBulletin 3.7 did this circa 2004.
By looking further into it, I think what I'm looking for is not to "prevent" impersonation from happening or "skip" it, because that happens at the moment of switching, whereas I'm looking for something that happens at every request. It's more like: when the session is loaded, I want to somehow retrieve the original user and replace it into the current user... something like that
Hey Matteo S. !
Hmm. This is really interesting... and it's not a problem I've ever thought of before. And once again, it comes back to the "token" object. We know that after we switch users, our token is a SwitchUserToken. And we also know that we can get the original token by saying $token->getOriginalToken().
Using these two things, I think you could trick the system. You would do it by:
A) Register a listener to the RequestEvent (the one that happens early in Symfony). Use a priority of 7 - you can see an example of priorities here: https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-subscriber - the reason I'm using 7 is that the security system is initialized with a listener at priority 8... and we want our listener called right after that.
B) In this listener, inject the Security service and use $this->security->getToken() to get the token object. If this is NOT an instance of SwitchUserToken, just return and do nothing. Or, if the URL does NOT start with /admin, return and do nothing (so, do nothing if the current URL is one where you want the normal switch user behavior to happen).
C) If it IS an instance of SwitchUserToken, then get the token and call $originalToken = $token->getOriginalToken()
. store the SwitchUserToken on some property on your subscriber for later - e.g. $this->switchUserToken = $token;
D) Now we are going to take that $originalToken
and tell Symfony that we are authenticated using it. Do that by injecting the TokenStorageInterface service, and then call $this->tokenStorage->setToken($originalToken)
.
E) And... you're done! You should now be logged in as whatever the original user & token was. But, before the request finishes, you need to "restore" the SwitchUserToken, otherwise you'll "lose" the impersonation permanently. To do that, use the same class you created in step (A) and make it listen to ResponseEvent with a priority of 1 (so that you are called before ContextListener, which has a priority of 0). Very simply, if ($this->switchUserToken)
then $this->tokenStorage->setToken($this->switchUserToken)
.
The tk;dr is that you "change" the token to the original right after security starts and change it BACK to the "switch user" token right before security finishes. I can't think of any problems this would cause... it's kind of a cool idea ;).
Let me know how it goes!
Cheers!
So I'm trying this, but I think I've run into a problem. When my listener handles the RequestEvent, and the requested url is an /admin one, it seems that an AccessDeniedException has already been thrown, so apparently it's too late to switch tokens. This is the output of dd($event->getRequest()->attributes) inside my listener:
Symfony\Component\HttpFoundation\ParameterBag {#759 ▼
#parameters: array:4 [▼
"_controller" => "error_controller"
"exception" => Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException {#579 ▶}
"logger" => Symfony\Bridge\Monolog\Logger {#49 …6}
"_stopwatch_token" => "b8c8c0"
]
}
I'm using a priority of 7 for my listener as you said.
Hey Matteo S.!
Ah, darn. I bet I know the issue: part of the Firewall listener (the one that we are trying to execute right *after*) is the enforcement of access_control. So the Firewall listener initializes security (yay!) but then immediately uses the current user to enforce access_control (boo!).
If i'm right, the simplest solution would be to stop using access_control, which may be no big deal... or a huge bummer. If you NEED access_control, then... hmm. Here is another solution that *may* work. In the listener that handles RequestEvent, you're going to convert this into an event subscriber AND a security authenticator. The ResponseEvent logic will stay the same, but the RequestEvent logic get worked into the custom authenticator methods. We're basically going to do the RequestEvent logic but from inside of the authenticator methods, which should allow it to run during the security initialization but after access_control. In supports(), you would check to see if $this->security->getToken() is the SwitchUserToken. If it is, return true. In authenticate(), you would return a SelfValidatingPassport() containing the original user. And you would set the original SwitchUserToken on a property: the same "flow" as before. Then, in the ResponseEvent listener, you would replace the token at the end of the request lke normal.
I'm still making some assumptions here (I'm assuming the logic that loads the SwitchToken from the session and puts that into the security system occurs before your authenticators are called)... so it may not work. Also, this will make it look like your user is being authenticated on every request. What that means in practical terms is simply that events like LoginSuccessEvent will trigger on every request. So if you're relying on this to run code after a user *really* logs in, that'll mess things up. As a work around, you could do everything I said above about the authenticator except that you (A) return false ALWAYS in supports and (B) right before that return statement, you do all the logic of setting the token into the token storage. Basically, you "abuse" the authenticator system: you take advantage of the fact that supports() will be called at the correct time, and then do all your logic there, even though that's not what it's meant for ;).
tl;dr the access_control muddies things a lot. Doing this may still be possible, but it might not (depends on if my assumptions are correct).
Let me know if you get it working!
Cheers!
EDIT: BOGUS COMMENT my bad (I'll leave this to avoid causing confusion if you have aready received a notification of it)
[NOT TRUE:] I have tried the "Stop using access_control" approach. I control access with /** @IsGranted */ annotations instead. Now the flow goes as expected, my listener with priority 7 gets called when I go to /admin too, I can dd() stuff as expected, but even though I switch the token with $this->tokenStorage->setToken($token->getOriginalToken()), I get "access denied" regardless (that's because I wasn't properly checking for the controller being called).
Wooo! I was reading your comment in my inbox thinking "darn, I really thought that would work!". Haha, really happy it did - a nice system!
Thank you soooooooooooo much!! I'll try and let you know. You are the author and voice of the tutorials, right? I ask because I'm reading your replies with your voice in my head and if it's not you I need to stop doing that - lol.
Hey PhpFan,
> I'm reading your replies with your voice in my head and if it's not you I need to stop doing that - lol.
Hahaha, that's funny! :D And yes, you're right! Ryan is the author and the voice of this tutorial ;)
Cheers!
Also, how do you access the original user (i.e. the impersonator) from twig? I want to show the username or email of the original user next to the "exit impersonation" link.
Hey Matteo S.!
Another good question! The answer goes back to the "security token" thing - a topic I don't talk about much in this tutorial because it's rarely useful (but in a few cases, it's very useful). After switching, your "security token" is an instance of SwitchUserToken: https://github.com/symfony/.... You can access this in Twig via app.token. So, app.token.originalToken will give you the "original" token object (probably an instance of UsernamePasswordToken... but it doesn't really matter, since most tokens have all the same methods except for a few special things like SwitchUserToken). To get the original user, it would be app.token.originalToken.user.
I hope that helps :).
Cheers!
What's the difference between using is_granted('ROLE_PREVIOUS_ADMIN') and is_granted('IS_IMPERSONATOR')?
Hey Matteo S.!
Effectively nothing. I don't know why both exist. On a technical level:
A) IS_IMPERSONATOR looks for your security "token" to be an instance of SwitchUserToken, which is the token that switch_user gives you.
B) ROLE_PREVIOUS_ADMIN obviously looks for that role... and that extra role is added always when you use the switch_user system.
I had never really thought about it before, but those are two different ways to check for the exact same thing. ROLE_PREVIOUS_ADMIN predates the other... so we may have added IS_IMPERSONATOR to be more clear, but never removed the other way.
Cheers!
Thank you!!! Now with what you said I just grepped (could have done it before actually) and found this:
symfony/security-core/Authorization/Voter/RoleVoter.php: trigger_deprecation('symfony/security-core', '5.1', 'The ROLE_PREVIOUS_ADMIN role is deprecated and will be removed in version 6.0, use the IS_IMPERSONATOR attribute instead.');
// 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.17.5
"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
}
}
Hey, how i have to extend this to allow the user identificatioin with more fields. For e.g. email and company_id.