Add Temporary Passwords and Forced Password Change

What does this MR do and why?

Adds a temporary password / forced password change feature to Crafty, allowing admins to reset user passwords and optionally require the user to set a new password on their next login.

Key changes

  • Database migration (20260216_temp_password.py): Adds require_password_change (boolean) and password_expires (nullable datetime) columns to the users table.
  • New API endpoint POST /api/v2/users/:id/reset-password: Allows admins (superuser or USER_CONFIG permission) to reset a user's password. Requires the client to provide the password (generation is handled client-side with configurable length, default 16). Supports optional expiry window (expires_hours, -1 for no expiry) and a require_password_change flag. Includes superuser protection (non-superusers cannot reset superuser passwords) and a manager guard (non-superusers can only reset passwords for users they directly manage).
  • API gate (base_handler.py): When require_password_change is set, all API requests are blocked with a 403 PASSWORD_CHANGE_REQUIRED error except PATCH /api/v2/users/@me (password change) and POST /api/v2/auth/invalidate_tokens (logout). API key access is exempt.
  • Login flow (login.py): Checks for expired temp passwords at login time (returns 401 TEMP_PASSWORD_EXPIRED). When the user has require_password_change set, the login response includes require_password_change: true.
  • Forced password change UI (embedded in login.html): A hidden <form id="change-password-form"> is revealed in-place on the login page after a successful login with require_password_change: true. Calls PATCH /api/v2/users/@me to bypass the API gate. Includes client-side validation (match check, min length 8).
  • Password flag auto-clear: When a user successfully changes their password (via the shared UsersController.update_user() method, called by both PATCH /api/v2/users/@me and PATCH /api/v2/users/:id), require_password_change and password_expires are automatically cleared, and all existing tokens are invalidated.
  • Admin UI updates (panel_config.html, panel_edit_user.html): Adds a reset password dialog (with generate button, length selector, expiry hours, and require-change checkbox). On panel_config.html, the dialog appears for all other users. On panel_edit_user.html, the reset button is restricted to superusers editing other users. Self-edit shows the standard password change dialog instead.
  • Translation keys: Full English translations added under the tempPassword namespace for the reset dialog UI, plus passwordChangeRequired and tempPasswordExpired keys in the login namespace for login-time messages.

Closes Issues

  • #146

    • This does not generate a link, but still satisfies the general idea.
  • #581

I also see the opportunity to add MFA setup to the change password workflow. This can be done in a future branch.

Screenshots or screen recordings

Screenshot_2026-02-21_230858 Screenshot_2026-02-21_230933

Screenshot_2026-02-21_230848

Screenshot_2026-02-26_191046

Screenshot_2026-02-26_191119

Config.json

Screenshot_2026-02-26_200321

How to set up and validate locally

  1. Start the Crafty 4 dev server - the 20260216_temp_password migration will run automatically, adding the new columns.
  2. Reset a user's password as admin:
    • Go to Crafty Config > Users and click the lock icon next to a user (not yourself).
    • In the dialog, optionally set a password length, expiry, and toggle "Require password change." Click "Reset Password."
    • Confirm the generated password is displayed.
  3. Log in as the reset user:
    • Use the temporary password to log in.
    • Verify you are redirected to /panel/change_password.
    • Verify you cannot navigate to other pages or make API calls (403 gate).
    • Set a new password and confirm redirect to the dashboard.
  4. Test expiry:
    • Reset a user's password with a short expiry (e.g., 1 hour).
    • Attempt to log in - verify TEMP_PASSWORD_EXPIRED error.
  5. Test superuser protection:
    • As a non-superuser with USER_CONFIG permission, try to reset a superuser's password - verify 403 ACCESS_DENIED.
  6. Test self-edit:
    • On the panel config user list, verify your own row shows the standard password change dialog (not the reset dialog).

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

  • Have you checked this doesn't interfere/conflict/duplicate someone elses work?
  • Have you fully tested your changes?
  • Have you resolved any lint issues?
  • Have you assigned a reviewer?
  • Have you applied correct labels?
Edited by Corey Koval

Merge request reports

Loading