Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions docs/cookie_authentication.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
.. _cookie_authentication:

Cookie-based Authentication
===============================

Overview
--------

``JWTCookieAuthentication`` is an optional authentication backend that
authenticates JSON Web Tokens (JWTs) **exclusively from HttpOnly cookies**.

Unlike ``JWTAuthentication``, this backend **does not read tokens from the
Authorization header**. Instead, it expects JWTs to already be present in
cookies attached to the incoming request.

This authentication method is intended for browser-based applications
where storing JWTs in HttpOnly cookies is preferred over exposing tokens
to JavaScript.

---

How it works
------------

The ``JWTCookieAuthentication`` backend behaves as follows:

* Reads the JWT from a configured cookie (default: ``access``)
* If the cookie is missing, authentication is skipped
* If the cookie contains an invalid or expired token, authentication fails
* The Authorization header is intentionally ignored

This backend only handles **authentication**. It does not issue tokens
or set cookies.

---

Enabling cookie authentication
------------------------------

To enable cookie-based authentication, configure Django REST Framework
to use ``JWTCookieAuthentication``:

.. code-block:: python

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTCookieAuthentication",
),
}

Configure the cookie name using ``SIMPLE_JWT`` settings:

.. code-block:: python

SIMPLE_JWT = {
"AUTH_COOKIE": "access",
}

---

Token issuance and login
------------------------

``JWTCookieAuthentication`` **does not create or set cookies**.

The default SimpleJWT token views (``TokenObtainPairView`` and
``TokenRefreshView``) return tokens in the response body and **do not store
tokens in cookies**.

When using cookie-based authentication, JWTs **must already be present in
cookies**. This typically requires using **cookie-aware login and refresh
views** that store tokens in HttpOnly cookies.

Example of setting a cookie after successful authentication:

.. code-block:: python

response.set_cookie(
"access",
access_token,
max_age=3600,
path="/",
secure=True,
httponly=True,
samesite="Lax",
)

The responsibility for issuing tokens and setting cookies lies outside
of this authentication backend.

---

Refresh tokens
--------------

If refresh tokens are used with cookie-based authentication, they should
also be stored in HttpOnly cookies and handled by cookie-aware refresh
views.

The default ``TokenRefreshView`` expects refresh tokens in the request
body and does not read cookies.

---

Security considerations
-----------------------

When using cookie-based JWT authentication, the following security
requirements apply:

* Cookies **must** be marked ``HttpOnly`` to prevent access from JavaScript
* Cookies **should** be marked ``Secure`` when used over HTTPS
* CSRF protection **must be enabled** for unsafe HTTP methods
* This backend is recommended only for browser-based clients

Failure to enforce CSRF protection when using cookies may expose the
application to cross-site request forgery attacks.

---

When to use this backend
-----------------------

Use ``JWTCookieAuthentication`` if:

* You are building a browser-based application
* You want to avoid storing JWTs in JavaScript-accessible storage
* You prefer HttpOnly cookie-based authentication

Do **not** use this backend if:

* You rely on Authorization headers for authentication
* You are building APIs intended for third-party clients
* You require stateless, header-only authentication
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Contents
token_types
blacklist_app
stateless_user_authentication
cookie_authentication
development_and_contributing
drf_yasg_integration
rest_framework_simplejwt
Expand Down
21 changes: 20 additions & 1 deletion rest_framework_simplejwt/authentication.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, TypeVar
from typing import Any, Optional, TypeVar

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -176,6 +177,24 @@ def get_user(self, validated_token: Token) -> AuthUser:
JWTTokenUserAuthentication = JWTStatelessUserAuthentication


class JWTCookieAuthentication(JWTAuthentication):
cookie_name: str = api_settings.AUTH_COOKIE

def authenticate(self, request: Request) -> tuple[AuthUser, Token] | None:
raw_token: str | None = self.get_cookie(request).get(self.cookie_name)

if raw_token is None:
return None

validated_token = self.get_validated_token(raw_token)
user = self.get_user(validated_token)

return user, validated_token

def get_cookie(self, request: Request) -> dict[str, str]:
return request._request.COOKIES


def default_user_authentication_rule(user: AuthUser | None) -> bool:
# Prior to Django 1.10, inactive users could be authenticated with the
# default `ModelBackend`. As of Django 1.10, the `ModelBackend`
Expand Down
1 change: 1 addition & 0 deletions rest_framework_simplejwt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"CHECK_REVOKE_TOKEN": False,
"REVOKE_TOKEN_CLAIM": "hash_password",
"CHECK_USER_IS_ACTIVE": True,
"AUTH_COOKIE": "access",
}

IMPORT_STRINGS = (
Expand Down
42 changes: 42 additions & 0 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory

from rest_framework_simplejwt import authentication
Expand Down Expand Up @@ -277,3 +278,44 @@ def username(self):

# Restore default TokenUser for future tests
api_settings.TOKEN_USER_CLASS = temp


class TestJWTCookieAuthentication(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.backend = authentication.JWTCookieAuthentication()

self.user = User.objects.create_user(
username="anirudhk", password="password123", is_active=True
)

self.access_token = str(AccessToken.for_user(self.user))

def test_cookie_authentication(self) -> None:
django_request = self.factory.get("/protected-cookie/")
django_request.COOKIES[api_settings.AUTH_COOKIE] = self.access_token

result = self.backend.authenticate(Request(django_request))

self.assertIsNotNone(result)

user, token = result

self.assertEqual(user, self.user)
self.assertIsInstance(token, AccessToken)

def test_authenticate_without_token(self) -> None:
dj_request = self.factory.get("/cookie-protected/")
request = Request(dj_request)

result = self.backend.authenticate(request)

self.assertIsNone(result)

def test_authenticate_with_invalid_cookie(self) -> None:
dj_request = self.factory.get("/cookie-protected/")
dj_request.COOKIES[api_settings.AUTH_COOKIE] = "invalid_token_value"

request = Request(dj_request)
with self.assertRaises(AuthenticationFailed):
self.backend.authenticate(request)
5 changes: 5 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@
re_path(r"^token/verify/$", jwt_views.token_verify, name="token_verify"),
re_path(r"^token/blacklist/$", jwt_views.token_blacklist, name="token_blacklist"),
re_path(r"^test-view/$", views.test_view, name="test_view"),
re_path(
r"^cookie-protected/$",
views.cookie_protected_view,
name="cookie_protected_view",
),
]
11 changes: 11 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ def get(self, request):


test_view = TestView.as_view()


class CookieProtectedView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (authentication.JWTCookieAuthentication,)

def get(self, request):
return Response({"foo": "bar"})


cookie_protected_view = CookieProtectedView.as_view()