diff --git a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0009-jwt-in-session-cookie.rst b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0009-jwt-in-session-cookie.rst index d48b974cf8..dc949cf70e 100644 --- a/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0009-jwt-in-session-cookie.rst +++ b/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0009-jwt-in-session-cookie.rst @@ -61,8 +61,8 @@ Login -> Cookie -> API * Contains only the public key signature portion of the JWT. * Enable HTTPOnly_ so the signature is unavailable to JS code. See `JWT Cookie Security`_ below. -#. **Automatically recombine and extract the JWT from Cookies on API calls.** - * We will create a new middleware that will reconstitute the divided JWT from its two cookies and store the +#. **Automatically recombine and extract the JWT from Cookies on API calls.** + * A new middleware JwtAuthCookieMiddleware will reconstitute the divided JWT from its two cookies and store the recombined JWT in a temporary cookie specified by JWT_AUTH_COOKIE_. * The `Django Rest Framework JWT`_ library we use makes use of the JWT_AUTH_COOKIE_ configuration setting. When set, the JSONWebTokenAuthentication_ class `automatically extracts the JWT from the cookie`_. Since all @@ -81,12 +81,19 @@ Login -> Cookie -> API * To prevent this issue, we will introduce a new HTTP header called "HTTP_USE_JWT_COOKIE" that will be selectively set only by microfrontends that want to use JWT cookie based authentication. The new middleware will check for this header before trying to reconstitute and use the JWT token. + * Additionally, select login-required APIs can be updated to redirect the caller to the Login page when the JWT + expires. This can be accomplished by enabling `JwtRedirectToLoginIfUnauthenticatedMiddleware`_ in the Django + service and updating the API to require the `LoginRedirectIfUnauthenticated`_ permission class. The middleware + automatically sets "HTTP_USE_JWT_COOKIE" for incoming requests to APIs that require the + `LoginRedirectIfUnauthenticated`_ permission. .. _`Lightrail's design`: https://medium.com/lightrail/getting-token-authentication-right-in-a-stateless-single-page-application-57d0c6474e3 .. _Django Rest Framework JWT: https://getblimp.github.io/django-rest-framework-jwt/ .. _JWT_AUTH_COOKIE: https://github.com/GetBlimp/django-rest-framework-jwt/blob/master/docs/index.md#jwt_auth_cookie .. _JSONWebTokenAuthentication: https://github.com/GetBlimp/django-rest-framework-jwt/blob/0a0bd402ec21fd6b9a5f715d114411836fbb2923/rest_framework_jwt/authentication.py#L71 .. _automatically extracts the JWT from the cookie: https://github.com/GetBlimp/django-rest-framework-jwt/blob/0a0bd402ec21fd6b9a5f715d114411836fbb2923/rest_framework_jwt/authentication.py#L86-L87 +.. _JwtRedirectToLoginIfUnauthenticatedMiddleware: https://github.com/edx/edx-drf-extensions/blob/0351010f1836e4cebd6bdc757d477b2f56265b17/edx_rest_framework_extensions/auth/jwt/middleware.py#L76 +.. _LoginRedirectIfUnauthenticated: https://github.com/edx/edx-drf-extensions/blob/0351010f1836e4cebd6bdc757d477b2f56265b17/edx_rest_framework_extensions/permissions.py#L147 JWT Cookie Lifetime @@ -106,7 +113,7 @@ JWT Cookie Lifetime #. **Revocation with short-lived JWTs** Given the tradeoff between long-lived JWTs versus immediacy of revocation, we need to configure an appropriate expiration value for JWT cookies. In a future world with an API gateway, we *may* have longer lived JWTs with a *stateful* check against a centralized `JWT blacklist`_ and each JWT uniquely - identified by a `JWT ID (jti)`_. In the meantime, we will err on the side of security and have short-lived JWTs. + identified by a `JWT ID (jti)`_. In the meantime, we will err on the side of security and have short-lived JWTs. #. **Refresh JWT Cookies.** When a JWT expires, we do not want to ask the user to login again while their browser session remains alive. A microfrontend will detect JWT expiration upon receiving a 401 response from an API @@ -129,7 +136,7 @@ JWT Cookie Content supported by a browser. `Modern browsers have treated this requirement as a maximum`_ - and hence do not support more than 4096 bytes. Our current JWT size is about 970 bytes (varying with size of user identifiers, like user's name, etc). (Side note: Signing a JWT with a 2048 byte asymmetric key increases the JWT's size by 325 bytes.) - + To minimize the JWT's size from the start, we should eliminate any unnecessary data that is `currently embedded in the JWT`_. For example: @@ -149,12 +156,12 @@ JWT Cookie Security #. **Enable CSRF Protection.** Storing JWTs in HTTP cookies is potentially vulnerable to CSRF attacks. See `JWT Cookie Storage Security`_. To protect against this: - + * Enable the HttpOnly_ flag on the **"JWT Signature Cookie"**, so Javascript code cannot misuse the JWT. * Enable the Secure_ flag on the cookie, so it will not be sent (and thus leaked) through an unencrypted channel. * Enable `Django's CSRF middleware`_ for every response. * Ensure all GET requests are side-effect free. - + * Note: The `same-origin policy`_ protects against CSRF attacks on GET requests since the rogue website cannot access the response from the GET request. * However, even though the rogue website cannot access the response, the GET request is still processed on the @@ -181,7 +188,7 @@ JWT Cookie Security Consequences ------------ -#. Since session cookies have a limited size of `at least 4096 bytes`_, we will need to monitor its size increase +#. Since HTTP cookies have a limited size of `at least 4096 bytes`_, we will need to monitor its size increase over time and implement a warning before it exceeds the size. Having this hard limit requires us to be judicious of what data is included in the JWT. A bloated JWT is not necessarily a benefit to overall web performance. @@ -199,6 +206,7 @@ Consequences #. Continue to use and expand the current `JS-accessible user-info cookie`_, which contains user-data. #. Have the server populate the initial DOM with this data, but this would only work for server-generated HTML. +.. _at least 4096 bytes: http://browsercookielimits.squawky.net/ .. _JWT sessionStorage and localStorage Security: https://stormpath. com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage#so-whats-the-difference .. _JS-accessible user-info cookie: https://github.com/edx/edx-platform/blob/70d1ca474012b89e4c7184d25499eb87b3135409/common/djangoapps/student/cookies.py#L151