Fixed #357 - Password hashing when changed manually in admin#360
Fixed #357 - Password hashing when changed manually in admin#360Alien501 wants to merge 8 commits intodjangoindia:mainfrom
Conversation
| def save(self, *args, **kwargs): | ||
| self.email = self.email.lower().strip() | ||
|
|
||
| if not self.password.startswith('pbkdf2_sha256'): |
There was a problem hiding this comment.
This would only work if the default hashing algorithm is used. If other algorithms are used, it would cause issues.
|
@PankajVishw50 made it to be generic, could you check my latest push |
PankajVishw50
left a comment
There was a problem hiding this comment.
Looks good to me. Just a small typo in comment has should be written hash
| def save(self, *args, **kwargs): | ||
| self.email = self.email.lower().strip() | ||
| try: | ||
| # Trying to find the has used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-) |
There was a problem hiding this comment.
Fix Typo. has -> hash
| try: | ||
| # Trying to find the hash used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-) | ||
| identify_hasher(self.password) | ||
| except: |
There was a problem hiding this comment.
It's not ideal to use bare except. You can use except Exception: if you're not sure what errors will be thrown.
ankanchanda
left a comment
There was a problem hiding this comment.
Please address the comment by sumit4613, and close this.
| if not self.password.startswith('pbkdf2_sha256'): | ||
| self.password = make_password(self.password) | ||
|
|
There was a problem hiding this comment.
Don't we need to change anything on the login part?
| try: | ||
| # Trying to find the hash used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-) | ||
| identify_hasher(self.password) | ||
| except ValueError: | ||
| # Hashing password | ||
| self.password = make_password(self.password) |
There was a problem hiding this comment.
@Alien501 I think this is not the best approach. This will try to hash the password everytime a user instance is saved. This is a django admin specific issue, and IMHO it would be better to override the save_model method of UserAdmin to something like:
def save_model(self, request, obj, form, change):
if "password" in form.changed_data or not change:
obj.set_password(form.cleaned_data["password"])
return super().save_model(request, obj, form, change)
Now, whenever a user password is changed or a new user is created from django admin, It will hash its password. Also, by default django uses PBKDF2PasswordHasher which is pretty safe. Since we won't be changing the password hasher ever, using set_password(which uses make_password under the hood) looks like a good choice to me.
Let me know what you think.
There was a problem hiding this comment.
Agreed. I'll move the password hashing logic to UserAdmin instead. Thanks for the suggestion
|
@CodeRabbit fullreview |
|
@Yadavanurag13 I'll conduct a full review of the PR to ensure the password hashing implementation is properly done and follows best practices. ✅ Actions performedFull review triggered. |
WalkthroughIntroduces a custom save_model in UserAdmin to hash passwords on creation or when the password field changes, calling set_password before delegating to the superclass. Also adds hashers imports in the user model file without changing public APIs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin as Admin User
participant UI as Django Admin UI
participant UA as UserAdmin.save_model
participant User as User instance
participant DB as Database
Admin->>UI: Edit user password and save
UI->>UA: save_model(request, obj, form, change)
alt New object or "password" in changed_data
UA->>User: set_password(raw_password)
note right of UA: Hashes the provided password
else No password change
UA->>User: Leave password as-is
end
UA->>DB: super().save_model(...)\n(persist user)
DB-->>UA: Saved
UA-->>UI: Return
UI-->>Admin: Save complete
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
| if "password" in form.changed_data or not change: | ||
| obj.set_password(form.cleaned_data["password"]) | ||
| return super().save_model(request, obj, form, change) |
There was a problem hiding this comment.
Avoid double-hashing already encoded passwords
Right now any value that looks like a Django hash (e.g. pbkdf2_sha256$...) gets passed back through set_password, so if an admin pastes an existing hash when migrating data the user’s credentials become unusable. Use the identify_hasher helper you already imported to detect pre-hashed inputs and only call set_password when we truly have raw text.
- if "password" in form.changed_data or not change:
- obj.set_password(form.cleaned_data["password"])
+ if "password" in form.changed_data or not change:
+ password = form.cleaned_data.get("password")
+ if password:
+ try:
+ identify_hasher(password)
+ except ValueError:
+ obj.set_password(password)
+ else:
+ obj.password = password
return super().save_model(request, obj, form, change)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if "password" in form.changed_data or not change: | |
| obj.set_password(form.cleaned_data["password"]) | |
| return super().save_model(request, obj, form, change) | |
| if "password" in form.changed_data or not change: | |
| password = form.cleaned_data.get("password") | |
| if password: | |
| try: | |
| identify_hasher(password) | |
| except ValueError: | |
| obj.set_password(password) | |
| else: | |
| obj.password = password | |
| return super().save_model(request, obj, form, change) |
🤖 Prompt for AI Agents
In backend/djangoindia/db/admin.py around lines 404-406, the code always calls
obj.set_password(...) which will double-hash values that are already Django
password hashes; use identify_hasher to detect pre-hashed inputs and only call
set_password for raw passwords. Modify the conditional so that when "password"
in form.changed_data or not change, you get pw = form.cleaned_data["password"],
attempt to identify_hasher(pw) in a try/except: if identification succeeds treat
it as an existing hash and assign obj.password = pw directly, otherwise call
obj.set_password(pw); keep the existing return super().save_model(...)
unchanged.
Closes #357
Now password will be hashed when modified in admin
Changes
Type of change
How has this been tested?
Author Checklist
mainfromfork:branchnameSummary by CodeRabbit