-
Notifications
You must be signed in to change notification settings - Fork 22
fix:bug in third party authentication email and name update #382
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
fix:bug in third party authentication email and name update #382
Conversation
robrap
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you.
- Please read https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0051-bp-conventional-commits.html for how to format your commit messages. You can use "fix: ...", or "fixup! ..." as you add new commits to the PR (that can be squashed at the end).
- You have "... and name update" in the PR title. Is there supposed to be something about this? Also, do we need to look into whether fullname is updated in another step that might have the same issue?
- Are there unit tests for
update_emailyou can enhance, or add?
requirements/ci.txt
Outdated
| # -r requirements/ci.in | ||
| virtualenv==20.30.0 | ||
| # via tox | ||
| edx-django-utils==8.0.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate you looking into CI failures. Definitely worth looking into before (or in parallel) with a review, as long as you let the reviewer know that you are aware of the failures and are looking into it.
- See https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0018-bp-python-dependencies.html for some help. It is a decision doc, and not a how-to, so it isn't as brief as it could be.
- This should be moved to
base.inwithout a version. - There is a github action for upgrading: https://github.com/openedx/auth-backends/actions/workflows/upgrade-python-requirements.yml. You might not be able to run this. Do you have it on your fork, and can you run there? If not, you can run
make upgradefrom a container (which doesn't always work, because it may not match Python exactly, or other small differences).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving this unresolved until part 3 is handled. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do you see a "Resolve" button on this conversation in github, and are you able to mark as resolved?
- Are you able to give me write access on your private fork, and does that give me the capability to resolve conversations here?
Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I am able to see "Resolve" button and can mark it as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Akanshu-2u: You do not need to do anything on this PR, but when I had written all of the comments like "This can be marked resolved.", it would have helped me if you had actually marked that am as resolved. I thought you couldn't, but I was wrong. We can try this on the new PR. Or, you can give me write access on your fork, and maybe I can resolve myself. Thanks.
| from django.contrib.auth import get_user_model | ||
| import logging | ||
| from edx_django_utils.monitoring import set_custom_attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing you might have sorting issue here. Not sure if isort is running. I'm not sure if what I added below is correct, but it may be closer.
| from django.contrib.auth import get_user_model | |
| import logging | |
| from edx_django_utils.monitoring import set_custom_attribute | |
| import logging | |
| from django.contrib.auth import get_user_model | |
| from edx_django_utils.monitoring import set_custom_attribute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| Before updating the email, verify that the username in details matches the user's username. | ||
| If usernames don't match, log a warning and don't update the email. | ||
| Custom attributes are set to track username mismatches. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate good comment, but maybe these details could just be left to well commented code below. This is a judgement call. What you have is not wrong. I'd personally go lighter here. The more text, the more comments can drift from code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| user_username = getattr(user, 'username', None) | ||
|
|
||
| # Check if usernames match | ||
| username_match = details_username and user_username and details_username == user_username |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be the same? You already know these have a value, even if None.
| username_match = details_username and user_username and details_username == user_username | |
| username_match = details_username == user_username |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| username_match = details_username and user_username and details_username == user_username | ||
|
|
||
| # Set custom attribute for all cases | ||
| set_custom_attribute('update_email.username_mismatch', not username_match) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please follow openedx/edx-django-utils#328 for annotating/documenting each custom attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
7e5adfa to
68fbf10
Compare
robrap
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates.
auth_backends/pipeline.py
Outdated
| @@ -1,10 +1,13 @@ | |||
| """Python-social-auth pipeline functions. | |||
| For more info visit http://python-social-auth.readthedocs.org/en/latest/pipeline.html. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that this link is broken, and should be updated to:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| ) | ||
| # .. custom_attribute_name: update_email.details_username | ||
| # .. custom_attribute_description: Records the username provided in the | ||
| # authentication details when a mismatch occurs with the user's username. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got the term "social details" from the doc https://python-social-auth.readthedocs.io/en/latest/pipeline.html I noted elsewhere. I don't want to call this "authentication details", because if the authenticated user (user) is a different user, the authentication details would not be for a different user.
| # authentication details when a mismatch occurs with the user's username. | |
| # social details when a mismatch occurs with the user's username. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
|
|
||
| # .. custom_attribute_name: update_email.user_username | ||
| # .. custom_attribute_description: Records the actual username of the user | ||
| # when a mismatch occurs with the authentication details username. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I want to be careful about using the term "authentication" here.
| # when a mismatch occurs with the authentication details username. | |
| # when a mismatch occurs with the social details' username. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/tests/test_pipeline.py
Outdated
| # Verify email was updated | ||
| self.user = User.objects.get(username=self.user.username) | ||
| self.assertEqual(self.user.email, updated_email) | ||
| self.strategy.storage.user.changed.assert_called_once_with(self.user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion on a mock seems unnecessary, since you've already asserted that the email was updated. Can you explain why this is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assertion "self.strategy.storage.user.changed.assert_called_once_with(self.user)" verifies that our function correctly notifies the strategy when a user is changed.
While the email equality check confirms the outcome (email updated), this assertion verifies the mechanism (strategy notification). This is crucial because:
- It ensures the function fulfills its complete contract with the authentication system.
- It prevents refactoring that might bypass the strategy notification while still updating the email.
- It maintains consistency with social-auth-core's expected pipeline behavior.
In short, it tests both what happened and how it happened - both essential for complete test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/tests/test_pipeline.py
Outdated
| # Verify email was not updated | ||
| self.user = User.objects.get(username=self.user.username) | ||
| self.assertEqual(self.user.email, old_email) | ||
| self.strategy.storage.user.changed.assert_not_called() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this assertion feels a little like overkill, unless you want to be very clear that the email didn't change and change back or something. I'll leave it up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. You can ignore this. I'll go with your earlier explanation. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
requirements/base.in
Outdated
| six | ||
| social-auth-app-django | ||
| social-auth-core # Common auth interfaces | ||
| edx-django-utils |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Please alphabetize.
- Please set your editor to ensure a blank line at the end of files.
- See fix:bug in third party authentication email and name update #382 (comment). You'll still need to run
make upgradefrom a container to build the*.txtfiles appropriately. Let me know if this gives you issues.
Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. custom_attribute_description: Records the actual username of the user | ||
| # when a mismatch occurs with the authentication details username. | ||
| set_custom_attribute('update_email.user_username', str(user_username)) | ||
| return # Exit without updating email |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to add a temporary rollout toggle before this return. It could be a SettingToggle, based on a setting named SKIP_UPDATE_EMAIL_ON_USERNAME_MISMATCH. See docs:
- https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/implement_the_right_toggle_type.html (to implement)
- https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html (to annotate)
I'm a little nervous about if it is possible to have None for the details' username, but still have a details' email provided. Adding this observability, with the toggle disabled, would allow us to test before enabling the toggle. We could later clean that up.
We might want to add the following to determine if this is ever the case:
set_custom_attribute('update_email.details_has_email', details.get('email') is not None)
If everything looks good from an observability perspective, we can enable the toggle. If things still look good, we can remove the temporary toggle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please let me know if you have any questions about this. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
|
[request] You'll also want to update the version and changelog to 4.6.0. See example changes needed in this commit. |
…clean up test cases
auth_backends/pipeline.py
Outdated
| # .. toggle_description: Determines whether to block email updates when usernames don't match. | ||
| # When enabled (True), email updates will be blocked when the username in social auth details | ||
| # doesn't match the user's username. When disabled (False), email updates will proceed regardless | ||
| # of username mismatches. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| # of username mismatches. | |
| # of username mismatches. This will be used for a temporary rollout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
robrap
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you.
auth_backends/pipeline.py
Outdated
| # .. toggle_use_cases: temporary | ||
| # .. toggle_creation_date: 2025-06-05 | ||
| # .. toggle_target_removal_date: 2026-06-05 | ||
| # .. toggle_warnings: This is a temporary toggle for a security enhancement rollout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved and edited in https://github.com/openedx/auth-backends/pull/382/files#r2141085575.
| # .. toggle_warnings: This is a temporary toggle for a security enhancement rollout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. toggle_creation_date: 2025-06-05 | ||
| # .. toggle_target_removal_date: 2026-06-05 | ||
| # .. toggle_warnings: This is a temporary toggle for a security enhancement rollout. | ||
| # .. toggle_tickets: ARCHBOM-2181 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't link to private tickets in public repositories. This PR and its description will be enough.
| # .. toggle_tickets: ARCHBOM-2181 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. custom_attribute_name: update_email.details_username | ||
| # .. custom_attribute_description: Records the username provided in the | ||
| # social details when a mismatch occurs with the user's username. | ||
| set_custom_attribute('update_email.details_username', str(details_username)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let the observability tool treat None differently from '' if it wishes.
| set_custom_attribute('update_email.details_username', str(details_username)) | |
| set_custom_attribute('update_email.details_username', details_username) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. custom_attribute_name: update_email.user_username | ||
| # .. custom_attribute_description: Records the actual username of the user | ||
| # when a mismatch occurs with the social details username. | ||
| set_custom_attribute('update_email.user_username', str(user_username)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| set_custom_attribute('update_email.user_username', str(user_username)) | |
| set_custom_attribute('update_email.user_username', user_username) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
requirements/ci.txt
Outdated
| # -r requirements/ci.in | ||
| virtualenv==20.30.0 | ||
| # via tox | ||
| edx-django-utils==8.0.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
|
@Akanshu-2u: You can ignore all comment threads ending with "This can be marked resolved." Thank you. |
robrap
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good and is very close, despite the fact that I left a bunch of comments. Thank you!
CHANGELOG.rst
Outdated
| Unreleased | ||
| ---------- | ||
|
|
||
| * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switching this to a blank line:
| * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
CHANGELOG.rst
Outdated
| ---------- | ||
|
|
||
| * | ||
| [4.6.0] - 2025-06-09 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might update again, but this is closer.
| [4.6.0] - 2025-06-09 | |
| [4.6.0] - 2025-06-18 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. toggle_description: Determines whether to block email updates when usernames don't match. | ||
| # When enabled (True), email updates will be blocked when the username in social auth details | ||
| # doesn't match the user's username. When disabled (False), email updates will proceed regardless | ||
| # of username mismatches.This will be used for a temporary rollout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Original suggestion had a space.
| # of username mismatches.This will be used for a temporary rollout. | |
| # of username mismatches. This will be used for a temporary rollout. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. toggle_creation_date: 2025-06-05 | ||
| # .. toggle_target_removal_date: 2026-06-05 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's give a shorter window for follow-up. I also updated the date.
| # .. toggle_creation_date: 2025-06-05 | |
| # .. toggle_target_removal_date: 2026-06-05 | |
| # .. toggle_creation_date: 2025-06-18 | |
| # .. toggle_target_removal_date: 2025-08-18 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| details_username = details.get('username') | ||
| user_username = getattr(user, 'username', None) | ||
| # Check if usernames match | ||
| username_match = details_username == user_username |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this were username_mismatch (in place of username_match), you would avoid the not in several places below, which would read more like english, and would be more consistent with the temporary toggle name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/pipeline.py
Outdated
| # .. custom_attribute_name: update_email.details_has_email | ||
| # .. custom_attribute_description: Records whether the details contain an email | ||
| # when a username mismatch occurs, to identify potential edge cases. | ||
| set_custom_attribute('update_email.details_has_email', details.get('email') is not None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure when and if you might get an empty email here, so let's use bool instead. You do not need to add another unit test. This is just in case.
| set_custom_attribute('update_email.details_has_email', details.get('email') is not None) | |
| set_custom_attribute('update_email.details_has_email', bool(details.get('email'))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
…ine tests for username mismatch toggle
robrap
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. Let me know if you want to make this final change.
auth_backends/tests/test_pipeline.py
Outdated
| mock_set_attribute.assert_called_with('update_email.username_mismatch', False) | ||
| mock_set_attribute.assert_any_call('update_email.username_mismatch', False) | ||
| mock_set_attribute.assert_any_call('update_email.rollout_toggle_enabled', False) | ||
| # email_updated should not be called since email wasn't updated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional/non-blocking] The mock has an array of calls args. Consider checking that the custom attribute was not called in place of a comment. Consider adding the same check to the True case to show that the code is appropriately checking, because it would be simple to code up a false positive that isn't really checking what you want.
Please leave a comment if you decide to leave as-is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/tests/test_pipeline.py
Outdated
| mock_set_attribute.assert_any_call('update_email.rollout_toggle_enabled', False) | ||
| # email_updated should not be called since email wasn't updated | ||
| # Verify email_updated was not set | ||
| for call_args in mock_set_attribute.call_args_list: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I would use a list comprehension, because then you could easily assert that it is not found, but also assert that it was found.
- If you put this in a helper function, like
assert_set_attribute_call(found = False), you could use the method in the earlier unit test, and prove that your code would find it if it could.
See example code:
>>> call_args = [('a', 1), ('b', 2)]
>>> [x for x in call_args if x[0] == 'a']
[('a', 1)]
>>> len([x for x in call_args if x[0] == 'a']) == 0
False
>>> len([x for x in call_args if x[0] == 'c']) == 0
True
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
auth_backends/tests/test_pipeline.py
Outdated
| # email_updated should not be called since email wasn't updated | ||
| # Verify email_updated was not set | ||
| for call_args in mock_set_attribute.call_args_list: | ||
| self.assertNotEqual(call_args[0][0], 'update_email.email_updated') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unclear if call_args[0][0] is correct or not. As noted in the other column, if you use the same method to assert elsewhere that this call is found, it would make it easier to trust that it not being found is not a false positive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be marked resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@openedx/committers-auth-backends: [request] I have reviewed and approved. If you are able to approve, squash and release the library, that would be great. Thank you!
We plan to remove temporary toggles once we've proven this safe.
UPDATE: The PR description (which I can't update) reads:
Prevents potential account takeover by ensuring emails are only updated when the authentication flow correctly identifies the user.
This isn't really true. User records are updated with the wrong email, but since this happens in a service other than the identity provider, there is no account takeover. It does cause invalid data.
idegtiarov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Akanshu-2u @robrap thank you guys for the great work you did on that PR.
Description:
This PR enhances the update_email pipeline function with security verification that ensures email updates only occur when usernames match between the authentication provider and the existing user account. It adds detailed logging and monitoring attributes to track username mismatches, helping identify potential security issues or integration problems in the authentication flow.
Added Username Verification:
Now validates that the username from the authentication provider matches the user's username before updating email.
Uses defensive coding with [details.get('username')] and [getattr(user, 'username', None)].
Added Monitoring and Logging:
Sets a custom attribute [update_email.username_mismatch] for all cases to track mismatches.
For mismatches only:
Security Improvement:
Prevents potential account takeover by ensuring emails are only updated when the authentication flow correctly identifies
the user.
Provides visibility into authentication integration issues through the added monitoring.
Unit-Tests
Replaced real strategy objects with [MagicMock] for better test isolation and reliability.
Added realistic behavior to mocks with [side_effect] to properly test database interactions.
Added comprehensive test coverage for success paths, edge cases, and security scenarios.
Created specific test for username mismatch scenario to verify security controls.
Testing Performed
JIRA:
(https://2u-internal.atlassian.net/browse/ARCHBOM-2181)
Reviewers:
@robrap