Skip to content

[Security] New Password Policy listener #49821

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

Closed
wants to merge 1 commit into from

Conversation

Spomky
Copy link
Contributor

@Spomky Spomky commented Mar 26, 2023

Q A
Branch? 6.4
Bug fix? no
New feature? yes
Deprecations? no
Tickets none
License MIT
Doc PR symfony/symfony-docs#... [TODO]

This PR adds the possibility to define security policy on the password.
When at least one rule is defined, the PasswordPolicyListener will be called to verify the submitted password (on login) is acceptable.
The aim of this feature is to enforce the update of passwords.
Hereafter some use cases:

  • There were no rules when designing the app. Now password length should be at least 8 characters and short passwords should be changed.
  • The minimum password strength was 2 from a scale from 0 to 5. The IT Service decided to increase the minimum strength to 2.
  • The user database has leaked and is available on the internet. Leaked passwords are now prohibited.

The listener has a high priority (512, TBD) in order to be called before the user is fetched and the password is checked against the value in the database. The policy is only based on the plain text password value and not on the user state (e.g. last password update).

Todo:

  • Tests
  • Documentation

@Spomky Spomky requested review from wouterj and chalasr as code owners March 26, 2023 13:43
@carsonbot carsonbot added this to the 6.3 milestone Mar 26, 2023
@Spomky Spomky marked this pull request as draft March 26, 2023 13:44
@Spomky Spomky marked this pull request as ready for review March 26, 2023 20:09
@Spomky Spomky force-pushed the features/constraint-badge branch from ca3689a to 944f4d1 Compare March 26, 2023 20:14
@wouterj
Copy link
Member

wouterj commented Mar 27, 2023

Hi! Thanks for the series of security feature PRs!

However, I have some doubts about this particular feature:

A) If we ignore length constraints, all other constraints seem more suited in a registration process and not an authentication process. I can't recall seeing a website that tells me I have an unsafe password during authentication. For the basic constraints like length, I don't think we need to integrate with the Validator component (we already check on length btw)

B) The badges and passport listeners are created to centralize security sensitive code. I don't consider this validator a security critical feature. Also, given the validator component has its own simple high level API, I think using it directly in your own listener or authenticator instead of this badge is more clear of the intent.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 27, 2023

The use case we talked about with @Spomky made sense to me:

By having constraints as a badge, we can then easily implement login strategies where a password reset is enforced when ppl log in with a weak password (when some policy is strengthened).

@Spomky
Copy link
Contributor Author

Spomky commented Mar 27, 2023

Hi @wouterj,

Many thanks for your feedback and your advice.
The main idea is that I wanted to prevent login with a compromised password. At first sight, I duplicated the logic from the NotCompromised constraint, but after a short discussion with @nicolas-grekas we went to the idea of a badge that could verify the input against any other constraint.

@nicolas-grekas
Copy link
Member

@wouterj WDYT of our responses to your comment?

@wouterj
Copy link
Member

wouterj commented May 13, 2023

Sorry, I have been thinking about this PR the past weeks, but I didn't write down my thoughts here.

I think the "prevent login with a compromised password" idea is very interesting. This is becoming a more common practice, and is also recommended by NIST. However, I think I would like a very scoped badge rather than a generic badge like this one, to "limit" the usages, which makes it easier to make changes on later on and discourage bad practices (e.g. requiring a password to contain at least 1 special character, etc.).

Two other things I have in mind are:

  • We were cautious with storing plaintext passwords in badges (maybe overly so). E.g. the PasswordCredentials automatically clears the plaintext password after it is checked. So I'm a bit apprehensive with introducing another badge containing the plaintext password (I realize we did add an PasswordUpgradeBadge before though).
  • I'm not that fond of configuring a "password policy" (rules to determine if a user should change their password) in an authenticator (this also applies to PasswordUpgradeBadge). I would rather have the authenticator only add a PasswordCredentials and then have a PasswordPolicyListener that checks whether the password passes the policy or not. We might even add this as a new configuration option security.password_policy or the like to configure the exact rules that must be checked on the password.

@chalasr
Copy link
Member

chalasr commented May 14, 2023

I agree such concern would be better handled outside of the authenticator despite the "opportunistic upon login" aspect.

@wouterj
Copy link
Member

wouterj commented May 14, 2023

Just a dummy example, we might do something like this:

namespace Symfony\Component\PasswordHasher\Policy;

interface PasswordPolicy
{
    public function accept(string $plaintextPassword): bool;
}
security:
    password_policy:
        - NotCompromisedPolicy
        - 'App\Security\CustomPolicy'

A listener on CheckPassportEvent would then check a PasswordCredentials with these policies and block authentication before the CheckCredentialsListener is executed (necessary to prevent user enumeration).

We could extract the haveibeenpwned integration from the Validation component (into PasswordHasher?) and reuse it in both the constraint and policy. Maybe we implement some more policies, but we should make sure to only implement policies following best practices (e.g. from NIST). This last point is also why I'm not keen to integrate with the Validator component, this makes it way to easy to follow bad practices imho.

This would make it quite easy to enforce these policies, while still allowing users to implement not-best practice policies when necessary (e.g. a client of us requires us to have an "update password every x months" policy).

@Spomky
Copy link
Contributor Author

Spomky commented May 14, 2023

Many thanks for your feedback.

However, I think I would like a very scoped badge rather than a generic badge like this one, to "limit" the usages, which makes it easier to make changes on later on and discourage bad practices (e.g. requiring a password to contain at least 1 special character, etc.).

To be honest, this is what I usually do and that was used in recent workshops (badge and listener). It works fine and most of the code is mostly copied from the constraint.
If you are ok with copy/paste, I will change it in a dedicated badge

We were cautious with storing plaintext passwords in badges (maybe overly so)

OK noted. When the badge is marked as resolved, I will clear it.

I would rather have the authenticator only add a PasswordCredentials and then have a PasswordPolicyListener

OK. To me, this means this PasswordPolicyListener will be executed before any other listener and especially the PasswordCredentialsListener otherwise the password is cleared.

@Spomky
Copy link
Contributor Author

Spomky commented May 14, 2023

Just a dummy example, we might do something like this:

I understand the logic. Let me time for implementing it.

@wouterj
Copy link
Member

wouterj commented May 14, 2023

I understand the logic. Let me time for implementing it.

Cool, thanks for continueing this. Take it easy, the next future freeze is far away 🙂 and feel free to ping me if you need help getting this ready for 6.4.

@Spomky Spomky changed the title [Security] New ConstraintBadge and listener [Security] New Password Policy listener May 14, 2023
@Spomky
Copy link
Contributor Author

Spomky commented May 14, 2023

Cool, thanks for continueing this. Take it easy, the next future freeze is far away 🙂 and feel free to ping me if you need help getting this ready for 6.4.

🫢I took time to redo this PR (only the component). I concur with you, the design is much better now.

@Spomky Spomky force-pushed the features/constraint-badge branch from f1d8efd to f730b43 Compare May 14, 2023 15:04
Copy link
Member

@stof stof left a comment

Choose a reason for hiding this comment

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

the XSD file must be updated

@Spomky
Copy link
Contributor Author

Spomky commented May 15, 2023

Many thanks @stof. I addressed your comments.

I have an issue with the test that fails. It looks like the value of $config['password_policy'] is always an empty list. It is correctly populated in $configs, but seems to be replaced by the configuration processor call.
@wouterj suggested to change ->fixXmlConfig('password_policy', 'password_policy') in MainConfiguration but the result is always the same.
Do you have any clue on what is causing this issue? I certainly missed something stupid, but I cannot figure out what?

@stof
Copy link
Member

stof commented May 15, 2023

Well, I don't think using the same name for the singular and the plural works fine. I think you should use an actual plural name password_policies, which will solve this issue of using a single and plural with the same value.

@Spomky
Copy link
Contributor Author

Spomky commented May 15, 2023

That was my assumption, unfortunately the service list is still empty

@Spomky Spomky force-pushed the features/constraint-badge branch 3 times, most recently from c110187 to ea2516f Compare May 16, 2023 07:32
@Spomky Spomky force-pushed the features/constraint-badge branch from ea2516f to 1b69f77 Compare May 16, 2023 07:40
if (!$config['password_policies']) {
$container->removeDefinition('security.password_policy_listener');
} else {
$policyServices = array_map(static fn (string $id) => new Definition($id), $config['password_policies']);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is correct: entries in the list of policies cannot directly map to a service definition. that'd mean not being allowed to configure their constructor arguments for example.
Instead, we could use new Reference, but I'm wondering if we're not missing a DI tag to do the link, and use tagged_iterator()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. I modified the extension and my service definitions in the test.
The use of a tag is a good idea, but can we detect if a service is tagged in this method?

Copy link
Member

Choose a reason for hiding this comment

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

not in this method. We would have to do it in a compiler pass (or use tagged_iterator without checking things)

@Spomky Spomky force-pushed the features/constraint-badge branch 2 times, most recently from d77603d to b508f6d Compare May 16, 2023 08:48
@nicolas-grekas nicolas-grekas modified the milestones: 6.3, 6.4 May 23, 2023
@Spomky Spomky force-pushed the features/constraint-badge branch 2 times, most recently from 7a94c5b to ca558b9 Compare May 29, 2023 06:55
*/
public function __construct(
private readonly ValidatorInterface $validator,
private readonly array $constraints = []
Copy link
Member

Choose a reason for hiding this comment

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

missing comma at the end

/**
* @throws AuthenticationException
*/
public function verify(#[\SensitiveParameter] string $plaintextPassword): bool;
Copy link
Member

Choose a reason for hiding this comment

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

Could we stipulate that, instead of returning a bool, it must either throw a PasswordPolicyException or return void?

@Spomky Spomky force-pushed the features/constraint-badge branch from f8d02f3 to dcca81c Compare July 7, 2023 19:32
@Spomky
Copy link
Contributor Author

Spomky commented Jul 17, 2023

Closing as the concept should be revised in depth.

@Spomky Spomky closed this Jul 17, 2023
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.

7 participants