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,
Here is the code when the framework refreshes the user (can be found in \Symfony\Component\Security\Http\Firewall\ContextListener):
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 anUnsupportedUserException
, this means that the provider isn't responsible for the suppliedUserInterface
. 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 suppliedUserInterface
, 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. AndInMemoryUserProvider
is responsible for theUserInterface
implemented bySymfony\Component\Security\Core\User\User
.In the frontend, "user" is in fact authenticated successfully. However, in the user refresh attempt,
UserInterface
because it is also an instance ofSymfony\Component\Security\Core\User\User
.UsernameNotFoundException
.refreshUser()
routine won't bother to try with next provider becauseUsernameNotFoundException
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
andUser
classes and change therefreshUser()
method to check against the copiedUser
class, so that the frontend and backend user provider uses different user classes and won't clash.