## 1. "Why Does the Backend Not Know Who I Am?" (The Problem Begins) {#sec-ffb74987f051} OAuth2, JWT, session authentication… there are countless ways to authenticate, and for most projects they’re sufficient. I used them all. * When I added **OAuth2** to a custom email client or ChatGPT’s MyGPT, it felt like a real user experience. * A single‑page Django app works best with **session authentication**. * In a front‑end/back‑end split, **JWT** was the cleanest solution. But there’s a point where this combination collapses: **asynchronous work (Celery)**. When a user clicks a button, the request isn’t handled directly by the back‑end; it’s handed off to a distant AI compute server or worker. The worker then asks: > "I received the request, but whose behalf am I acting for? There is no `request.user`." ![A robot worker holding an API key and delivering a letter](/media/editor_temp/6/b63803ff-21ba-4f1b-a5af-47c8ca0fdd25.png) ## 2. The Pain of "Backend ↔ Backend" + "Async Worker (Celery)" {#sec-a7eab2678145} The turning point for me was the architecture where **backend services talk to each other** and a **Celery worker sits in the middle**. The flow isn’t: 1. User sends a web request 2. Backend enqueues a "job" 3. Celery worker consumes the job and makes an **asynchronous call** to some compute service The hardest part of this chain is that the worker: * Has no `request.user` * Has no session (it isn’t a browser) * Finds JWT handling ambiguous – who issues the token, where is it stored, how is it passed? * Cannot use OAuth2 because that flow assumes a user interaction When JWT and sessions become powerless, the question reduces to a single one: > "How does the compute server know which tenant/user this job belongs to?" ## 3. Workers Need Identification Before Authentication {#sec-77f5863a0a93} In a normal web request, **authentication = login = a user**. A worker, however, is an **application process** that borrows CPU – it needs to be *identified* first. Authentication can be handled later with HMAC or a secret key. * The job must run on **Customer A’s data** * The result must be stored under **Customer A’s resources** * Billing, permissions, and quota must be deducted for **Customer A** Trying to force JWT or session logic into this scenario inflates the token‑issuance, storage, transmission, expiration, and rotation design. More importantly, it feels fundamentally wrong to “issue a JWT for a user who isn’t actively using a browser”. The discomfort was enough to discard that approach. ## 4. Solution: API Key Was Simple and Powerful {#sec-2110738d8481} Enter **API Key**. It solved the problem in one stroke. * The worker adds **one header** to authenticate and identify itself. * The receiving service maps that key instantly to a **specific user/customer**. * Rotating or revoking keys becomes straightforward. For example, the back‑end would call the compute service like this: ```http POST /v1/ai/jobs Authorization: Api-Key Content-Type: application/json { "job_id": "...", "payload": {...} } ``` The worker doesn’t need a `request.user`; the downstream service uses the API key to resolve the user. ## 5. Game‑Changer: Tying API Keys to Users Simplifies Operations {#sec-762242363071} What really impressed me was the ability to bind the key to a **user model**. The generic `rest_framework_api_key` library provides keys, but my use‑case required a **key ↔ user relationship**. By subclassing `AbstractAPIKey` into `CustomAPIKey` and adding a foreign key to `AUTH_USER_MODEL`, the integration became seamless. ```python from django.conf import settings from rest_framework_api_key.models import AbstractAPIKey from django.db import models class CustomAPIKey(AbstractAPIKey): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" ) is_test = models.BooleanField(default=False) # stage / test keys ``` Now authentication turned into a full‑featured **operational lever**. ## 6. Benefits of Auto‑Issuing a Key Per User {#sec-d180d12a94f2} Generating a key automatically when a user signs up unlocked several conveniences. ### 1) Easy validity management {#sec-bfe883fb0c50} * Look up, deactivate, or delete a specific user’s keys with a single query. * When a user leaves, cascade deletion cleans up associated keys. ### 2) Straightforward key rotation {#sec-7f4bccda836d} * If a key is suspected of being compromised, issue a new one and retire the old. * Supporting multiple keys enables **zero‑downtime replacement** (new key rolls out, old key is retired later). ### 3) Billing, quota, and permission tied to the user {#sec-1081bea2890b} * Charge or limit **per user**, not per key. * No need to infer “whose request is this?” each time. ### 4) Multiple keys per user {#sec-1bc8fa524379} The `is_test` flag shines here. Because the relationship is a **FK rather than OneToOne**, a single user can own several keys for different purposes. * A **stage‑mode key** and a **production key** can coexist. * Development and production flows stay isolated. * Logging can clearly separate **test traffic** from **real traffic**. ## 7. Authentication Choices Are About Context, Not Hierarchy {#sec-f73d5faf76f6} In summary, the optimal combo I keep using looks like this: * **OAuth2** – best for external services where user consent is essential. * **Session authentication** – ideal for a pure Django app; fast and simple. * **JWT** – fits front‑end/back‑end separation, mobile, SPA, etc. * **API Key** – shines in back‑end‑to‑back‑end, automation, workers, and batch jobs where there is *no* user interaction. When a Celery worker enters the picture, trying to force a “login‑based” auth model only adds complexity. API keys become the clean escape hatch. ## 8. Closing Thoughts {#sec-d746e56908e8} Humans (browsers/apps) are naturally handled with sessions, JWTs, or OAuth2. Workers, however, are processes that need to be **identified**. I switched to API keys not because of a grand security theory, but because it was the simplest solution for that specific gap. By tying the key to a user, key management turned from a nuisance into a powerful operational lever. Do you use API keys often? The pattern I described is just one slice of their convenience, but I hope this experience gives you a useful perspective. --- **Related Posts** - [Protect Server‑to‑Server Requests with HMAC Signatures in Django/DRF](/ko/whitedec/2025/12/9/django-drf-hmac-signature-server-to-server-integrity/) - [Lessons from the React RCE Incident: HMAC, Key Rotation, and Zero Trust](/ko/whitedec/2025/12/8/react-rce-lesson-hmac-key-rotation-zero-trust/)