wps_forward_bot
🚀 New issue to wemake-services/django-modern-rest by @vyhuholl
📝 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 example
Reasoning
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
1 · 345 ·