Skip to main content

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.update()

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:

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.create()
# 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:

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plain_passwords-0.26.6.tar.gz (107.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plain_passwords-0.26.6-py3-none-any.whl (108.0 kB view details)

Uploaded Python 3

File details

Details for the file plain_passwords-0.26.6.tar.gz.

File metadata

  • Download URL: plain_passwords-0.26.6.tar.gz
  • Upload date:
  • Size: 107.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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

Hashes for plain_passwords-0.26.6.tar.gz
Algorithm Hash digest
SHA256 8ef3b87a7ea5c7fff45c3e556f3e2df6b19d098200b5a6e24a21e7c9c3b635e9
MD5 e84c3fa21b36d6173b34f4757d125250
BLAKE2b-256 a00e92e6aca8be9fe2fb98f951653f9795085042d17045d6fe5656133bbdcf52

See more details on using hashes here.

File details

Details for the file plain_passwords-0.26.6-py3-none-any.whl.

File metadata

  • Download URL: plain_passwords-0.26.6-py3-none-any.whl
  • Upload date:
  • Size: 108.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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

Hashes for plain_passwords-0.26.6-py3-none-any.whl
Algorithm Hash digest
SHA256 4bd731197569bde3c5e2d31949aed73ac4c20cfd72b4849269bee19de70a967b
MD5 4a4e37cc1ee7c9c607d931e34513f862
BLAKE2b-256 f5d66b3c8cffd36b5c5d1500b82630be308ac12d92f701e1f6b1290e867a6d0d

See more details on using hashes here.

Release history Release notifications | RSS feed

0.26.7

2 files

This release

0.26.6 This release

2 files

0.26.5

2 files

0.26.4

2 files

0.26.3

2 files

0.26.2

2 files

0.26.1

2 files

0.26.0

2 files

0.25.2

2 files

0.25.1

2 files

0.25.0

2 files

0.24.1

2 files

0.24.0

2 files

0.23.6

2 files

0.23.5

2 files

0.23.4

2 files

0.23.3

2 files

0.23.2

2 files

0.23.1

2 files

0.23.0

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.19.1

2 files

0.19.0

2 files

0.18.0

2 files

0.17.1

2 files

0.17.0

2 files

0.16.0

2 files

0.15.1

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

0.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page