mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-20 11:41:35 +00:00
0450c74f53
Application token authentication currently uses Django's default password hasher. On our production hardware, token requests take at least ~500 ms. Authentication happens per user of each application, so this cost accumulates across frequently used integrations. Password hashers deliberately make guessing expensive to protect human-chosen passwords after a database leak. Our application secrets are generated server-side using a cryptographically secure random generator, with a default length of 128 alphanumeric characters. Guessing these secrets is already computationally infeasible, making password stretching an unnecessary CPU cost. Use salted SHA-256 for application secrets while retaining constant-time comparison, and keep user password hashing unchanged. Existing secrets migrate after successful verification without requiring key rotation. Conditional updates prevent migration from overwriting a concurrent rotation. Store the new hash in `client_secret_sha256` while preserving the original Django hash in `client_secret`. New applications populate both fields, allowing the previous release to authenticate both existing and newly created applications if we need to roll back. Once every application has migrated and the rollback window has closed, complete the migration by removing the legacy verification code and `client_secret` field. This assumes securely generated, high-entropy secrets. Deployments that reduce `APPLICATION_CLIENT_SECRET_LENGTH` or supply predictable secrets lose the offline guessing protection that the previous slow hasher provided.
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""Application secrets only: keep fast hashing out of PASSWORD_HASHERS.
|
|
|
|
Secrets must be securely randomly generated, not human-chosen.
|
|
"""
|
|
|
|
import hashlib
|
|
import re
|
|
|
|
from django.contrib.auth.hashers import check_password
|
|
from django.utils.crypto import constant_time_compare
|
|
from django.utils.encoding import force_bytes
|
|
|
|
CLIENT_SECRET_HASH_ALGORITHM = "sha256" # noqa: S105
|
|
CLIENT_SECRET_HASH_VERSION = "v0" # noqa: S105
|
|
CLIENT_SECRET_HASH_PREFIX = (
|
|
f"{CLIENT_SECRET_HASH_ALGORITHM}${CLIENT_SECRET_HASH_VERSION}$"
|
|
)
|
|
|
|
# Accept only the versioned format: sha256$v0$<digest>.
|
|
CLIENT_SECRET_HASH_PATTERN = re.compile(
|
|
rf"{re.escape(CLIENT_SECRET_HASH_PREFIX)}(?P<digest>[0-9a-f]{{64}})"
|
|
)
|
|
|
|
|
|
def _digest(raw_secret):
|
|
"""Return the hex SHA-256 digest of a raw secret."""
|
|
return hashlib.sha256(force_bytes(raw_secret)).hexdigest()
|
|
|
|
|
|
def hash_client_secret(raw_secret):
|
|
"""Hash a machine-generated application secret without key stretching."""
|
|
return f"{CLIENT_SECRET_HASH_PREFIX}{_digest(raw_secret)}"
|
|
|
|
|
|
def verify_client_secret(raw_secret, encoded):
|
|
"""Verify the versioned application format or a legacy Django password hash."""
|
|
if raw_secret is None:
|
|
return False
|
|
|
|
match = CLIENT_SECRET_HASH_PATTERN.fullmatch(encoded)
|
|
|
|
# Legacy path
|
|
if not match:
|
|
return check_password(raw_secret, encoded)
|
|
|
|
return constant_time_compare(match["digest"], _digest(raw_secret))
|