Authentication
Buraq ships two complementary auth mechanisms that can coexist in the same project:
| Mechanism | When to use |
|---|---|
Session-based (AuthenticationMiddleware) |
Server-rendered pages, cookie sessions |
JWT (Authorization: Bearer …) |
APIs, mobile clients, SPA back-ends |
Session-based authentication
Section titled “Session-based authentication”Add both middleware layers to your application (order matters — AuthenticationMiddleware wraps SessionMiddleware):
from buraq.contrib.auth.middleware import AuthenticationMiddlewarefrom buraq.contrib.sessions import SessionMiddlewarefrom buraq.conf import settings
app.add_middleware(AuthenticationMiddleware)app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY)Login / Logout views
Section titled “Login / Logout views”from buraq.contrib.auth import authenticate, login, logoutfrom buraq.shortcuts import redirect, render
async def login_view(request): if request.method == "POST": form = await request.form() user = await authenticate( request, username=form["username"], password=form["password"], ) if user: await login(request, user) next_url = request.query_params.get("next", "/dashboard") return redirect(next_url) # Show error return await render(request, "auth/login.html")
async def logout_view(request): await logout(request) return redirect("/")authenticate()
Section titled “authenticate()”from buraq.contrib.auth import authenticate
user = await authenticate(request, username="alice", password="secret")# Returns User instance on success, None on failure or inactive accountlogin()
Section titled “login()”from buraq.contrib.auth import login
await login(request, user)# Writes _auth_user_id to session, updates last_loginlogout()
Section titled “logout()”from buraq.contrib.auth import logout
await logout(request)# Flushes the entire session, sets request.user = AnonymousUser()request.user
Section titled “request.user”After AuthenticationMiddleware runs, every request has request.user:
async def dashboard(request): if not request.user.is_authenticated: return redirect("/auth/login")
return await render(request, "dashboard.html", {"user": request.user})request.user is either a User instance (with is_authenticated = True) or an AnonymousUser (with is_authenticated = False).
AnonymousUser
Section titled “AnonymousUser”from buraq.contrib.auth.models import AnonymousUser
anon = AnonymousUser()anon.is_authenticated # Falseanon.is_staff # Falseanon.is_superuser # FalseProtecting views with decorators
Section titled “Protecting views with decorators”from buraq.decorators import login_required, staff_required, superuser_required, permission_required
@login_requiredasync def dashboard(request): return await render(request, "dashboard.html", {"user": request.user})
@login_required(login_url="/signin")async def settings_view(request): ...
@staff_requiredasync def admin_view(request): ...
@superuser_requiredasync def super_view(request): ...
@permission_required("blog.publish_post")async def publish_view(request, pk: int): ...@user_passes_test
Section titled “@user_passes_test”Redirect based on any custom condition:
from buraq.decorators import user_passes_test
@user_passes_test(lambda u: u.is_active and u.date_joined.year >= 2024)async def new_users_only(request): ...Protecting class-based views
Section titled “Protecting class-based views”from buraq.views.mixins import LoginRequiredMixin, PermissionRequiredMixin, UserPassesTestMixin
class DashboardView(LoginRequiredMixin, TemplateView): template_name = "dashboard.html"
class PublishView(PermissionRequiredMixin, UpdateView): model = Post permission_required = "blog.publish_post"
class StaffOnlyView(UserPassesTestMixin, DetailView): model = Report
async def test_func(self, request) -> bool: return request.user.is_staffSee Auth Mixins for the full reference.
Permissions
Section titled “Permissions”Check whether a user holds a specific permission:
# In a viewcan_publish = await request.user.has_perm("blog.publish_post")can_all = await request.user.has_perms(["blog.add_post", "blog.change_post"])any_blog = await request.user.has_module_perms("blog")
# List user's direct permissions and groupsperms = await request.user.user_permissions()groups = await request.user.groups()Superusers (is_superuser=True) always return True from all has_perm* checks.
See Permissions & Groups for setup and assignment.
Password utilities
Section titled “Password utilities”from buraq.contrib.auth import make_password, check_password, validate_password
hashed = await make_password("my-secret")ok = await check_password("my-secret", hashed)
# Keep the user logged in after a password changefrom buraq.contrib.auth import update_session_auth_hashawait update_session_auth_hash(request, user)Password validation
Section titled “Password validation”Buraq ships a set of password validators that run during registration and password-change flows. Configure them in settings:
AUTH_PASSWORD_VALIDATORS = [ {"NAME": "buraq.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 8}}, {"NAME": "buraq.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "buraq.contrib.auth.password_validation.NumericPasswordValidator"}, {"NAME": "buraq.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "buraq.contrib.auth.password_validation.MaximumLengthValidator"},]validate_password()
Section titled “validate_password()”Call from registration or change-password views — raises ValidationError
listing all failures from all configured validators:
from buraq.contrib.auth.password_validation import validate_passwordfrom buraq.exceptions import ValidationError
try: validate_password("abc123", user=request.user)except ValidationError as e: # e.messages → list of failure strings return await render(request, "auth/register.html", {"errors": e.messages})Available validators
Section titled “Available validators”| Class | Description | Default option |
|---|---|---|
MinimumLengthValidator |
Minimum number of characters | min_length=8 |
CommonPasswordValidator |
Rejects commonly used passwords | — |
NumericPasswordValidator |
Rejects all-digit passwords | — |
UserAttributeSimilarityValidator |
Rejects passwords too similar to user fields | max_similarity=0.7 |
MaximumLengthValidator |
Guards against DoS via bcrypt | max_length=4096 |
Custom validator
Section titled “Custom validator”Implement validate(password, user=None) and optionally get_help_text():
class NoSpacesValidator: def validate(self, password, user=None): if " " in password: raise ValidationError("Password may not contain spaces.")
def get_help_text(self): return "Your password may not contain spaces."Register it just like a built-in validator:
AUTH_PASSWORD_VALIDATORS = [ ..., {"NAME": "myapp.validators.NoSpacesValidator"},]Authentication backends
Section titled “Authentication backends”authenticate() iterates AUTHENTICATION_BACKENDS in order and returns the first user a backend yields. The default backend checks the User table.
Configuration
Section titled “Configuration”AUTHENTICATION_BACKENDS = [ "myapp.backends.LDAPBackend", # checked first "buraq.contrib.auth.backends.ModelBackend", # fallback]Built-in backends
Section titled “Built-in backends”| Backend | Behaviour |
|---|---|
buraq.contrib.auth.backends.ModelBackend |
Default. Checks username + password against the User table; rejects inactive users (is_active=False). |
buraq.contrib.auth.backends.AllowAllUsersModelBackend |
Like ModelBackend but authenticates inactive users too. Useful when you want to show a “your account is disabled” page after login rather than a generic invalid-credentials error. |
buraq.contrib.auth.backends.AllowAllUsersRemoteUserBackend |
Remote-user backend that authenticates inactive users. Pair with upstream authentication (e.g. nginx auth_request) when the proxy asserts the identity and Buraq should accept it regardless of is_active. |
AUTHENTICATION_BACKENDS = [ "buraq.contrib.auth.backends.AllowAllUsersModelBackend",]Writing a custom backend
Section titled “Writing a custom backend”A backend is any class with an async authenticate method. get_user is optional but required for session restoration.
class LDAPBackend: async def authenticate(self, request, *, username: str, password: str): user = await ldap_lookup(username, password) # returns User or None return user
async def get_user(self, user_id: int): from buraq.contrib.auth.models import User return await User.objects.get_or_none(id=user_id)authenticate() in views works unchanged — the backend selection is transparent:
user = await authenticate(request, username="alice", password="secret")user._auth_backend records which backend authenticated the user.
JWT authentication
Section titled “JWT authentication”Useful for API endpoints consumed by mobile or SPA clients.
SECRET_KEY = "your-jwt-secret"JWT_ALGORITHM = "HS256"JWT_EXPIRY_MINUTES = 60urlpatterns = [ path("/auth", include("buraq.contrib.auth.urls")),]How a token is used
Section titled “How a token is used”Signing in sets access_token as an HttpOnly cookie alongside the session, so a
browser needs no extra work. A client that would rather carry the token itself
sends it back on each request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Either form is accepted. Verification is an HMAC over the token — no database query — so a request carrying a forged or expired token is rejected without touching the database at all. A valid one then loads the user, the same single lookup a session costs, because a token issued before an account was disabled must not still open the door.
Tokens are signed with SECRET_KEY using JWT_ALGORITHM, which accepts the
HMAC family (HS256, HS384, HS512). The algorithm named inside a token is
never trusted — a token claiming "alg": "none" is rejected, not honoured.
To issue one yourself:
from buraq.contrib.auth.tokens import decode_token, encode_token, token_for_user
token = token_for_user(user) # {"sub": user.pk}claims = decode_token(token) # raises TokenError if forged or expired
# Any other claims — a scope, a one-off confirmation link:invite = encode_token({"invite": "abc"}, expires_minutes=15)Logging out clears the cookie; a token a client kept a copy of stays valid until
it expires, which is what JWT_EXPIRY_MINUTES is for.
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
POST |
/auth/register |
Create a new user |
POST |
/auth/login |
Sign in; sets the session and the access_token cookie |
GET |
/auth/login |
Login form |
GET |
/auth/logout |
Log the current user out |
POST |
/auth/logout |
Log the current user out |
Class-based auth views
Section titled “Class-based auth views”Buraq ships ready-to-use class-based views for the full login/logout/password flow. Mount them with path() and supply the matching templates.
from buraq.contrib.auth.views import ( LoginView, LogoutView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView,)
urlpatterns = [ path("/auth/login", LoginView.as_view(), name="login"), path("/auth/logout", LogoutView.as_view(), name="logout"), path("/auth/password/change", PasswordChangeView.as_view(), name="password_change"), path("/auth/password/reset", PasswordResetView.as_view(), name="password_reset"), path("/auth/password/reset/{token}", PasswordResetConfirmView.as_view(), name="password_reset_confirm"),]LoginView
Section titled “LoginView”| Attribute | Default | Description |
|---|---|---|
template_name |
registration/login.html |
GET template |
redirect_field_name |
"next" |
Query param for post-login redirect |
success_url |
"/" |
Fallback redirect |
redirect_authenticated_user |
False |
Skip login for already-authenticated users |
On POST, validates username + password, sets an access_token HttpOnly cookie, and redirects.
LogoutView
Section titled “LogoutView”| Attribute | Default |
|---|---|
next_page |
"/" |
template_name |
registration/logged_out.html |
Deletes the access_token cookie and redirects to next_page.
PasswordChangeView
Section titled “PasswordChangeView”Accepts old_password, new_password1, new_password2. Validates the old password with Argon2 and updates hashed_password on success.
Template: registration/password_change_form.html
PasswordResetView
Section titled “PasswordResetView”Accepts an email address, looks up the user, generates a HMAC-SHA256–signed token (uid-timestamp-sig), and sends a reset link by email. Silently succeeds even for unknown addresses (prevents user enumeration). Token expires after 24 hours.
Template: registration/password_reset_form.html
Email template: registration/password_reset_email.html
PasswordResetConfirmView
Section titled “PasswordResetConfirmView”Validates the signed token from the URL, verifies the HMAC, and sets the new password. Rejects expired (> 24 h) or tampered tokens with a clear error message.
Template: registration/password_reset_confirm.html
PasswordResetDoneView / PasswordChangeDoneView / PasswordResetCompleteView
Section titled “PasswordResetDoneView / PasswordChangeDoneView / PasswordResetCompleteView”These three confirmation views display success pages after each step of the password flow. They render a template and accept no form input:
| View | Default template | Shown after |
|---|---|---|
PasswordResetDoneView |
registration/password_reset_done.html |
Reset email sent |
PasswordChangeDoneView |
registration/password_change_done.html |
Password changed |
PasswordResetCompleteView |
registration/password_reset_complete.html |
Reset confirmed |
Mount them alongside the other auth views:
from buraq.contrib.auth.views import ( PasswordResetDoneView, PasswordChangeDoneView, PasswordResetCompleteView,)
urlpatterns = [ ... path("/auth/password/reset/done", PasswordResetDoneView.as_view(), name="password_reset_done"), path("/auth/password/change/done", PasswordChangeDoneView.as_view(), name="password_change_done"), path("/auth/password/reset/complete", PasswordResetCompleteView.as_view(), name="password_reset_complete"),]PasswordResetTokenGenerator
Section titled “PasswordResetTokenGenerator”The underlying token generator is available directly if you need to create or validate reset links from your own code:
from buraq.contrib.auth import PasswordResetTokenGenerator
generator = PasswordResetTokenGenerator()
# Generate a token for a usertoken = generator.make_token(user) # e.g. "6b4t2c-abc12345..."
# Validate a token from a reset URLis_valid = generator.check_token(user, token) # → True / FalseTokens are HMAC-SHA256–signed and time-limited. The expiry window is controlled by PASSWORD_RESET_TIMEOUT in settings (default: 86 400 seconds / 24 hours):
PASSWORD_RESET_TIMEOUT = 3600 # 1 hourCustom user model
Section titled “Custom user model”Buraq ships AbstractBaseUser and AbstractUser for building custom user models, and PermissionsMixin for adding group/permission support.
from buraq.contrib.auth.models import AbstractBaseUser, PermissionsMixin
class StaffUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True) is_staff = models.BooleanField(default=False)
USERNAME_FIELD = "email"Use get_user_model() anywhere you need a reference to the active user model — it respects AUTH_USER_MODEL:
from buraq.contrib.auth.models import get_user_model
User = get_user_model()user = await User.objects.get(email="alice@example.com")AbstractUser is the concrete default user class (with username, email, is_staff, etc.) that you can subclass without reimplementing everything:
from buraq.contrib.auth.models import AbstractUser
class Profile(AbstractUser): bio = models.TextField(blank=True)Creating users
Section titled “Creating users”from buraq.contrib.auth.models import Userfrom buraq.contrib.auth import make_password
user = await User.objects.create( username = "alice", email = "alice@example.com", hashed_password = await make_password("secure-password"), is_active = True,)Creating a superuser via CLI
Section titled “Creating a superuser via CLI”buraq createsuperuserUser model fields
Section titled “User model fields”| Field | Type | Description |
|---|---|---|
username |
str |
Unique username |
email |
str |
Unique email |
hashed_password |
str |
Argon2 hash |
is_active |
bool |
Can log in (default: True) |
is_staff |
bool |
Admin panel access |
is_superuser |
bool |
All permissions |
is_authenticated |
bool |
Always True for real users |
date_joined |
datetime |
Account creation time |
last_login |
datetime |
Updated on each login() call |