Password authentication for Plain.
Project description
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 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:
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:
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 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:
# 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">
<div>
<label for="{{ form.email.html_id }}">Email</label>
<input
type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.html_id }}"
value="{{ form.email.value }}"
>
{% for error in form.email.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
<div>
<label for="{{ form.password.html_id }}">Password</label>
<input
type="password"
name="{{ form.password.html_name }}"
id="{{ form.password.html_id }}"
>
{% for error in form.password.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
<button type="submit">Log in</button>
</form>
{% endblock %}
For password resets, install plain.email and create a reset email template.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plain_passwords-0.25.1.tar.gz.
File metadata
- Download URL: plain_passwords-0.25.1.tar.gz
- Upload date:
- Size: 103.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03d4fafdbb0443447719aa9d6372ea5ca92f5174ea272ac8233d53276f20be0d
|
|
| MD5 |
276e39c46f561feb0773250a341ef10d
|
|
| BLAKE2b-256 |
18e7a6ef06c1708e38e2054762bbe6074bf298456523f5351d6882a081581190
|
File details
Details for the file plain_passwords-0.25.1-py3-none-any.whl.
File metadata
- Download URL: plain_passwords-0.25.1-py3-none-any.whl
- Upload date:
- Size: 107.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.3 {"installer":{"name":"uv","version":"0.11.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bdbacf91b710e8e151bd65bae613df81f7c02022d66539cccfab0748c53c180b
|
|
| MD5 |
7204defbe741b56030912b73cc9d5668
|
|
| BLAKE2b-256 |
424c457d3ac4916d9db5ad4280da1119408eebdb5130f8ebe95dc1a692c048f5
|