Symfony2 + FOSUserBundle: http_basic authentication works, http_digest fails - symfony

I'm configuring a Symfony2 application using the FOSUserBundle to use http_digest authentication. This is with Symfony 2.1.0-BETA2.
In security.yml, I am simply switching out http_basic for http_digest and adding the required key property. All else remains the same.
Relevant configuration that works:
firewalls:
main:
pattern: ^/
anonymous: ~
form_login: false
provider: fos_user_bundle
http_basic:
realm: "Example Realm"
Relevant configuration that does not work:
firewalls:
main:
pattern: ^/
anonymous: ~
form_login: false
provider: fos_user_bundle
http_digest:
realm: "Example Realm"
key: "%secret%"
As you can see, the only difference is switching out http_basic for http_digest. Changing the value of the key property appears to make no difference.
When using an in_memory provider, http_digest works just fine. This matter is only present when using the fos_user_bundle provider.
By working, I mean that with when using http_basic, the valid user credentials are accepted. When using http_digest the same valid user details are not accepted and the browser's default http authentication prompt is re-displayed.
Between security configuration changes, I clear both the dev and prod caches, empty the browser cache and close the browser.
Is there something critical I'm missing from the configuration?
Update
I've logged a successful http_basic attempt and an unsuccessful http_digest attempt and diffed the logs.
Both logs are identical up to and including where Doctrine logs the SQL query used for authentication.
Following the authentication query in the http_digest log are the lines:
security.DEBUG: Expected response: '3460e5c31b09d4e8872650838a0c0f1a' but received: '5debe5d0028f65ae292ffdea2616ac19'; is AuthenticationDao returning clear text passwords? [] []
security.INFO: exception 'Symfony\Component\Security\Core\Exception\BadCredentialsException' with message 'Incorrect response' in /home/jon/www/local.app.simplytestable.com/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php:105
Passwords, using the FOSUserBundle, are salted and hashed.
I'd like to ascertain whether this matter arises due a misconfiguration on my part or whether it's a bug in the FOSUserBundle.

HTTP digest authentication works, in short, by comparing hashes calculated from values including the username, realm, password and various nonse and replay-avoidance values.
The plaintext password is required on both the client and server side to generate the same hashes.
FOSUserBundle salts and hashes passwords.
The server-side hash generated within the Symfony\Component\Security\Http\Firewall\DigestAuthenticationListener class will be passed a hashed not plaintext password and so can never generate the correct hash for comparison.

Related

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

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.

How to automatically fill in required parameter in Symfony

So, all my routings looks like this
login: /studio/{uuid}/login
logout: /studio/uuid/logout
programs: /studio/{uuid}/programs
levels: /studio/{uuid}
and everytime I access the link via {{ url('login|orwhathever') }} I have to pass uuid. Because all my links will contain uuid I would like to automatize that.
Of course, previously I did not have uuid in the URL and it was just stored in the session if I needed it. But I recently noticed a problem when session expires it supposed to generate login url but it contains uuid parameter and because this parameter is NULL it will throw an error.
So either it must be fixed by providing {uuid} to each url or
somehow pass uuid to Symfony logout method in security.yml
frontoffice_area:
pattern: ^/
anonymous: ~
simple_form:
authenticator: security.user_authenticator
login_path: login #this needs uuid from somewhere
check_path: validate

dynamic parameters in security.yml

The isssue I am trying to address is to automatically redirect my application to the user entered url after login. Since I have a centralized authentication server I cant user 'HTTP_REFERER' as it always returns null when am being transferred to the authentication server.
My solution is to use the security.yml in my application server to pass the redirection url as url parameter. I have implemented that as follows,
parameters.php
$container->setParameter('referer', $_SERVER['REQUEST_URI']);
security.yml
secured_area:
pattern: ^/
stateless: true
form_login:
login_path: %accounts_host%/signin?referer=%referer%
simple_preauth:
authenticator: app.security.authenticator
But my issue is that the referer parameter in security.yml is always static. It always gives the first application url I type in. Lets say if i type www.appserver.com/product/1, the next time if I type in www.appserver.com/product/200 the referer will always return www.appserver.com/product/1 in the authenitcation server.
However, If I do a print_r($_SERVER['REQUEST_URI']); exit(); in the parameters.php the value changes for my each request.
I was at this for quite some time and I am very lost at this point. Any help on getting this referer value dynamically on the security.yml would be really appreciated. thanks :)

Symfony2: Unable to login successfully with two firewalls using two user providers

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.

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