Symfony 4 - Security - Share context from multiple firewalls to one another - symfony

I think I reached some limit with the Symfony security component. Here's my problem: I have two firewall to manage two users type (with two distinct entities) authentication and access to two different part of the website. I have a third part to manage files, uploads, ... that have to be private and both users types need to access it.
So I made multiple providers in security.yml:
providers:
# used to reload user from session & other features (e.g. switch_user)
core_user_provider:
entity:
class: Akyos\CoreBundle\Entity\User
property: email
platform_user_provider:
entity:
class: App\Entity\Platform\UserPlatform
property: email
file_manager_provider:
chain:
providers: [core_user_provider, platform_user_provider]
And multiple firewalls
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
core:
pattern: ^/(app|admin)/
context: shared
provider: core_user_provider
anonymous: lazy
guard:
authenticators:
- Akyos\CoreBundle\Security\CoreBundleAuthenticator
logout:
path: app_logout
target: 'home'
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
file_manager:
pattern: ^/(file-manager)
context: shared
provider: file_manager_provider
anonymous: lazy
guard:
authenticators:
- App\Security\FileManagerAuthenticator
logout:
path: file_manager_logout
target: 'home'
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
platform:
pattern: ^/(platorm_login|plateforme)
context: shared
provider: platform_user_provider
anonymous: lazy
guard:
authenticators:
- App\Security\PlatformAuthenticator
logout:
path: platform_logout
target: 'home'
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
main:
anonymous: lazy
So a Platform user can't access Core, and a Core user can't access Platform. But both users needs to access File-manager, without re-log in. I can't place /file-manager urls under Core or Platform firewall because the other wouldn't grant access to it. So I need a third firewall to manage File-manager access. It use a chain provider that groups both Core and Platform users. It doesn't work either because if a Core user authenticate through the Core firewall it is not authenticated for the File-manager one, so it redirect to File-manager login page.. if the user logs in File-manager part it can access it, but when it turn back to Core part it has to re-connect again.
I tried several things but the closest solution is to use the context option on firewalls, so when a user is logged in through Core part it can access File-manager part without re-log because both firewalls shared the same context. That's what I want. But I also need it for the Platform firewall! So I also add same context option to it, and it works, both users types can access File-manager without log in again :D But as the three firewalls share the same context, Core users can access to Platform and vice-versa, and that breaks all the separation logic.. :'(
I need a way to tell security component "File-manager firewall has same context as Core firewall, and File-manager firewall has same context as Platform firewall, but Core and Platform firewalls doesn't share the same context". Something like this:
firewalls:
core:
context: core
file_manager:
context: [core,platform]
platform:
context: platform
main:
anonymous: lazy
I found nothing about it. Maybe it can't be done, maybe I have to create custom provider or authenticator to hack it. Maybe I can make it without Symfony, it's only php after all, so could I make the file-manager part accessible to every one (so under the main firewall) and add a Listener that would check if the request is for file-manager, find in session if there is a previous logged-in user, check if the user is a Core or a Platform user and then redirect if not... ? How can I find the previous Core or Platform user in session, when on a "main firewall" page (= authenticated as anonymous), without Symfony functions ? I'm not good enough to know how I could achieve that. Help ?
Thanks

I've finally let 3 providers and firewalls with context shared between it. To prevent Core users to access Platform, and vice-versa, I added access_control:
- { path: ^/file-manager, roles: [ROLE_PLATFORM, ROLE_CORE] }
- { path: ^/core, roles: ROLE_CORE }
- { path: ^/plateforme, roles: ROLE_PLATFORM }
so it ends with a 403 access denied error. That's not the behavior I want so I also added 'access_denied_url' option on both core and platform firewall to redirect user on the good login page. As contexts are shared, users are already logged, so on login template I check instance of user object to advice him to disconnect first before trying to access this part.
{% if instanceOf(app.user, 'App\\Entity\\PlatformUser') %}
You're already logged in Platform space, please log out before access Core space.
{% else %}
You're already logged in as {{ app.user.username }}, log out or access core panel.
{% endif %}
It's a bit confusing to have context shared between parts that shouldn't share anything but access to file-manager, but no user can access other part so.. that works.

Related

Symfony Logon failing on TEST and PROD environments

I have a symfony 5.3 installation with 2 security providers.
If I'm using the DEV environment I can login using users from both the providers, but when I change the environment to TEST or PROD I can only login using one of them.
If I add logging to my authenticator I see that I reach the function onAuthorizationSuccess with the right user on both cases, but if I'm on TEST or PROD, the page that I see is again the login page. If I switch between environments I can replicate the behaviour any time: dev ok, test and prod not passing from the login page for one of the providers. The other one ok in all the situations.
I understand that Symfony is working differently on dev than test or prod, but I'm not able to figure out what is the difference.
There are no errors on the logs and I have cache:clear a few times..
Is there any way I can debug this behaviour?
The behaviour changes completely if I'm on DEV or TEST/PROD environments. The test environment uses a .env.test copy of .env.dev, so it should'nt affect the result.
If I change .env to use DEV and clean cache it works: the 2 types of users can login, if I change .env to use TEST for example and clean cache again it doesn't work...
My security.yaml (relevant parts):
security:
chain_provider:
chain:
providers: [app_user_provider, sec_user_provider]
app_user_provider:
entity:
class: App\Entity\WebUser
property: login
sec_user_provider:
entity:
class: App\Entity\Security\User
property: login
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: chain_provider
guard:
authenticators:
- App\Security\BasicAuthenticator
logout:
path: app_logout

how to get all users connected to default user without going through login page?

I want to secure my API with FOSUserBundle (and avoid the use of FOSOAuthServerBundle due to lack of documentation with complete example).
To make my API secure, I just created firewalls as follows:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_doc:
pattern: ^/api/doc
security: false
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
- { path: ^/admin/, roles: [ROLE_SUPER_ADMIN]}
This is great! But this requires that every one has to be connected before using the website so that I can display the list of ads, etc. ( Even GET queries needs a connected for security purpose)
To overcome, I am thinking about having all the users connected with a "default" user with limited privileges (just enough for simple querying without posting).
Is it possible to do so?
Thanks for your help.
PS: I did manage to get FOSUserBundle and FOSOAuthServerBundle working together. In fact, I was able to create a client and get an access token for it through command lines. But, I was not able to config the security.yml properly so that I get a login page that returns an access token.
Take a look at the symfony entry on How to Authenticate Users with API Keys. The entry has most of the code that you need and is well documented.
You do have to add a field for the API key in your fos_user table e.g.
ALTER TABLE fos_user ADD api_key VARCHAR(255) DEFAULT NULL;
You also need to correctly implement the ApiKeyUserProvider::getUsernameForApiKey to look up in your database for the user based on the given API key, something like this:
$repository = $this->manager->getRepository('Application\UserBundle\Entity\User');
$user = $repository->findOneBy(array('apiKey' => $apiKey));
return empty($user) ? $user : $user->getUsername();
I implemented this a few years ago, it was simple to follow and has worked great!

Authentication : app_dev.php/_wdt/511509b611682 instead of homepage

I'm looking at security in Symfony 2.0 and I have a problem I can't explain.
My security bundle is very simple for now.
I try to put everything working before putting real providers.
So, now, when I go on the site, It sends me on the login form as expected. I put the user and the password and then, instead of home page, i am send to "/.../app_dev.php/_wdt/511509b611682" (different number each time).
My user isn't marked as authenticated in the debug toolbar.
If i take off the end of the url, i arrive on homepage. My user seems to be identified and authenticated in the debug toolbar.
This arrives only in dev environment. In prod environment, it seems to work as expected.
Thanks for your help
Further to #artworkad's answer, you have to add the dev firewall before your main firewall, otherwise it will never match:
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
#...
When you log in using development environment you will be redirected to index_dev.php/_wdt/4e95412bc6871.
WDT aka web debug toolbar can be removed from the scope of the firewall via
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
Actually it is not related to SecurityBundle, anyways it is documented here https://github.com/FriendsOfSymfony/FOSUserBundle/issues/368
Also you can put this lines in access_control:
{ path: ^/_wdt, role: IS_AUTHENTICATED_ANONYMOUSLY }
{ path: ^/_profiler, role: IS_AUTHENTICATED_ANONYMOUSLY }
but I prefer keep my access_control for only my app routes.

How to configure a remember me aware listener in Symfony 2?

I have implemented a custom authentication provider successfully, but now I also need to add 'remember me' functionality, and I couldn't find docs on how to do that.
I tried adding this:
remember_me:
key: "%secret%"
lifetime: 31536000 # 1 year
always_remember_me: true
But it says this:
You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.
I found this but I'm not sure how to use it: Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider
So where is the RememberMeAwareInterface? (I guess there is one? Like ContainerAware) And what should I do with it?
I don't think I need to write my own implementation, the default one should work fine with my custom auth provider.
I was having the same issue with a custom Facebook authentication provider I wrote. The solution ended up being pretty simple:
I'll assume you implemented a custom authentication provider with a custom SecurityFactoryInterface implementation that extends from Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory. If you did this, the rest is a matter of configuration:
In your security configuration, configure the remember_me functionality for your firewall. Assuming you're configuring that into the public firewall, the added config params might look something like this:
firewalls:
public:
remember_me:
key: "%secret%"
lifetime: 31536000 # 365 days in seconds
path: /
domain: ~ # Defaults to the current domain from $_SERVER
In the same configuration, enable the remember_me functionality for your authentication provider. Assuming you're configuring that into the public firewall and your SecurityFactoryInterface implementation's getKey() method returns yourAuthProviderKey, the added config params might look something like this:
firewalls:
public:
yourAuthProviderKey:
remember_me: true
Finally, when your Authentication Provider handles logins, make sure you request the remember me feature by having an http GET or POST parameter named _remember_me with value 1 in the http request. (Note though: this parameter might need a different name if you changed its default value in your security config.) For example, in my case, I had to tell Facebook to redirect to the following URL after it handled the authentication: http://www.mydomain.com/auth-callback/?_remember_me=1. (Note the part after the ?)
Hope this helps!
Did you add this to your form_login section?
form_login:
remember_me: true
From Symfony 2.8, the "key" is replaced by "secret". So you will have:
remember_me:
secret: %secret%
lifetime: 31536000
If you run across this error, that is the fix
You can try this:
firewalls:
secured_area:
pattern: ^/
anonymous: ~
form_login:
csrf_provider: form.csrf_provider
login_path: login
check_path: login_check
always_use_default_target_path: true
default_target_path: /the-cao
remember_me: true
logout:
path: /logout
target: /
remember_me:
key: "%secret%"
lifetime: 31536000 # 365 days in seconds
path: /
domain: ~ # Defaults to the current domain from $_SERVER
In my case it happened when i create a Factory class implementing SecurityFactoryInterface (as it was described in example "How to Create a custom Authentication Provider"). Later i found that another way to create this Factory is extending from AbstractFactory which contains neccessary readme stuff (you can find it create() method). So there are two solutions: 1) extend AbstractFactory instead of implementing SecurityFactoryInterface 2) implement SecurityFactoryInterface and copypaste readme related code. In symfony 3.1:
// add remember-me aware tag if requested
if ($this->isRememberMeAware($config)) {
$container
->getDefinition($listenerId)
->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProviderId))
;
}

Authenticate multiple symfony2 firewalls with one login form

I have two firewalls:
api (for API calls)
main (for everything else)
My client app login happens via the main firewall. However, it does interact with endpoints under the api firewall to fetch data. The problem here is that I don't want to force the user to log in a second time for authenticating against the second firewall.
How can I authenticate against both firewalls with just a single login form?
Perhaps you could try the 'context' firewall property.
Say you have a configuration something like this (which presumably you do):
security:
// providers etc ...
firewall:
main:
pattern: # ...
provider: my_users
http_basic: ~
api:
pattern: # ...
provider: my_users
http_basic: ~
In this case the user's session will contain a '_security_main' property after authenticating against the 'main' firewall, and then when they attempt to access an 'api' location they will be prompted to re-auth and will then gain a '_security_api' session property.
To prevent this re-prompt, you can add the 'context' property to each firewall definition you wish to share the same authentication - so:
security:
# providers etc ...
firewall:
main:
pattern: # ...
provider: my_users
http_basic: ~
context: primary_auth # new
api:
pattern: # ...
provider: my_users
http_basic: ~
context: primary_auth # new
In this case, upon authentication with the 'main' firewall, a '_security_primary_auth' property will be set in the user's session. Any subsequent requests inside the 'api' firewill will then use the value of '_security_primary_auth' to establish authentication status (and so the user will appear authenticated).
Of course this authentication context sharing will work both ways around (whether they auth first with the 'main' or the 'api' firewall) - if you only wanted transience in one direction, things would be more complex.
Hope this helps.

Resources