Skip to content

Updating PyJWT to 2.10.0 broke login #831

Open
@jeremyqa

Description

@jeremyqa

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions