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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Follows RFC 7235, where authentication failures should return 401.
- Clearer for clients: signals an auth issue instead of suggesting the endpoint is missing.

### Bugfixes
- Fixed OpenAPI schema generation for token obtain endpoints by explicitly
declaring dynamically returned fields (`access`, `refresh`) as read-only
serializer fields.

## 5.5.1

Expand Down
5 changes: 5 additions & 0 deletions rest_framework_simplejwt/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ def get_token(cls, user: AuthUser) -> Token:
class TokenObtainPairSerializer(TokenObtainSerializer):
token_class = RefreshToken

# Dynamically add read-only fields for schema generation only
access = serializers.CharField(read_only=True)
refresh = serializers.CharField(read_only=True)

def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
data = super().validate(attrs)

Expand All @@ -90,6 +94,7 @@ def validate(self, attrs: dict[str, Any]) -> dict[str, str]:

class TokenObtainSlidingSerializer(TokenObtainSerializer):
token_class = SlidingToken
token = serializers.CharField(read_only=True)

def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
data = super().validate(attrs)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ def test_it_should_produce_a_json_web_token_when_valid(self):
# token should not raise an exception.
SlidingToken(s.validated_data["token"])

def test_token_field_is_read_only_for_schema(self):
serializer = TokenObtainSlidingSerializer()
self.assertIn("token", serializer.fields)
self.assertTrue(serializer.fields["token"].read_only)


class TestTokenObtainPairSerializer(TestCase):
def setUp(self):
Expand Down Expand Up @@ -185,6 +190,45 @@ def test_it_should_produce_a_json_web_token_when_valid(self):
AccessToken(s.validated_data["access"])
RefreshToken(s.validated_data["refresh"])

def test_access_and_refresh_fields_are_read_only_for_schema(self):
"""
Ensure 'access' and 'refresh' fields are read-only so
automatic schema generators (drf-yasg, drf-spectacular) detect them.
"""
serializer = TokenObtainPairSerializer()
# access and refresh should exist
self.assertIn("access", serializer.fields)
self.assertIn("refresh", serializer.fields)
# read-only ensures schema generators know they are output-only
self.assertTrue(serializer.fields["access"].read_only)
self.assertTrue(serializer.fields["refresh"].read_only)

def test_schema_fields_do_not_break_runtime_validation(self):
"""
Ensure the patch doesn't interfere with normal validation and token creation.
"""
serializer = TokenObtainPairSerializer(
context=MagicMock(),
data={
TokenObtainPairSerializer.username_field: self.username,
"password": self.password,
},
)
self.assertTrue(serializer.is_valid())
validated_data = serializer.validated_data
self.assertIn("access", validated_data)
self.assertIn("refresh", validated_data)

# Tokens should be valid
access_token = AccessToken(validated_data["access"])
refresh_token = RefreshToken(validated_data["refresh"])

# Validate token type and some claims, but don't assert JTI equality
self.assertEqual(access_token["token_type"], "access")
self.assertEqual(refresh_token["token_type"], "refresh")
self.assertEqual(access_token["user_id"], str(self.user.id))
self.assertEqual(refresh_token["user_id"], str(self.user.id))


class TestTokenRefreshSlidingSerializer(TestCase):
def setUp(self):
Expand Down