-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
[Security] AuthenticatorManager to make "authenticators" first-class security #33558
Conversation
8960e3a
to
b485767
Compare
src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php
Outdated
Show resolved
Hide resolved
b485767
to
93ce631
Compare
Just note: 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. |
Good movement, but AFIK it is impossible to define custom config nodes with guards. Should we add method similar to |
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 @fabpot @nicolas-grekas @weaverryan what should we do to get this into the next release (probably not 4.4, but 5.0?) |
In order to be usable a class should as of now implement both interfaces, so the |
Making it experimental means that it cannot be part of 4.4. But it can be part of 5.0. |
93ce631
to
b21b514
Compare
959fbbf
to
dc68936
Compare
src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
Show resolved
Hide resolved
2b2d17f
to
7799078
Compare
src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
Outdated
Show resolved
Hide resolved
src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
Outdated
Show resolved
Hide resolved
6ba8ec6
to
028baf0
Compare
Thank you @wouterj. |
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 |
@wouterj understood, good to have the security part upgraded :) |
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) 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
…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
…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
…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
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
-> executesAuthenticatorManagerListener
AuthenticatorManagerListener
-> callsAuthenticatorManager
AuthenticatorManager
-> calls each authenticatorThis 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()
andcheckCredentials()
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):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 factoryCustomCredentials
: allows a closure to do credentials checkingCsrfTokenBadge
: automatic CSRF token verificationPasswordUpgradeBadge
: enables password migrationRememberMeBadge
: enables remember-me support for this authenticatorC)
AuthenticatorManager
contains all logic to authenticateAs 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 SecurityPassport
, 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 inAccessListener
. 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 assymfony/security
. This means moving the new classes toSymfony\Component\Security
Convert LDAP to the new system
This is fixed (and merged) by [Security] Refactor logout listener to dispatch an event instead #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 thekernel.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
andAnonymousAuthenticator
: 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 theAccessListener
andAuthorizationChecker
and probably also a new Security attribute (to replaceIS_AUTHENTICATED_ANONYMOUSLY
). Related issues: [Security] deprecate non UserInterface users #34909, [Security][RFC] Exception thrown on call to isGranted without token #30609