Technical Specification — Auth Backend (Django REST) for a Sharetribe‑like Marketplace
0) Context & Goal
Backend API to power marketplace authentication and account creation for two primary roles: customer and business. This spec covers only the Auth surface required by the Vue SPA’s Login & Sign‑up flows, with production‑grade security, validation, throttling, email verification, and observability.
1) Stack & Versions
- Python: 3.10+
- Django: 5.x (ASGI‑ready)
- Django REST Framework: 3.15+
- Auth:
djangorestframework-simplejwt(rotating refresh tokens) - DB: PostgreSQL 14+ (UUID PKs)
- Cache/Rate limit: Redis
- Task queue (emails): Celery + Redis broker
- OpenAPI: drf‑spectacular
2) Project Structure
backend/
manage.py
pyproject.toml
/config
__init__.py
settings.py
urls.py
asgi.py
/apps
/accounts
__init__.py
apps.py
admin.py
models.py
validators.py
serializers.py
views.py
urls.py
signals.py
tokens.py
tasks.py
tests/
test_auth_api.py
test_models.py
3) Data Model
3.1 User
# apps/accounts/models.py (excerpt)
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
username = None # use email as unique ID
email = models.EmailField(unique=True, db_index=True)
class Role(models.TextChoices):
CUSTOMER = "customer", "Customer"
BUSINESS = "business", "Business"
role = models.CharField(max_length=16, choices=Role.choices, default=Role.CUSTOMER)
email_verified = models.BooleanField(default=False)
marketing_opt_in = models.BooleanField(default=False)
USERNAME_FIELD = "email"
REQUIRED_FIELDS: list[str] = []
3.2 BusinessProfile (conditional)
class BusinessProfile(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.OneToOneField("accounts.User", on_delete=models.CASCADE, related_name="business")
business_name = models.CharField(max_length=120)
website = models.URLField(blank=True)
phone_e164 = models.CharField(max_length=20, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Signal on user creation (role=business) to auto‑create BusinessProfile.
4) API Endpoints
Base path: /api/v1. JSON only. All times UTC ISO‑8601.
4.1 POST /auth/register
Request
{
"role": "customer" | "business",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"password": "S3cure!Pass",
"business": {"businessName": "Ada Services", "website": "https://ada.services", "phone": "+12125551212"},
"acceptTos": true,
"marketingOptIn": false
}
201 Response
{
"user": {
"id": "uuid",
"role": "customer",
"email": "ada@example.com",
"firstName": "Ada",
"lastName": "Lovelace",
"emailVerified": false
},
"tokens": {"access":"<jwt>","refresh":"<jwt>"},
"requiresEmailVerification": true
}
Errors: EMAIL_IN_USE, WEAK_PASSWORD, VALIDATION_ERROR
4.2 POST /auth/login
Request
{ "email": "user@example.com", "password": "••••••••" }
200 Response
{ "user": { ... }, "tokens": {"access":"<jwt>","refresh":"<jwt>"} }
401 INVALID_CREDENTIALS (generic)
4.3 POST /auth/token/refresh
{ "refresh": "<jwt>" } → { "access": "<jwt>" }
4.4 POST /auth/logout
{} → 204 (server blacklists/rotates refresh token)
4.5 POST /auth/forgot-password
{ "email": "user@example.com" } → 204 (always)
4.6 POST /auth/reset-password
{ "token": "<opaque>", "password": "NewPass!23" } → 204
Errors: TOKEN_INVALID, WEAK_PASSWORD
4.7 GET /auth/verify-email
Query: ?token=<opaque> → 302 to success/failure page (for web) or 200 JSON (for SPA fetch)
4.8 GET /me
Auth: Bearer access token → 200 { user profile, role, emailVerified, business? }
4.9 PATCH /me
Partial updates (firstName, lastName, marketingOptIn, business fields if role=business)
Note: Cookie‑based refresh is supported as an alternative deployment (httpOnly, SameSite=Lax). In that mode /auth/token/refresh reads refresh from cookie and returns only access.
5) Security
- Passwords: Django PBKDF2 hasher (default),
AUTH_PASSWORD_VALIDATORSenabled. - JWT: Access 5–15 min; Refresh 14–30 days; rotation + blacklist on logout; include
jti. - PII: Never log email/password; structure logs with request IDs.
- CORS: Allow only known origins; send
Vary: Origin. - CSRF: If cookies are used, enable CSRF tokens for unsafe methods.
- Email enumeration: Forgot/reset/verif endpoints return generic responses.
- HTTPS only:
SECURE_*settings, HSTS,SESSION_COOKIE_SECURE. - Brute force: DRF throttle + IP/user lockouts after N failures.
6) Validation & Business Rules
- Email: RFC validation + MX optional.
- Password: ≥8 chars; at least 3/4 classes; no leading/trailing spaces.
- Names: 1–80 chars.
- Business: name 2–120; website URL; phone E.164.
- Uniqueness: email unique (case‑insensitive).
- ToS: required to create account; store timestamp & version.
7) Email & Verification
- Send transactional emails via provider (SES/Sendgrid/Postmark).
- On register: issue
EmailVerificationToken(time‑boxed, single‑use) and Celery task to send template with CTA. - On forgot password: issue reset token (time‑boxed, single‑use).
- Tokens stored either as signed serializer (
TimestampSigner) or DB model w/jti+ expiry.
# apps/accounts/tokens.py (concept)
from itsdangerous import URLSafeTimedSerializer
ts = URLSafeTimedSerializer(settings.SECRET_KEY, salt="email-verify")
# generate: ts.dumps({"uid": str(user.id), "jti": uuid4().hex})
# verify: payload = ts.loads(token, max_age=86400)
8) Rate Limits & Abuse Controls
- Login: 10/min per IP, 30/hr per IP, 10/hr per user email.
- Register: 5/min per IP.
- Forgot/Reset: 5/hr per IP + per email.
- On crossing thresholds, respond
429withRetry-After. - Optional CAPTCHA (score‑based) when throttle nears thresholds.
9) Settings (snippets)
# config/settings.py (excerpts)
AUTH_USER_MODEL = "accounts.User"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.UserRateThrottle",
"rest_framework.throttling.AnonRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"user": "1000/day",
"anon": "200/day",
"login": "10/min",
},
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10),
"REFRESH_TOKEN_LIFETIME": timedelta(days=21),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"AUTH_HEADER_TYPES": ("Bearer",),
"SIGNING_KEY": env("JWT_SIGNING_KEY"),
}
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
SECURE_HSTS_SECONDS = 31536000
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
10) Observability
- Structured JSON logging (uvicorn/gunicorn) with request ID middleware.
- Audit fields: created_at/updated_at; last_login captured.
- Metrics: counters for register/login success/failure, 401/429, verification sends.
- Error tracking: Sentry with PII scrubbing.
- OpenAPI served at
/api/schema/; Swagger UI at/api/docs/.
11) Testing
- Unit: serializers, validators, token utils.
- API: register, login, refresh, logout, forgot, reset, verify (happy + edge + throttled).
- Factories: factory_boy for User and BusinessProfile.
- Fixtures: email backend (locmem), Redis fakes.
- Coverage: >= 85% for accounts app.
# apps/accounts/tests/test_auth_api.py (sketch)
def test_register_customer(api_client):
res = api_client.post("/api/v1/auth/register", { ... }, format="json")
assert res.status_code == 201
assert res.data["requiresEmailVerification"] is True
12) Acceptance Criteria
- API passes all tests; OpenAPI docs accurate.
- JWT rotation/blacklist works; logout invalidates refresh.
- Email verification flow functional end‑to‑end.
- Throttling active with
Retry-Afterheaders. - Security headers and HTTPS enforced.
- Interoperable with Vue SPA spec (payloads match).
13) Out of Scope (now)
- Marketplace domain models (listings, bookings, payouts).
- SSO (SAML/OIDC), social OAuth (Google/Apple).
- Admin dashboards & staff impersonation.
Appendix A — Serializers (sketch)
# apps/accounts/serializers.py (excerpts)
class RegisterSerializer(serializers.Serializer):
role = serializers.ChoiceField(choices=["customer","business"], default="customer")
firstName = serializers.CharField(max_length=80)
lastName = serializers.CharField(max_length=80)
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
acceptTos = serializers.BooleanField()
marketingOptIn = serializers.BooleanField(required=False, default=False)
business = BusinessSerializer(required=False)
def validate_email(self, email):
if User.objects.filter(email__iexact=email).exists():
raise serializers.ValidationError("EMAIL_IN_USE")
return email
def validate(self, attrs):
validate_password(attrs["password"]) # uses AUTH_PASSWORD_VALIDATORS
if not attrs.get("acceptTos"):
raise serializers.ValidationError({"acceptTos": "REQUIRED"})
return attrs
def create(self, data):
role = data.pop("role")
business_data = data.pop("business", None)
user = User.objects.create_user(
email=data["email"],
password=data["password"],
first_name=data["firstName"],
last_name=data["lastName"],
role=role,
marketing_opt_in=data.get("marketingOptIn", False),
)
if role == User.Role.BUSINESS and business_data:
BusinessProfile.objects.create(
user=user,
business_name=business_data["businessName"],
website=business_data.get("website",""),
phone_e164=business_data.get("phone",""),
)
return user
Appendix B — URLs (sketch)
# apps/accounts/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("auth/register", views.RegisterView.as_view()),
path("auth/login", views.LoginView.as_view()), # can proxy SimpleJWT token obtain pair
path("auth/token/refresh", views.TokenRefreshView.as_view()),
path("auth/logout", views.LogoutView.as_view()),
path("auth/forgot-password", views.ForgotPasswordView.as_view()),
path("auth/reset-password", views.ResetPasswordView.as_view()),
path("auth/verify-email", views.VerifyEmailView.as_view()),
path("me", views.MeView.as_view()),
]