📝 Reusable controllers to issue JWT tokens as cookies (#1290)
FEATURE
Thesis
Follow-up to #1287. That PR added
CookieJWTSyncAuth / CookieJWTAsyncAuth, which read a JWT from a cookie. Nothing in the framework writes one, so the issuing half is still left to every user.docs/pages/auth/jwt.rst currently carries a .. todo:: in place of an example, and this issue is that todo.What is needed
1. Obtain: authenticate and set the access and refresh cookies
2. Refresh: read the refresh token from its cookie and rotate both
3. Log out: clear both cookies, ideally blocklisting the access token (there is no logout controller today at all)
The design obstacle
This is the part that needs a decision, and it is why the todo is still a todo rather than a patch.
Cookie values are only known at request time, but both ways of declaring cookies fix them at decoration time:
•
@modify(cookies=...) takes NewCookie instances, and ModifyEndpointPayload.actionable_cookies() returns exactly those objects. Static values only.•
@validate takes ResponseSpec(cookies=...), which accepts only CookieSpec, that is a description and not a value. Runtime values then go through self.to_response(..., cookies={...: NewCookie(value=...)}).The second one works, and it is what a hand-written controller does today. But in a reusable controller the decorator runs once, on the base class. The cookie names and flags would be frozen there, and a subclass could only change them by redefining
post completely, which removes the reason to have a reusable controller in the first place.Note that per-subclass data does already reach endpoint metadata:
Controller.__init_subclass__ builds an Endpoint per concrete subclass, and ResponseSpecProvider.provide_response_specs receives controller_cls. So a hook is feasible. The open question is its shape, not whether it can exist.Open questions
• Where do the cookie names and flags come from?
ClassVars on the controller like the existing jwt_* settings, dmr settings, or a dedicated spec object?• Should the tokens still appear in the response body? Putting them in both places is convenient and undoes the point of
httponly.• Does logout blocklist the access token, or stay transport-only?
• How is
Set-Cookie represented in the generated OpenAPI schema when the names are configurable?Requirements for whatever we build
•
httponly=True and secure=True by default, and samesite no weaker than 'lax'• The refresh cookie scoped by
path to the refresh endpoint, so it is not sent to the rest of the API• Cookie names matching the auth side, which defaults to
DEFAULT_ACCESS_COOKIE (access_token) and DEFAULT_REFRESH_COOKIE (refresh_token) in dmr/security/jwt/cookie.py• Sync and async variants, like every other controller here
• Working together with the CSRF check in
CookieJWTSyncAuth• The
.. todo:: in docs/pages/auth/jwt.rst replaced by a real exampleReasoning
The cookie flags are the entire security surface of this flow. An example is copied verbatim far more often than it is read, and a copy that drops
httponly hands the token to any XSS on the page. So the docs deliberately ship no example until there is a controller that gets the defaults right.That is the same reasoning behind
ObtainTokensSyncController for the body flow: users should not have to reassemble the security-critical parts.#feature #help_wanted #django_modern_rest
sent via relator