aboutsummaryrefslogtreecommitdiff
path: root/jb/models/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'jb/models/auth.py')
-rw-r--r--jb/models/auth.py47
1 files changed, 40 insertions, 7 deletions
diff --git a/jb/models/auth.py b/jb/models/auth.py
index 5f31bd3..8b48088 100644
--- a/jb/models/auth.py
+++ b/jb/models/auth.py
@@ -1,5 +1,6 @@
import hashlib
import hmac
+from typing import Any
from pydantic import (
BaseModel,
@@ -8,6 +9,8 @@ from pydantic import (
Field,
TypeAdapter,
computed_field,
+ field_validator,
+ model_validator,
)
from jb.config import settings
@@ -30,29 +33,59 @@ def email_to_product_user_id(email: str) -> str:
return hmac.new(
key=salt_bytes,
msg=normalized_email.encode("utf-8"),
- digestmod=hashlib.sha256,
+ digestmod=hashlib.sha1,
).hexdigest()
-class AuthenticatedUser(BaseModel):
+class User(BaseModel):
"""A user that has been authenticated and exists in THL"""
email: EmailStr = Field()
+ display_name: str | None = Field(default=None, max_length=255)
+
+ @field_validator("email", mode="before")
+ @classmethod
+ def normalize_email(cls, value: Any) -> Any:
+ if not isinstance(value, str):
+ return value
+ if not value.isascii():
+ raise ValueError("email must contain ASCII characters only")
+ return value.lower()
+
+ @model_validator(mode="before")
+ @classmethod
+ def validate_product_user_id(cls, data: Any) -> Any:
+ if not isinstance(data, dict) or "product_user_id" not in data:
+ return data
+
+ provided_id = data["product_user_id"]
+ email = data.get("email")
+ if email is None:
+ return data
+
+ expected_id = email_to_product_user_id(str(email))
+ if not isinstance(provided_id, str) or not hmac.compare_digest(
+ provided_id, expected_id
+ ):
+ raise ValueError(f"product_user_id {provided_id} does not match email {email}")
+
+ # The computed field is authoritative; do not retain the input value.
+ validated_data = dict(data)
+ validated_data.pop("product_user_id")
+ return validated_data
+
@computed_field
+ @property
def product_user_id(self) -> str:
return email_to_product_user_id(self.email)
-class AccountCreate(BaseModel):
- email: EmailStr = Field()
-
-
class AccountLogin(BaseModel):
+ # There is no practical difference between an Account "login" and "create"
email: EmailStr = Field()
-
class MagicLinkExchangeRequest(BaseModel):
model_config = ConfigDict(extra="forbid")