Skip to content

[Security] Load the user before pre/post auth checks when needed #26788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 4, 2018

Conversation

chalasr
Copy link
Member

@chalasr chalasr commented Apr 4, 2018

Q A
Branch? 2.8
Bug fix? yes
New feature? n/a
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #26775
License MIT
Doc PR n/a

@@ -45,6 +46,11 @@ public function authenticate(TokenInterface $token)
}

$user = $authToken->getUser();

if (!$user instanceof UserInterface) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the user is Anon.? It will fail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it will not. instanceof handles strings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will call this: $user = $this->userProvider->loadUserByUsername('Anon.');, which will fail to retrieve the user and result in NULL

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, but loadUserByUsername() will throw a UsernameNotFoundException. Should we throw a specific exception before?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will also handle null: https://3v4l.org/Xrt8u

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @return UserInterface does enforce it to me, it doesn't allow null

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except that people can just do return null; and it won't fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, if they don't respect the contract of the interface, they enter unknown land. Things might break badly at any time (and the typehint error on the next call is such a failure), and the failure can even change in any Symfony patch release. We don't guarantee any backward compatibility for code not respecting the interface contract (and yes, any new API added in Symfony 4.1+ will use return types to enforce it better)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I fully agree, sadly (especially older implementations) are often based of "reverse engineered" authenticators, due to lack of better (SF2.0~2.6). Thus, I can't fully blame developers for making this "mistake".

$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same check added for consistency

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Apr 4, 2018

wrong target branch? (fixed)

@nicolas-grekas nicolas-grekas changed the base branch from master to 2.8 April 4, 2018 12:02
@chalasr chalasr force-pushed the simple-auth-user-checks branch 3 times, most recently from f40ce0f to 21f6216 Compare April 4, 2018 12:59
@chalasr chalasr force-pushed the simple-auth-user-checks branch from 21f6216 to c318306 Compare April 4, 2018 13:01
$user = $this->userProvider->loadUserByUsername($user);

if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
Copy link
Contributor

@dmaicher dmaicher Apr 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also call setToken for this one? forget this comment 😉

@nicolas-grekas
Copy link
Member

Thank you @chalasr.

@nicolas-grekas nicolas-grekas merged commit c318306 into symfony:2.8 Apr 4, 2018
nicolas-grekas added a commit that referenced this pull request Apr 4, 2018
…needed (chalasr)

This PR was merged into the 2.8 branch.

Discussion
----------

[Security] Load the user before pre/post auth checks when needed

| Q             | A
| ------------- | ---
| Branch?       | 2.8
| Bug fix?      | yes
| New feature?  | n/a
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #26775
| License       | MIT
| Doc PR        | n/a

Commits
-------

c318306 [Security] Load the user before pre/post auth checks when needed
@chalasr chalasr deleted the simple-auth-user-checks branch April 4, 2018 13:34
This was referenced Apr 6, 2018
@aschempp
Copy link
Contributor

Unfortunately, this implementation is broken as well.

  • If $user is not UserInterface, the new implementation calls the UserProviderwith the$user, which might be *an object implementing a __toString` method*.
  • The UserProvider only expects a string to be the argument, so an object would be wrong.
  • Also, the implementation enforces the return value of the user provider to be a UserInterface. That again is incorrect, as the return value still could be an object implementing a __toString method.

The only viable solution is to only run the UserChecker if $user is instanceof UserInterface.

@chalasr
Copy link
Member Author

chalasr commented Apr 11, 2018

The UserProvider only expects a string to be the argument, so an object would be wrong.

Right, casting to string beforehand should be enough to fix that.

Also, the implementation enforces the return value of the user provider to be a UserInterface. That again is incorrect, as the return value still could be an object implementing a __toString method.

No, it must return an instance of UserInterface.

I'll submit a patch tomorrow, thanks for reporting.

@aschempp
Copy link
Contributor

That will not solve the problem, at least not for us (Contao CMS).

For BC reasons, our current LTS version (Contao 4.4) is using a custom Authenticator registered in the firewall config. Our legacy user classes do not implement the UserInterface. We also don't actually load the user by username, but by firewall scope. Based on the firewall name, users are loaded from legacy classes.

It's all rather complex, but worked perfectly fine until now. Our legacy user class is stored in the token, and thanks to the __toString method the actual username is shown in the profiler etc.


Now, casting the user object to a string would not work, because the object's username would not be suitable for our user provider.

I also think the whole approach is flawed. The code is essentially calling the user provider again to get a user. It did not get one in the first place, why would it get one if you call it again? Even worse, we're cancelling the authentication if the user provider does not return a UserInterface on the second attempt, but that case is absolutely valid! There does not need to be a UserInterface, the user can always just be a string and be fully authenticated.

@linaori
Copy link
Contributor

linaori commented Apr 12, 2018

the user can always just be a string and be fully authenticated.

And this is one of the reasons why the User object should be restricted as data object and be limited to the security internal behavior, never be exposed to the outside.

While I agree that the current approach is not really ideal, I think that the Simple* variants are not really the way to go anymore. I know it doesn't fix your problem, but Guard has better boundries and restricts far more, leading to less edge-cases and bugs.

I'm curious though, why a custom authenticator to eventually return an anonymous user? I personally think that you should not be altering the Anonymous behavior that Symfony has. So far I've seen all issues that occur with this change, related to anonymous users and strings in custom authenticators.

@aschempp
Copy link
Contributor

I totally agree Guard would have been the way. We implemented this in Symfony 2.6 or 2.7, so there was no Guard. Since Contao 4.5 (latest) we switched to full Symfony authentication (which is kinda complex as well due to all the legacy stuff), but we're not using Simple Authentication anymore and our users actually do implement UserInterface now.

Still, I think there are valid use cases for not having a UserInterface. I did some work with Shibboleth a while ago, where the actual authentication was done through the Apache server (mod_shibboleth). So you simply have a server variable that gives you a username. And for some cases that might just be enough to have a IS_FULLY_AUTHENTICATED user.

Regardless of examples, I think my last paragraph was still valid. It does not make sense to try to load a user again from the user provider. The authenticator is responsible for that, and if there is no UserInterface then we just can't use the UserChecker.

@linaori
Copy link
Contributor

linaori commented Apr 12, 2018

Still, I think there are valid use cases for not having a UserInterface.

I agree, and I'm 100% sure that we can make Symfony never expose the UserInterface, by storing only authentication (identification) information in the token, rather than the user object. It would simplify a lot.

@chalasr
Copy link
Member Author

chalasr commented Apr 12, 2018

Point is the contract says UserInterface, you're on your own as soon as you return something else (see #26788 (comment)). That is consistent with the rest of the codebase which has the same kind of checks everywhere else (e.g. for guard authenticator's getUser() return value).
Considering that our BC promise doesn't cover its use case, can't Contao just make its user classes implement UserInterface?

The authenticator is responsible for that, and if there is no UserInterface then we just can't use the UserChecker.

That means we would have a different behavior when passing a username (or object with __toString() returning the username), not sure it's desirable.

At this stage I suggest to open a new issue to discuss about what can/should be done.

@aschempp
Copy link
Contributor

Considering that our BC promise doesn't cover its use case, can't Contao just make its user classes implement UserInterface?

I guess we could. It would be wonky as we're implementing UserInterface so the user can be passed to UserChecker which does nothing because the user is not AdvancedUserInterface. But yet, it would work… 😂

@linaori
Copy link
Contributor

linaori commented Apr 13, 2018

@aschempp That should be solved if this RFC would be implemented: #26348

Add a NullUserChecker, which features 0 checks. In 5.0 this would become the default UserChecker in the configuration.

@aschempp
Copy link
Contributor

Unfortunately, there are more issues with the implementation. Our SimplePreAuthenticatorInterface implementation returns an anonymous token if there is no user logged in. This again will result in an exception because is now enforcing a UserInterface 😞

@chalasr
Copy link
Member Author

chalasr commented Apr 14, 2018

@aschempp see #26871

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants