I am setting up a website which I want to use separate firewalls and authentication systems for frontend and backend. So my security.yml is configured as below. I am using in_memory user provider in early development phase.
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
backend_in_memory:
memory:
users:
admin: { password: admin, roles: [ 'ROLE_ADMIN' ] }
frontend_in_memory:
memory:
users:
user: { password: 12345, roles: [ 'ROLE_USER' ] }
firewalls:
# (Configuration for backend omitted)
frontend_login_page:
pattern: ^/login$
security: false
frontend:
pattern: ^/
provider: frontend_in_memory
anonymous: ~
form_login:
check_path: login_check_route # http://example.com/login_check
login_path: login_route # http://example.com/login
access_control:
# (Configuration for backend omitted)
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
I have omitted the backend part because it doesn't matter. The problem is still there when the omitted part is commented out.
The problem is that frontend authentication won't work with the above configuration. Here's what I did:
Visit http://example.com/login
Enter the credential (user:12345), click login
http://example.com/login_check authenticates the user
The authentication service redirects user back to http://example.com/. No error is thrown. In fact, when I turned on the debug_redirects option, it clearly shows that "user" is authenticated on the redirect page.
Expected behavior: The security token should show that I'm logged in as "user" after following the redirect and go back to the index page.
Actual behavior: The security token still shows "anonymous" login after following the redirect and go back to the index page.
But with nearly identical settings (paths and route names aren't the same), the backend part works correctly.
After some investigation I found that the cause is the way user providers is currently written. Notice that frontend_in_memory section is placed below backend_in_memory that is used for backend authentication. So I explicitly specify the frontend_in_memory provider for the frontend firewall. And it kind of works - I must login with "user:12345" in the frontend login page. Logging in with "admin" won't work. So it must be using the correct user provider. But I suspect that the framework cannot update the security token correctly because it is still searching the "user" account from the first user provider which is backend_in_memory. In fact I can make the above config work with either one of the following changes:
add "user" login to the backend_in_memory provider's user list (password needn't be the same), or
swap frontend_in_memory with backend_in_memory so that frontend_in_memory becomes the first user provider.
Of course they are not the correct way of solving this problem. Adding "user" account to the backend makes no sense at all; swapping the order of two user providers fixes the frontend but breaks the backend.
I would like to know what's wrong and how to fix this. Thank you!
I was stuck when I posted the question, but after a sleep the answer is found ;)
Turns out I came across an issue reported long ago:
https://github.com/symfony/symfony/issues/4498
In short,
The problem isn't about the configuration.
And it isn't about authentication neither.
It actually relates to how an authenticated user is refreshed after redirection. That's why the app is correctly authenticated as "user" on the redirect page, but not after that.
Here is the code when the framework refreshes the user (can be found in \Symfony\Component\Security\Http\Firewall\ContextListener):
foreach ($this->userProviders as $provider) {
try {
$refreshedUser = $provider->refreshUser($user);
$token->setUser($refreshedUser);
if (null !== $this->logger) {
$this->logger->debug(sprintf('Username "%s" was reloaded from user provider.', $refreshedUser->getUsername()));
}
return $token;
} catch (UnsupportedUserException $unsupported) {
// let's try the next user provider // *1
} catch (UsernameNotFoundException $notFound) {
if (null !== $this->logger) {
$this->logger->warning(sprintf('Username "%s" could not be found.', $notFound->getUsername()));
}
return; // *2
}
}
The above code shows how the framework loops through the user providers to find the particular user (refreshUser()). *1 and *2 are added by me. If a user provider throws an UnsupportedUserException, this means that the provider isn't responsible for the supplied UserInterface. The listener will then iterate to the next user provider (*1).
However, if what the user provider thrown is a UsernameNotFoundException, this means that the provider is responsible for the supplied UserInterface, but the corresponding account could not be found. The loop will then stop immediately. (*2)
In my question, the same user provider, \Symfony\Component\Security\Core\User\InMemoryUserProvider, is used in both frontend and backend environment. And InMemoryUserProvider is responsible for the UserInterface implemented by Symfony\Component\Security\Core\User\User.
In the frontend, "user" is in fact authenticated successfully. However, in the user refresh attempt,
The order of the user providers will be like this: backend in-memory provider, frontend in-memory provider.
So, backend in-memory provider will run first.
The backend in-memory provider believes it is responsible for the supplied UserInterface because it is also an instance of Symfony\Component\Security\Core\User\User.
But it fails to locate the "user" account (it only has the "admin" account).
It then throws a UsernameNotFoundException.
The refreshUser() routine won't bother to try with next provider because UsernameNotFoundException means that the responsible user provider is already found. Instead it stops trying and removes the authentication token.
This explains why the configuration won't work. Despite using a different user provider, the only way to work around this is to copy the framework's InMemoryUserProvider and User classes and change the refreshUser() method to check against the copied User class, so that the frontend and backend user provider uses different user classes and won't clash.
Related
I am trying to debug a Resque setup in an (inherited) app, and so I found that there is a route for resque at /hidden/resque that would be nifty to access, but I am unable to access the route. I am wondering what I need to do ... When I try to access that route I get a HTTP 500 due to this error being thrown:
Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException: Full authentication is required to access this resource.
I have tried accessing it both as a web page (after authenticating as an admin role on a different route) and using curl -H 'Authorization: Basic 9339034147964aebec6716c0110311d1' 'https://web.mysite/hidden/resque' -v. No go.
So what constitues "full authentication"? I am already logged in as an admin user on one of the other routes. Would I need to add anything more to the below config? This has not been setup by me, so I would not know if it ever worked.
app/config/routing.yml
ResqueBundle:
resource: "#ResqueBundle/Resources/config/routing.xml"
prefix: /hidden/resque
app/config/security.yml
access_control:
- { path: ^/hidden, roles: ROLE_ADMIN }
According to the docs:
IS_AUTHENTICATED_FULLY: This is similar to IS_AUTHENTICATED_REMEMBERED, but stronger. Users who are logged in only because of a "remember me cookie" will have IS_AUTHENTICATED_REMEMBERED but will not have IS_AUTHENTICATED_FULLY.
How can I be "more logged in" than using a cookie? Should I send a basic auth header with username and password base64 encoded?
If you ask for full authentication.
I.E:
/**
* Requiring IS_AUTHENTICATED_FULLY
*
* #IsGranted("IS_AUTHENTICATED_FULLY", message="Nope, no access")
*/
Then when you are logging in with an user, your Authorization Checker must have granted you the IS_AUTHENTICATED_FULLY status in order to have access.
As explained in the docs:
IS_AUTHENTICATED_FULLY: This is similar to IS_AUTHENTICATED_REMEMBERED, but stronger. Users who are logged in only because of a "remember me cookie" will have IS_AUTHENTICATED_REMEMBERED but will not have IS_AUTHENTICATED_FULLY.
You will be completely Authenticated if you manually log in, and not via a cookie. If you are using a command that remembers your credentials, that might be the issue.
Check Doc nº3 to see whether your actual way of entering that route falls inside the IS_REMEMBERED status. Even maybe you end up prefering using the less restrictive IS_AUTHENTICATED_REMEMBERED
Check the different documentations here:
https://symfony.com/doc/3.4/security.html#checking-to-see-if-a-user-is-logged-in-is-authenticated-fully
https://symfony.com/doc/3.4/security.html#learn-more
https://symfony.com/doc/3.4/security/remember_me.html
https://symfony.com/doc/3.4/components/security/authorization.html#authorization-checker
https://github.com/symfony/symfony/blob/3.4/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
In my Symfony project I have a members zone accessible only by logged users then I write this in my security.yml (access_control):
{ path: ^/membre/, role: IS_AUTHENTICATED_REMEMBERED }
I did too a form with fos_user_security_check action and a _target_path = detail_page_membre.
When I try to log my user, I was redirected to / and in my log I have this:
User has been authenticated successfully. {"username":"toto"} []
Matched route "detail_page_membre".
Populated the TokenStorage with an anonymous Token.
Access denied, the user is not fully authenticated; redirecting to
authentication entry point.
I don't have write any firewalls maybe it's this?
This issue often happens because of the session:
check your session configuration under framework in config.php file:
framework:
session:
# handler_id set to null will use default session handler from php.ini
handler_id: ~
Read more:
Logging in redirects to the login page with no errors
Symfony2 - Access is denied (user is not fully authenticated)
I am trying to adjust FOSUserBundle to work with my Neo4j database and I cant seem to get it working. After a long time of trying to implement my own user system without any luck (Setting up NEO4j in Symfony2), I started trying to use the FOSUserBundle.
I have used the following articles and repositories:
https://github.com/ikwattro/Neo4jUserBundle
I have taken this and copied all of the files into my UserBundle. I have changed the namespaces.
I have taken the graph manager from here: https://github.com/ikwattro/KwattroNeo4jOGMBundle
For the rest, I have followed the FOSUserBundle documentation.
Now, when I go to the registration form, all fields appear and I can fill in my preferred credentials. This works. After I click on submit I get redirected to the success page, on which an alert overlay is displayed:
An error occurred while loading the web debug toolbar (500: Internal
Server Error). Do you want to open the profiler?
If I then enter the profiler, I can see that I have successfully been authorized and logged in as the user that I just created. The data is also successfully saved in my neo4j database.
The problem now is that if I go to any other page of my Symfony project, I am logged in as Anonymous again. And If I go to the login page, the form is displayed correctly, but it always returns: Invalid credentials.
I am guessing, that there is something wrong with my sessions or my security?
This is my security.yml:
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
Neo4jUserBundle\Entity\User: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
fos_userbundle:
id: neo4j.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_provider: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
I dont know if this information can help or is relevant, but in the profiler under the Request right after registration (when the user is still authenticated correctly) this is the session information:
Session Metadata
Key Value
Created Tue, 21 Jul 15 17:27:34 +0200
Last used Tue, 21 Jul 15 17:27:34 +0200
Lifetime 0
Session Attributes
Key Value
_csrf/authenticate A_H4Ul1XHFYoxQdOirdmbBQRRCJ01Xh8EkGeC6Y7xw0
_csrf/registration OAXAXhfhcN6z0WekMN0fk8zg4ikk5uCCZBlvhy8DyVY
_security.last_username test
_security_main C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":444:{a:3:{i:0;N;i:1;s:4:"main";i:2;s:404:"a:4:{i:0;C:32:"neo4jProxyUserBundle_Entity_User":192:{a:9:{i:0;s:60:"$2y$13$e49oj61cdjk88kk040wg8exlwqVzbdQB5IVNG18Wqcbe.EW8KXi72";i:1;s:31:"e49oj61cdjk88kk040wg8kcc4cg40c4";i:2;s:4:"test";i:3;s:4:"test";i:4;b:0;i:5;b:0;i:6;b:0;i:7;b:1;i:8;i:66;}}i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}}";}}
Flashes
Key Value
success [0 => registration.flash.user_created, 1 => registration.flash.user_created, 2 => registration.flash.user_created, 3 => registration.flash.user_created, 4 => registration.flash.user_created, 5 => registration.flash.user_created, 6 => registration.flash.user_created, 7 => registration.flash.user_created, 8 => registration.flash.user_created]
Any help or hints would be appreciated.
UPDATE [21.07.2015]
I have now created a repository: https://github.com/JoranBeaufort/Neo4jUserBundle (I hope this works, the first time I have used GitHub)
I guess that there is something off with the session handling?
Another thing to point out is, that the dependency injection does not seem to do anything. I must be missing a few vital things.
It would be great to be able to offer a Neo4jUserBundle that works out of the box with FOSUserBundle and can be configured in the config file of the Symfony project. Great and vital would also be the authentication with the database (use username and password to connect to neo4j).
UPDATE [22.07.2015]
I have changed the bundlename and I think I have finally gotten the DependencyInjection to work. I'm not quite sure but I think I had a problem with how I named my classes.
I have also tried what you suggested with findUserById. I have written a controller which takes the route myapp.com/neo4juser/debug/finduserbyid/{id} and then uses the findUserById method to return the user. This is working. I have a user in my Neo4j-Database with an ID = 68 and an email=test#test.test. If I now enter myapp.com/neo4juser/debug/finduserbyid/68 the page is loaded displaying the right email of that user.
TWIG can be found here: https://github.com/JoranBeaufort/Neo4jUserBundle/blob/master/Resources/views/Debug/finduserbyid.html.twig
And the CONTROLLER here: https://github.com/JoranBeaufort/Neo4jUserBundle/blob/master/Controller/DebugController.php
The methods in the UserManager seem to be returning the desired objects.
Does this help in figuring out why the login does not work in any way? Does the serialization have anything to do with the error or the encryption type? Or could it be something to do with the CSRF? Any further hints?
UPDATE [23.07.2015]
When using in_memory as the provider and setting up an in_memory user, the login works. So now I have narrowed down the problem to the provider.
I am getting closer! Now the error in the dev.log file reads:
[2015-07-23 17:11:54] security.INFO: Authentication request failed. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException(code: 0): Bad credentials. at bla/vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php:73, Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException(code: 0): Username \"test\" does not exist. at bla/src/Neo4jUserBundle/Security/UserProvider.php:42)"} []
Important is the part `Username "test" does not exist.I am guessing this means that something is not working in the user provider. Can anyone spot what the problem might be? The Provider which I am calling can be found here: https://github.com/JoranBeaufort/Neo4jUserBundle/tree/master/Security
Ok. I opened a PR for some tweaks, but I couldn't get the stuff working.
The tweaks I've done are adding the possibility to define a user and password for via the neo4j_user config, and load the services.yml file in the DI extension.
When I register a user, it is well created in the database. However for fetching a user, after some debug, I can see that the underlying client (neo4jphp combined with neo4j-php-ogm) are using the legacy indexes and it is throwing some errors at this stage.
I can not help further except to tell you to try to not use an ogm in the beginning and try with raw cypher queries.
I'm afraid trying to update both libraries used can be difficult in a first instance.
I would say the problem, as of 27/12/2015, is that UserManager, at line 73, does not return the found user (is a void function). I have not tried it yet and can´t for a few days, and maybe it´s an answer not sought anymore, but I´m pretty sure that´s the problem.
IDEs won´t find it a problems due to the #return tag:
* Finds a user by username
*
* #param string $username
*
* #return UserInterface
*/
public function findUserByUsername($username)
{
$this->findUserBy(array('usernameCanonical' => $this->canonicalizeUsername($username)));
}
I'm trying to figure out why I can't set multiple user providers into a single provider.
Currently I'm configuring ACL. For the user providers I want to have a couple of "hard-coded" users and users which would be loaded from a database.
Reading the documentation it's stated that you're not required to have two user providers - one for the in_memory users and one for the database users. You should be able to combine them into a single user provider (which is what I'm trying to do).
The suggested configuration is:
security:
providers:
main_provider:
memory:
users:
foo: { password: test }
entity:
class: Acme\UserBundle\Entity\User,
property: username
My configuration is:
security:
providers:
main_provider:
memory:
users:
foo: { password: test }
entity:
class: Company\EntitiesBundle\Entity\User,
property: username
Unfortunately I get this exception:
InvalidConfigurationException: Invalid configuration for path "security.providers.main_provider": You cannot set multiple provider types for the same provider
If I, however, set two different providers and chain them, it works without problems.
I can't figure out why this would happen? It's clearly stated in the documentation - you can accomplish this even more easily by combining the two sources into a single provider.
What am I missing here?
Why don't you chain providers? The documentation that you're referring states that you can use multiple user providers "...by creating a new provider that chains the two together".
http://symfony.com/doc/current/book/security.html#using-multiple-user-providers
Each authentication mechanism (e.g. HTTP Authentication, form login, etc) uses exactly one user provider, and will use the first declared user provider by default. But what if you want to specify a few users via configuration and the rest of your users in the database? This is possible by creating a new provider that chains the two together.
Now, all authentication mechanisms will use the chain_provider, since it's the first specified. The chain_provider will, in turn, try to load the user from both the in_memory and user_db providers.
All you have to do is to setup a chain provider.
# app/config/security.yml
security:
providers:
main_provider:
chain:
providers: [memory_provider, entity_provider]
memory_provider:
memory:
users:
foo: { password: test }
entity_provider:
entity:
class: Company\EntitiesBundle\Entity\User
property: username
Which Symfony version are you using? If 2.0, in 2.0 documentation the configuration is slightly different:
# app/config/security.yml
security:
providers:
main_provider:
users:
foo: { password: test }
entity: { class: Acme\UserBundle\Entity\User, property: username }
Notice the memory key missing.
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.