# plain.passwords **Password hashing, validation, and authentication views for Plain.** - [Overview](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#overview) - [Password hashing](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#password-hashing) - [Password validation](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#password-validation) - [PasswordField](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#passwordfield) - [Views](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#views) - [Login](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#login) - [Signup](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#signup) - [Password change](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#password-change) - [Password reset](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#password-reset) - [Forms](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#forms) - [Settings](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#settings) - [FAQs](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#faqs) - [Installation](https://plainframework.com/docs/plain-passwords/plain/passwords/?llm#installation) ## Overview You can hash and verify passwords using the `hash_password` and `check_password` functions: ```python from plain.passwords.hashers import hash_password, check_password # Hash a password for storage hashed = hash_password("my-secret-password") # Returns something like: pbkdf2_sha256$720000$abc123...$xyz789... # Verify a password against a hash is_valid = check_password("my-secret-password", hashed) # Returns True ``` For user authentication, you can use the built-in views. Add [`PasswordLoginView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordLoginView) to your URLs: ```python # app/urls.py from plain.urls import path from plain.passwords.views import PasswordLoginView urlpatterns = [ path("login/", PasswordLoginView, name="login"), ] ``` ## Password hashing Passwords are hashed using PBKDF2 with SHA256 by default. The [`hash_password`](https://plainframework.com/docs/plain-passwords/plain/passwords/hashers.py?llm#hash_password) function generates a secure hash: ```python from plain.passwords.hashers import hash_password hashed = hash_password("user-password") ``` The [`check_password`](https://plainframework.com/docs/plain-passwords/plain/passwords/hashers.py?llm#check_password) function verifies a password against a stored hash. It also handles automatic hash upgrades when the hashing algorithm changes: ```python from plain.passwords.hashers import check_password def setter(new_hash): # Called when the hash needs to be upgraded user.password = new_hash user.save() is_valid = check_password("user-password", stored_hash, setter=setter) ``` You can configure which hashers are available via the `PASSWORD_HASHERS` setting. The first hasher in the list is used for new passwords: ```python # app/settings.py PASSWORD_HASHERS = [ "plain.passwords.hashers.PBKDF2PasswordHasher", ] ``` To create a custom hasher, subclass [`BasePasswordHasher`](https://plainframework.com/docs/plain-passwords/plain/passwords/hashers.py?llm#BasePasswordHasher) and implement the required methods. ## Password validation Three validators are included for checking password strength: - [`MinimumLengthValidator`](https://plainframework.com/docs/plain-passwords/plain/passwords/validators.py?llm#MinimumLengthValidator) - Ensures passwords meet a minimum length (default: 8 characters) - [`CommonPasswordValidator`](https://plainframework.com/docs/plain-passwords/plain/passwords/validators.py?llm#CommonPasswordValidator) - Rejects passwords from a list of 20,000 common passwords - [`NumericPasswordValidator`](https://plainframework.com/docs/plain-passwords/plain/passwords/validators.py?llm#NumericPasswordValidator) - Rejects passwords that are entirely numeric ```python from plain.passwords.validators import ( MinimumLengthValidator, CommonPasswordValidator, NumericPasswordValidator, ) from plain.exceptions import ValidationError validators = [ MinimumLengthValidator(min_length=10), CommonPasswordValidator(), NumericPasswordValidator(), ] password = "test" for validator in validators: try: validator(password) except ValidationError as e: print(e.message) ``` ## PasswordField [`PasswordField`](https://plainframework.com/docs/plain-passwords/plain/passwords/models.py?llm#PasswordField) is a model field that automatically hashes passwords before saving. It includes all three validators by default: ```python from plain import postgres from plain.postgres import types from plain.passwords.models import PasswordField @postgres.register_model class User(postgres.Model): email: str = types.EmailField() password = PasswordField() model_options = postgres.Options( constraints=[ postgres.UniqueConstraint(fields=["email"], name="unique_email"), ], ) ``` When you assign a raw password, it gets hashed automatically on save: ```python user = User(email="user@example.com", password="my-password") user.save() # user.password is now a hash like: pbkdf2_sha256$720000$... ``` For better type checking support, you can import from `plain.passwords.types`: ```python from plain.passwords.types import PasswordField ``` ## Views All views are designed to work with [plain.auth](https://plainframework.com/docs/plain-auth/plain/auth/README.md?llm) for session management. ### Login [`PasswordLoginView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordLoginView) handles email/password authentication: ```python from plain.urls import path from plain.passwords.views import PasswordLoginView urlpatterns = [ path("login/", PasswordLoginView, name="login"), ] ``` You can customize the success URL: ```python class MyLoginView(PasswordLoginView): success_url = "/dashboard/" ``` ### Signup [`PasswordSignupView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordSignupView) creates new users with email and password: ```python from plain.urls import path from plain.passwords.views import PasswordSignupView urlpatterns = [ path("signup/", PasswordSignupView, name="signup"), ] ``` ### Password change [`PasswordChangeView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordChangeView) lets authenticated users change their password by entering their current password: ```python from plain.urls import path from plain.passwords.views import PasswordChangeView urlpatterns = [ path("password/change/", PasswordChangeView, name="password_change"), ] ``` ### Password reset Password reset requires two views and an email template. [`PasswordForgotView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordForgotView) sends the reset email, and [`PasswordResetView`](https://plainframework.com/docs/plain-passwords/plain/passwords/views.py?llm#PasswordResetView) handles the token and new password: ```python from plain.urls import path from plain.passwords.views import PasswordForgotView, PasswordResetView class MyPasswordForgotView(PasswordForgotView): reset_confirm_url_name = "password_reset" success_url = "/login/" class MyPasswordResetView(PasswordResetView): success_url = "/login/" urlpatterns = [ path("password/forgot/", MyPasswordForgotView, name="password_forgot"), path("password/reset/", MyPasswordResetView, name="password_reset"), ] ``` You need to create a `password_reset` email template for [plain.email](https://plainframework.com/docs/plain-email/plain/email/README.md?llm). The template receives `email`, `user`, and `url` in its context. ## Forms Several forms are available for building custom authentication flows: - [`PasswordLoginForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordLoginForm) - Email and password login - [`PasswordSignupForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordSignupForm) - User registration with password confirmation - [`PasswordSetForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordSetForm) - Set a new password without the old one - [`PasswordChangeForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordChangeForm) - Change password with current password verification - [`PasswordResetForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordResetForm) - Request a password reset email ## Settings | Setting | Default | Env var | | ------------------ | ------- | ------------------------------- | | `PASSWORD_HASHERS` | `[...]` | `PLAIN_PASSWORD_HASHERS` (JSON) | See [`default_settings.py`](https://plainframework.com/docs/plain-passwords/plain/passwords/default_settings.py?llm) for more details. ## FAQs #### How do I customize the login form? Subclass [`PasswordLoginForm`](https://plainframework.com/docs/plain-passwords/plain/passwords/forms.py?llm#PasswordLoginForm) and set `form_class` on your view: ```python from plain.passwords.forms import PasswordLoginForm from plain.passwords.views import PasswordLoginView class MyLoginForm(PasswordLoginForm): # Add custom fields or validation pass class MyLoginView(PasswordLoginView): form_class = MyLoginForm ``` #### How do I customize password validation? Pass custom validators to `PasswordField`: ```python from plain.passwords.models import PasswordField from plain.passwords.validators import MinimumLengthValidator password = PasswordField(validators=[ MinimumLengthValidator(min_length=12), ]) ``` #### How do I use a different hashing algorithm? Add your hasher to `PASSWORD_HASHERS`. The first one is used for new passwords: ```python PASSWORD_HASHERS = [ "myapp.hashers.Argon2PasswordHasher", "plain.passwords.hashers.PBKDF2PasswordHasher", # For existing passwords ] ``` #### How long are password reset tokens valid? By default, tokens expire after 1 hour. Override `reset_token_max_age` on `PasswordResetView` to change this: ```python class MyPasswordResetView(PasswordResetView): reset_token_max_age = 60 * 60 * 24 # 24 hours ``` ## Installation Install the package from PyPI: ```bash uv add plain.passwords ``` Add the `password` field to your User model: ```python # app/models.py from plain import postgres from plain.postgres import types from plain.passwords.models import PasswordField @postgres.register_model class User(postgres.Model): email: str = types.EmailField() password = PasswordField() model_options = postgres.Options( constraints=[ postgres.UniqueConstraint(fields=["email"], name="unique_email"), ], ) ``` Add login and logout views to your URLs: ```python # app/urls.py from plain.urls import path from plain.auth.views import LogoutView from plain.passwords.views import PasswordLoginView urlpatterns = [ path("login/", PasswordLoginView, name="login"), path("logout/", LogoutView, name="logout"), ] ``` Create templates for your views. For the login view, create `templates/passwords/passwordlogin.html`: ```html {% extends "base.html" %} {% block content %}
{% endblock %} ``` For password resets, install [plain.email](https://plainframework.com/docs/plain-email/plain/email/README.md?llm) and create a reset email template.