Shift3 / boilerplate-client-react

The Bitwise standard starting point for new React web projects.
7 stars 10 forks source link

[Bug] Activation Link process still accessible after activating an account #767

Open anaps-qa opened 1 year ago

anaps-qa commented 1 year ago

Describe the bug

After having activated an account via an activation link sent to their email, the user Is able to go through the activation process again just by following the same link, including adding a new password, and receive a message of a successful activation. However, if the user tries to log back in with the new password, it fails. So while they did not activate the account again, it still creates a "false" activation process because of how far the user is able to follow the steps for activation.

Expected behavior

The user shouldn't be able to activate or even appear to activate an already existing account with its original activation link

To Reproduce

Steps to reproduce the behavior:

  1. Sign in as an admin user
  2. Add a user and send the activation link to the user's email
  3. Access the activation link and go through the process of activating the new user account
  4. Notice you receive a confirmation stating the account has been activated
  5. Log in to the new user account
  6. Log out and return to the activation email
  7. Click on the activation link again
  8. Follow the steps to activate the account once more, this time using a different password
  9. Notice you receive a confirmation stating the account has been activated
  10. Try logging in with the new password and notice that fails
  11. Login with the original activation password

Screenshots/Videos

https://github.com/Shift3/boilerplate-client-react/assets/103682123/50df34c3-9ed4-42f7-b97a-e5fa765d388d

Devices/Browsers:

Notes

anaps-qa commented 1 year ago

Similarly, an expired activation link is still accessible, allowing the user to go through the "false" activation process once again.

anaps-qa commented 1 year ago

Additionally, a resending a new activation link does not invalidate the original activation link.

coreyshuman commented 1 year ago

I'll use this issue to capture my notes on the subject. We are using the Djoser library to handle user auth, which includes the account activation, password reset, and email change workflows.

The Djoser email class generates the token and adds it to the email payload. https://github.com/sunscrapers/djoser/blob/bacf083b212bda1bf6eab2bf3f1c17e3dd5ba8ad/djoser/email.py#L17

This is using the Django built-in PasswordResetTokenGenerator to create the token. https://github.com/django/django/blob/ea8cbca579cc6742e119747fc1eb6ecf90638bce/django/contrib/auth/tokens.py#L40

Hash invalidation is explained here: https://github.com/django/django/blob/ea8cbca579cc6742e119747fc1eb6ecf90638bce/django/contrib/auth/tokens.py#L100

        Hash the user's primary key, email (if available), and some user state
        that's sure to change after a password reset to produce a token that is
        invalidated when it's used:
        1. The password field will change upon a password reset (even if the
           same password is chosen, due to password salting).
        2. The last_login field will usually be updated very shortly after
           a password reset.
        Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
        invalidates the token.

Therefore token invalidation is limited to changes to email, password, or last_login. An improvement would be to also include a security_hash on the user model which can be updated on any auth requests, such as how .NET IdentityFramework does it.

Furthermore, token validation occurs in the UidAndTokenSerializer class here: https://github.com/sunscrapers/djoser/blob/bacf083b212bda1bf6eab2bf3f1c17e3dd5ba8ad/djoser/serializers.py#L187

This is the base class for ActivationSerializer, which is defined as the serializer for the activate action here, if not overridden in settings: https://github.com/sunscrapers/djoser/blob/bacf083b212bda1bf6eab2bf3f1c17e3dd5ba8ad/djoser/views.py#L100

Therefore the act of replacing a serializer on any auth endpoints could render token validation silently disabled, as could any conf changes to serializers for related functions.

My takeaways so far are that token invalidation is not easily customizable, application security verification of our overridden Djoser functions is very difficult, and we should be very hesitant to any serializer changes without a comprehensive understanding of the underlying library code.

coreyshuman commented 1 year ago

The Djoser source code uses manual updates to last_login to invalidate old tokens: https://github.com/sunscrapers/djoser/blob/bacf083b212bda1bf6eab2bf3f1c17e3dd5ba8ad/djoser/views.py#L252