Skip to content
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

[Security] AuthenticatorManager to make "authenticators" first-class security #33558

Merged
merged 30 commits into from Apr 21, 2020

Conversation

wouterj
Copy link
Member

@wouterj wouterj commented Sep 11, 2019

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? no
Tickets -
License MIT
Doc PR tbd

The tl;dr

The old authentication listener + authentication provider system was replaced by a new "authenticator" system (similar to Guard authentication). All existing "auth systems" (e.g. form_login are now written as an "authenticator" in core).

Instead of each "authentication system" registering its own listener in the Firewall, there is now only one listener: AuthenticatorManagerListener

  • Firewall -> executes AuthenticatorManagerListener
  • AuthenticatorManagerListener -> calls AuthenticatorManager
  • AuthenticatorManager -> calls each authenticator

This PR contains no deprecations and the "new system" is marked as experimental. This allows to continue to develop the new Security system during the 5.x release cycle without disturbing Symfony users. In 5.4, we can deprecate "old" Security and remove it completely in 6.0.

Important Decisions

  • A) The new authentication manager - AuthenticatorManager - now dispatches 3 important "hook" events:

    • VerifyAuthenticatorCredentialsEvent: occurs at the point when a "password" needs to be checked. Allows us to centralize password checking, CSRF validation, password upgrading and the "user checker" logic.
    • LoginSuccessEvent: Dispatched after a successful authentication. E.g. used by remember me listener.
    • LoginFailedEvent: Dispatched after an unsuccessful authentication. Also used by remember me (and in theory could be used for login throttling).
  • B) getCredentials(), getUser() and checkCredentials() methods from old Guard are gone: their logic is centralized.
    Authenticators now have an authenticate(Request $request): PassportInterface method. A passport contains the user object, the credentials and any other add-in Security badges (e.g. CSRF):

    public function authenticate(Request $request): PassportInterface
    {
        return new Passport(
            $user,
            new PasswordCredentials($request->get('_password')),
            [
                new CsrfBadge($request->get('_token'))
            ]
        );
    }

    All badges (including the credentials) need to be resolved by listeners to VerifyAuthenticatorCredentialsEvent. There is build-in core support for the following badges/credentials:

    • PasswordCredentials: validated using the password encoder factory
    • CustomCredentials: allows a closure to do credentials checking
    • CsrfTokenBadge: automatic CSRF token verification
    • PasswordUpgradeBadge: enables password migration
    • RememberMeBadge: enables remember-me support for this authenticator
  • C) AuthenticatorManager contains all logic to authenticate
    As authenticators always relate to HTTP, the AuthenticatorManager contains all logic to authenticate. It has three methods, the most important two are:

    • authenticateRequest(Request $request): TokenInterface: Doing what is previously done by a listener and an authentication provider;
    • authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []) for manual login in e.g. a controller.
  • D) One AuthenticatorManager per firewall
    In the old system, there was 1 authentication manager containing all providers and each firewall had a specific firewall listener. In the new system, each firewall has a specific authentication manager.

  • E) Pre-authentication tokens are dropped.
    As everything is now handled inside AuthenticatorManager and everything is stored in the Security Passport, there was no need for a token anymore (removing lots of confusion about what information is inside the token).

    This change deprecates 2 authentication calls: one in AuthorizationChecker#isGranted() and one in AccessListener. These seem now to be mis-used to reload users (e.g. re-authenticate the user after you change their roles). This (some "way" to change a user's roles without logging them out) needs to be "fixed"/added in another PR.

  • F) The remember me service now uses all user providers
    Previously, only user providers of authentication providers listening on that firewall were used. This change is due to practical reasons and we don't think it is common to have 2 user providers supporting the same user instance. In any case, you can always explicitly configure the user provider under remember_me.

  • G) Auth Providers No Longer Clear the Token on Auth Failure
    Previously, authentication providers did $this->tokenStorage->setToken(null) upon authentication failure. This is not yet implemented: our reasoning is that if you've authenticated successfully using e.g. the login form, why should you be logged out if you visit the same login form and enter wrong credentials?
    The pre-authenticated authenticators are an exception here, they do reset the token upon authentication failure, just like the old system.

  • H) CSRF Generator Service ID No Longer Configurable
    The old Form login authentication provider allowed you to configure the CSRF generator service ID. This is no longer possible with the automated CSRF listener. This feature was introduced in the first CSRF commit and didn't get any updates ever since, so we don't think this feature is required. This could also be accomplished by checking CSRF manually in your authenticator, instead of using the automated check.

Future Considerations

  • Remove Security sub-components: Move CSRF to Symfony\Component\Csrf (just like mime); Deprecated Guard; Put HTTP + Core as symfony/security. This means moving the new classes to Symfony\Component\Security

  • Convert LDAP to the new system

  • This is fixed (and merged) by #36243 There is a need for some listeners to listen for events on one firewall, but not another (e.g. RememberMeListener). This is now fixed by checking the $providerKey. We thought it might be nice to introduce a feature to the event dispatcher:

    • Create one event dispatcher per firewall;
    • Extend the kernel.event_subscriber tag, so that you can optionally specify the dispatcher service ID (to allow listening on events for a specific dispatcher);
    • Add a listener that always also triggers the events on the main event dispatcher, in case you want a listener that is listening on all firewalls.
  • Drop the AnonymousToken and AnonymousAuthenticator: Anonymous authentication has never made much sense and complicates things (e.g. the user can be a string). For access control, an anonymous user has the same meaning as an un-authenticated one (null). This require changes in the AccessListener and AuthorizationChecker and probably also a new Security attribute (to replace IS_AUTHENTICATED_ANONYMOUSLY). Related issues: #34909, #30609

How to test

  1. Install the Symfony demo application (or any Symfony application)
  2. Clone my Symfony fork (git clone git@github.com:wouterj/symfony) and checkout my branch (git checkout security/deprecate-providers-listeners)
  3. Use the link utility to link my fork to the Symfony application: /path/to/symfony-fork/link /path/to/project
  4. Enable the new system by setting security.enable_authenticator_manager to true

@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch 2 times, most recently from 8960e3a to b485767 Compare Sep 11, 2019
@nicolas-grekas nicolas-grekas added this to the next milestone Sep 12, 2019
@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch from b485767 to 93ce631 Compare Sep 15, 2019
@Nyholm
Copy link
Member

@Nyholm Nyholm commented Sep 15, 2019

Just note:
A user should also be able to use the provider/listener/handler/factory implementation. I love Guard but sometimes it is not enough to work with my super special authentication.

I recently had a scenario where I get a user if passed with an authenticated jwt token. I should create a user entity if it missing (by using data in auth0), if email is valid I create an authenticated token, if not, I will ask the user to login (with different authenticator).

I was not able to write a nice solution with guard for this scenario, so I had to use the “low level code”.

I’m all for promoting guard and trying to make it better, but I think it would be use the more flexible solution when needed.

@Koc
Copy link
Contributor

@Koc Koc commented Sep 17, 2019

Good movement, but AFIK it is impossible to define custom config nodes with guards. Should we add method similar to addConfiguration(NodeDefinition $builder) inside GuardFactoryInterface like already done in SecurityFactoryInterface?

@wouterj
Copy link
Member Author

@wouterj wouterj commented Sep 21, 2019

I’m all for promoting guard and trying to make it better, but I think it would be use the more flexible solution when needed.

Hmm, interesting. Our assumption has always been that Guard is not less flexible than the current listener-provider solution. Guard only combines them into one class, which makes it more difficult to reuse partial systems (i.e. the DaoAuthenticationProvider).

I do not fully follow your requirements, but it would be interesting to see whether or not Guard (or a modification in Guard) could have fixed your requirements.


For now, I would say it doesn't matter as this PR is not about deprecating the old system. It's only introducing a new system next to it, to allow experimenting (and discovering exactly cases like you mentioned). For that reason, it's maybe a good idea to mark the new classes as @experimental (or do we only mark complete components as experimental?)

@fabpot @nicolas-grekas @weaverryan what should we do to get this into the next release (probably not 4.4, but 5.0?)

@wouterj
Copy link
Member Author

@wouterj wouterj commented Sep 21, 2019

Good movement, but AFIK it is impossible to define custom config nodes with guards. Should we add method similar to addConfiguration(NodeDefinition $builder) inside GuardFactoryInterface like already done in SecurityFactoryInterface?

In order to be usable a class should as of now implement both interfaces, so the addConfiguration() method is added by the other interface. For clarity, we can add the methods to the new interface as well.

@fabpot
Copy link
Member

@fabpot fabpot commented Sep 21, 2019

Making it experimental means that it cannot be part of 4.4. But it can be part of 5.0.

@symfony symfony deleted a comment from wouterj Oct 10, 2019
@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch from 93ce631 to b21b514 Compare Dec 13, 2019
@wouterj wouterj changed the base branch from 4.4 to master Dec 14, 2019
@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch 3 times, most recently from 959fbbf to dc68936 Compare Jan 26, 2020
@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch 2 times, most recently from 2b2d17f to 7799078 Compare Feb 12, 2020
@wouterj wouterj force-pushed the security/deprecate-providers-listeners branch 2 times, most recently from 6ba8ec6 to 028baf0 Compare Mar 1, 2020
fabpot
fabpot approved these changes Apr 21, 2020
@fabpot
Copy link
Member

@fabpot fabpot commented Apr 21, 2020

Thank you @wouterj.

@fabpot fabpot merged commit 1abdcbb into symfony:master Apr 21, 2020
2 of 3 checks passed
@wouterj wouterj deleted the security/deprecate-providers-listeners branch Apr 21, 2020
@wouterj
Copy link
Member Author

@wouterj wouterj commented Apr 21, 2020

Fyi, @noniagriconomie, I like your review and we've not forgotten your comments. However, we wanted to finally get this one in master, so I'll submit a PR fixing your comments later this day

@noniagriconomie
Copy link
Contributor

@noniagriconomie noniagriconomie commented Apr 21, 2020

@wouterj understood, good to have the security part upgraded :)

chalasr added a commit that referenced this issue Apr 21, 2020
This PR was merged into the 5.1-dev branch.

Discussion
----------

[Security] Fix missing nullable in CsrfTokenBadge

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       |
| License       | MIT
| Doc PR        |

Related to #33558 I noticed a minor 🤏  bug with the method return-type.

Commits
-------

5cb633c Update CsrfTokenBadge.php
wouterj added a commit to wouterj/symfony that referenced this issue Apr 21, 2020
wouterj added a commit to wouterj/symfony that referenced this issue Apr 21, 2020
chalasr added a commit that referenced this issue Apr 21, 2020
…wouterj)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[Security] Apply left-over review comments from #33558

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | n/a

This applies the review comments of @noniagriconomie in #33558. It's mostly doc fixes and one extra security-safeguard by resetting the plaintext password early (similair to what is done in `PasswordCredentials`).

Commits
-------

be3a9a9 Applied left-over review comments from #33558
fabpot added a commit that referenced this issue Apr 30, 2020
…ultiple authenticators (wouterj)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[Security] Require entry_point to be configured with multiple authenticators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | tbd

See @weaverryan's comment at #33558 (comment):

> I have it on my list to look at the entrypoint stuff more closely. But my gut reaction is this: let's fix them (or try to... or maybe in a PR after this) :). What I mean is this:
>
> -    It's always been confusing that your firewall may have multiple auth mechanisms that have their own "entry point"... and one is chosen seemingly at random :). I know it's not random, but why does the entrypoint from `form_login` "win" over `http_basic` if I have both defined under my firewall?
>
> -    Since we're moving to a new system, why not throw an exception the _moment_ that a firewall has multiple entrypoints available to it. Then we _force_ the user to choose the _one_ entrypoint that should be used.

---

**Before** (one authenticator)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      form_login: ...

# form login is your entry point
```

**After**
Same as before

---

**Before** (multiple authenticators)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...

# for some reason, FormLogin is now your entry point! (config order doesn't matter)
```

**After**
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: form_login
```

---

**Before** (custom entry point service)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: App\Security\CustomEntryPoint
```

**After**
Same as before

Commits
-------

7e86169 [Security] Require entry_point to be configured with multiple authenticators
symfony-splitter pushed a commit to symfony/security-bundle that referenced this issue Apr 30, 2020
…ultiple authenticators (wouterj)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[Security] Require entry_point to be configured with multiple authenticators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | tbd

See @weaverryan's comment at symfony/symfony#33558 (comment):

> I have it on my list to look at the entrypoint stuff more closely. But my gut reaction is this: let's fix them (or try to... or maybe in a PR after this) :). What I mean is this:
>
> -    It's always been confusing that your firewall may have multiple auth mechanisms that have their own "entry point"... and one is chosen seemingly at random :). I know it's not random, but why does the entrypoint from `form_login` "win" over `http_basic` if I have both defined under my firewall?
>
> -    Since we're moving to a new system, why not throw an exception the _moment_ that a firewall has multiple entrypoints available to it. Then we _force_ the user to choose the _one_ entrypoint that should be used.

---

**Before** (one authenticator)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      form_login: ...

# form login is your entry point
```

**After**
Same as before

---

**Before** (multiple authenticators)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...

# for some reason, FormLogin is now your entry point! (config order doesn't matter)
```

**After**
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: form_login
```

---

**Before** (custom entry point service)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: App\Security\CustomEntryPoint
```

**After**
Same as before

Commits
-------

7e861698e7 [Security] Require entry_point to be configured with multiple authenticators
fabpot added a commit that referenced this issue May 3, 2020
…m (wouterj)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[Security] Removed anonymous in the new security system

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | tbd

This was one of the "Future considerations" of #33558:

> Drop the AnonymousToken and AnonymousAuthenticator: Anonymous authentication has never made much sense and complicates things (e.g. the user can be a string). For access control, an anonymous user has the same meaning as an un-authenticated one (null). This require changes in the AccessListener and AuthorizationChecker and probably also a new Security attribute (to replace IS_AUTHENTICATED_ANONYMOUSLY). Related issues: #34909, #30609

This new experimental system is probably a once-in-a-lifetime change to make this change. @weaverryan and I have had some brainstorming about this. Some reasons why we think it makes 100% sense to do this change:

* From a Security perspective, **a user that is not authenticated is similar to an "unknown" user**: They both have no rights at all.
* **The higher level consequences of the AnonymousToken are confusing and inconsistent**:
  * It's hard to explain people new to Symfony Security that not being logged in still means you're authenticated within the Symfony app
  * To counter this, some higher level APIs explicitly mark anonymous tokens as not being authenticated, see e.g. the [`is_authenticated()` expression language function](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php#L33-L37)
  * The anonymous authentication resulted in the `IS_AUTHENTICATED` security attribute being removed from #35854, as there was no clear consensus on what its meaning should be
* **Spring Security, which is where this originated from, makes Anonymous a very special case**:

  > Finally, there is an AnonymousAuthenticationFilter, which is chained after the normal authentication mechanisms and automatically adds an AnonymousAuthenticationToken to the SecurityContextHolder if there is no existing Authentication held there.
  >
  > Note that there is no real conceptual difference between a user who is “anonymously authenticated” and an unauthenticated user. Spring Security's anonymous authentication just gives you a more convenient way to configure your access-control attributes. Calls to servlet API calls such as getCallerPrincipal, for example, will still return null even though there is actually an anonymous authentication object in the SecurityContextHolder.
* Symfony uses AnonymousToken much more than "just for convience in access-control attributes". **Removing anonymous tokens allows us to move towards only allowing `UserInterface` users**: #34909

---

Removing anonymous tokens do have an impact on `AccessListener` and `AuthorizationChecker`. These currently throw an exception if there is no token in the storage, instead of treating them like "unknown users" (i.e. no roles). See #30609 on a RFC about removing this exception. We can also see e.g. the [Twig `is_granted()` function explicitly catching this exception](https://github.com/symfony/symfony/blob/master/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php#L37-L52).

* **To make the changes in `AccessListener` and `AuthorizationChecker` BC, a flag has been added - default enabled - to throw an exception when no token is present** (which is automatically disabled when the new system is used). In Symfony 5.4 (or whenever the new system is no longer experimental), we can deprecate this flag and in 6.0 we can never throw the exception anymore.
* **`anonymous: lazy` has been deprecated in favor of `{ anonymous: true, lazy: true }`** This fixes the dependency on `AnonymousFactory` from the `SecurityExtension` and allows removing the `anonymous` option.
* **Introduced `PUBLIC_ACCESS` Security attribute** as alternative of `IS_AUTHENTICATED_ANONYMOUSLY`. Both work in the new system, the latter only triggers a deprecation notice (but may be usefull to allow switching back and forth between old and new system).

cc @javiereguiluz you might be interested, as I recently talked with you about this topic

Commits
-------

ac84a6c Removed AnonymousToken from the authenticator system
@nicolas-grekas nicolas-grekas removed this from the next milestone May 4, 2020
@nicolas-grekas nicolas-grekas added this to the 5.1 milestone May 4, 2020
@fabpot fabpot mentioned this pull request May 5, 2020
@curry684 curry684 mentioned this pull request Jun 15, 2020
@AkashicSeer

This comment has been minimized.

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.

None yet