Open
Description
I don't usually write issues, so bear with me. We noticed our dev environment suddenly wasn't letting anyone log in. It turns out it's because PyJWT updated to 2.10.0, and that began enforcing that subject
be a string.
djangorestframework-simplejwt==5.3.1
├── Django [required: >=3.2, installed: 4.2.16]
│ ├── asgiref [required: >=3.6.0,<4, installed: 3.8.1]
│ └── sqlparse [required: >=0.3.1, installed: 0.5.2]
├── djangorestframework [required: >=3.12, installed: 3.14.0]
│ ├── Django [required: >=3.0, installed: 4.2.16]
│ │ ├── asgiref [required: >=3.6.0,<4, installed: 3.8.1]
│ │ └── sqlparse [required: >=0.3.1, installed: 0.5.2]
│ └── pytz [required: Any, installed: 2023.3]
└── PyJWT [required: >=1.7.1,<3, installed: 2.10.0]
In this section, the code only sometimes converts the id to a string
> /usr/local/lib/python3.11/site-packages/rest_framework_simplejwt/tokens.py(208)
197 @classmethod
198 def for_user(cls, user: AuthUser) -> "Token":
199 """
200 Returns an authorization token for the given user that will be provided
201 after authenticating the user's credentials.
202 """
204 user_id = getattr(user, api_settings.USER_ID_FIELD)
205 if not isinstance(user_id, int):
206 user_id = str(user_id)
That returns a seemingly fine token. However, when trying to use it, downstream from here
> /usr/local/lib/python3.11/site-packages/rest_framework_simplejwt/authentication.py(50)
40 def authenticate(self, request: Request) -> Optional[Tuple[AuthUser, Token]]:
42 header = self.get_header(request)
43 if header is None:
44 return None
45
46 raw_token = self.get_raw_token(header)
47 if raw_token is None:
48 return None
49
50 -> validated_token = self.get_validated_token(raw_token)
51
52 return self.get_user(validated_token), validated_token
vv
> /usr/local/lib/python3.11/site-packages/rest_framework_simplejwt/tokens.py(56)
37 def __init__(self, token: Optional["Token"] = None, verify: bool = True) -> None:
38 """
39 !!!! IMPORTANT !!!! MUST raise a TokenError with a user-facing error
40 message if the given token is invalid, expired, or otherwise not safe
41 to use.
42 """
43 if self.token_type is None or self.lifetime is None:
44 raise TokenError(_("Cannot create token with no type or lifetime"))
45
46 self.token = token
47 self.current_time = aware_utcnow()
48
49 # Set up token
50 if token is not None:
51 # An encoded token was provided
52 token_backend = self.get_token_backend()
53
54 # Decode token
55 try:
56 -> self.payload = token_backend.decode(token, verify=verify)
57 except TokenBackendError:
58 raise TokenError(_("Token is invalid or expired"))
59
All the way down in
[42] > /usr/local/lib/python3.11/site-packages/jwt/api_jwt.py(167)decode_complete()
-> self._validate_claims(
167 -> self._validate_claims(
168 payload,
169 merged_options,
170 audience=audience,
171 issuer=issuer,
172 leeway=leeway,
173 subject=subject,
174 )
it blows up
[43] > /usr/local/lib/python3.11/site-packages/jwt/api_jwt.py(273)_validate_claims()
-> self._validate_sub(payload, subject)
> /usr/local/lib/python3.11/site-packages/jwt/api_jwt.py(273)
272 if options["verify_sub"]:
273 -> self._validate_sub(payload, subject)
274
275 if options["verify_jti"]:
276 self._validate_jti(payload)
InvalidSubjectError: Subject must be a string
287 def _validate_sub(self, payload: dict[str, Any], subject=None) -> None:
288 """
289 Checks whether "sub" if in the payload is valid ot not.
290 This is an Optional claim
291
292 :param payload(dict): The payload which needs to be validated
293 :param subject(str): The subject of the token
294 """
295
296 if "sub" not in payload:
297 return
298
299 if not isinstance(payload["sub"], str):
300 -> raise InvalidSubjectError("Subject must be a string")
(Pdb++) payload
{'token_type': 'access', 'exp': 1732058200, 'iat': 1732054600, 'jti': '537bae19596045f09177d29195d2ed71', 'sub': 3}
because sub
is 3
and not "3"
, i guess.
Maybe if simplejwt passes verify_sub: False
when it calls jwt.decode in /usr/local/lib/python3.11/site-packages/rest_framework_simplejwt/backends.py(139)
it would work? PyJWT seems to merge options
166 merged_options = {**self.options, **options}
167 -> self._validate_claims(
168 payload,
169 merged_options,
170 audience=audience,
171 issuer=issuer,
172 leeway=leeway,
173 subject=subject,
174 )
175
176 decoded["payload"] = payload
177 return decoded
(Pdb++) self.options
{'verify_signature': True, 'verify_exp': True, 'verify_nbf': True, 'verify_iat': True, 'verify_aud': True, 'verify_iss': True, 'verify_sub': True, 'verify_jti': True, 'require': []}
(Pdb++) options
{'verify_aud': False, 'verify_signature': True}
(Pdb++) merged_options
{'verify_signature': True, 'verify_exp': True, 'verify_nbf': True, 'verify_iat': True, 'verify_aud': False, 'verify_iss': True, 'verify_sub': True, 'verify_jti': True, 'require': []}
This seems to stem from the changes added in jpadilla/pyjwt#1005