plain.passwords
Password hashing, validation, and authentication views for Plain.
Overview
You can hash and verify passwords using the hash_password and check_password functions:
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 to your URLs:
# 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 function generates a secure hash:
from plain.passwords.hashers import hash_password
hashed = hash_password("user-password")
The check_password function verifies a password against a stored hash. It also handles automatic hash upgrades when the hashing algorithm changes:
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:
# app/settings.py
PASSWORD_HASHERS = [
"plain.passwords.hashers.PBKDF2PasswordHasher",
]
To create a custom hasher, subclass BasePasswordHasher and implement the required methods.
Password validation
Three validators are included for checking password strength:
MinimumLengthValidator- Ensures passwords meet a minimum length (default: 8 characters)CommonPasswordValidator- Rejects passwords from a list of 20,000 common passwordsNumericPasswordValidator- Rejects passwords that are entirely numeric
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 is a model field that automatically hashes passwords before saving. It includes all three validators by default:
from plain import models
from plain.passwords.models import PasswordField
@models.register_model
class User(models.Model):
email = models.EmailField(unique=True)
password = PasswordField()
When you assign a raw password, it gets hashed automatically on save:
user = User(email="[email protected]", 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:
from plain.passwords.types import PasswordField
Views
All views are designed to work with plain.auth for session management.
Login
PasswordLoginView handles email/password authentication:
from plain.urls import path
from plain.passwords.views import PasswordLoginView
urlpatterns = [
path("login/", PasswordLoginView, name="login"),
]
You can customize the success URL:
class MyLoginView(PasswordLoginView):
success_url = "/dashboard/"
Signup
PasswordSignupView creates new users with email and password:
from plain.urls import path
from plain.passwords.views import PasswordSignupView
urlpatterns = [
path("signup/", PasswordSignupView, name="signup"),
]
Password change
PasswordChangeView lets authenticated users change their password by entering their current password:
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 sends the reset email, and PasswordResetView handles the token and new password:
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. The template receives email, user, and url in its context.
Forms
Several forms are available for building custom authentication flows:
PasswordLoginForm- Email and password loginPasswordSignupForm- User registration with password confirmationPasswordSetForm- Set a new password without the old onePasswordChangeForm- Change password with current password verificationPasswordResetForm- Request a password reset email
Settings
| Setting | Default | Env var |
|---|---|---|
PASSWORD_HASHERS |
[...] |
PLAIN_PASSWORD_HASHERS (JSON) |
See default_settings.py for more details.
FAQs
How do I customize the login form?
Subclass PasswordLoginForm and set form_class on your view:
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:
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:
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:
class MyPasswordResetView(PasswordResetView):
reset_token_max_age = 60 * 60 * 24 # 24 hours
Installation
Install the package from PyPI:
uv add plain.passwords
Add the password field to your User model:
# app/models.py
from plain import models
from plain.passwords.models import PasswordField
@models.register_model
class User(models.Model):
email = models.EmailField(unique=True)
password = PasswordField()
Add login and logout views to your URLs:
# 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:
{% extends "base.html" %}
{% block content %}
<form method="post">
{{ csrf_input }}
{{ form.as_elements }}
<button type="submit">Log in</button>
</form>
{% endblock %}
For password resets, install plain.email and create a reset email template.