Edward/password expiration - OP-3113 - #434
Conversation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| - | - | Generic Password | 1dd11d0 | core/tests/test_services.py | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
Pull request overview
Implements backend support for password-expiration handling in authentication, including explicit expired-password responses, password reset hardening, and scheduled password-expiry reminder emails.
Changes:
- Extend authentication/login GraphQL mutation to surface password-expired and password-expiry warning fields, and trigger reset email flow for expired-password logins.
- Update password reset and set-password flows (rate limiting hook, reduced account-existence leakage, structured set-password responses).
- Add password-expiry reminder emails (templates + scheduler integration + reminder send/logging support) and expand test coverage.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates scheduler guidance for production deployments. |
| core/tests/test_services.py | Adds tests for password validity updates, expiry reminders, and expired-password authentication behavior. |
| core/tests/test_graphql.py | Adds GraphQL tests for expired-password login reset email and expiry-warning fields. |
| core/templates/password_reset.txt | Refines reset-password plaintext email content. |
| core/templates/password_reset.html | Adds HTML reset-password email template. |
| core/templates/password_expiry_reminder.txt | Adds plaintext password-expiry reminder template. |
| core/templates/password_expiry_reminder.html | Adds HTML password-expiry reminder template. |
| core/services/userServices.py | Adds allow-expired authentication path, password reset rate-limit helper, reset email changes, and expiry reminder selection/sending. |
| core/services/init.py | Exposes new reminder/rate-limit functions from the services package. |
| core/schema.py | Updates reset/set-password mutations and extends tokenAuth with expiry/expired-password response fields. |
| core/scheduler.py | Hooks core scheduled tasks into the scheduler setup. |
| core/scheduled_tasks.py | Registers the password-expiry reminder cron job. |
| core/models/user.py | Adds password reuse checks, password validity tracking, and reminder log model. |
| core/models/init.py | Exports PasswordExpiryReminderLog. |
| core/migrations/0036_passwordexpiryreminderlog.py | Creates DB table for password expiry reminder logs. |
| core/apps.py | Adds/loads new password-expiry configuration defaults. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.assertTrue(len(mail.outbox) == 1) | ||
| self.assertTrue(mail.outbox[0].subject == "[OpenIMIS] Reset Password") |
| def is_password_reset_rate_limited(request, username): | ||
| window = settings.PASSWORD_RESET_RATE_LIMIT_WINDOW | ||
| ip_address = ( | ||
| getattr(request, "axes_ip_address", None) | ||
| or request.META.get("REMOTE_ADDR", "unknown") | ||
| ) | ||
|
|
||
| normalized_username = (username or "").strip().lower() | ||
| account_hash = hashlib.sha256( | ||
| normalized_username.encode("utf-8") | ||
| ).hexdigest() | ||
|
|
||
| ip_count = _increment_reset_counter( | ||
| f"password-reset:ip:{ip_address}", | ||
| window, | ||
| ) | ||
| account_count = _increment_reset_counter( | ||
| f"password-reset:account:{account_hash}", | ||
| window, | ||
| ) | ||
|
|
||
| return ( | ||
| ip_count > settings.PASSWORD_RESET_RATE_LIMIT_PER_IP | ||
| or account_count > settings.PASSWORD_RESET_RATE_LIMIT_PER_ACCOUNT | ||
| ) |
| except ValidationError as validation_error: | ||
| logger.exception(validation_error) |
Implement expired-password backend flow
Summary
Implemented backend support for expired-password handling during login and password reset.
When a user logs in with correct credentials but their password has expired, the backend now returns an explicit password-expired response instead of treating it as normal incorrect credentials. No JWT token is issued for expired-password logins.
Backend changes
user_authenticationto supportallow_expired=True.InteractiveUser.password_validity/is_password_expired.passwordExpired=true,password_expiredreset_email_sentusernameImportant behavior
Expired-password handling only happens after the username and password are valid.
If the user enters the wrong password, they still see the normal incorrect credentials message. This avoids exposing account state for invalid login attempts.
Testing / verification
Verified locally that: