Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jwt/api_jwt.py: 60%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

193 statements  

1from __future__ import annotations 

2 

3import json 

4import os 

5import warnings 

6from calendar import timegm 

7from collections.abc import Container, Iterable, Sequence 

8from datetime import datetime, timedelta, timezone 

9from typing import TYPE_CHECKING, Any, Union, cast 

10 

11from .api_jws import PyJWS, _ALGORITHM_UNSET, _jws_global_obj 

12from .exceptions import ( 

13 DecodeError, 

14 ExpiredSignatureError, 

15 ImmatureSignatureError, 

16 InvalidAudienceError, 

17 InvalidIssuedAtError, 

18 InvalidIssuerError, 

19 InvalidJTIError, 

20 InvalidSubjectError, 

21 MissingRequiredClaimError, 

22) 

23from .warnings import RemovedInPyjwt3Warning 

24 

25if TYPE_CHECKING or bool(os.getenv("SPHINX_BUILD", "")): 

26 import sys 

27 

28 if sys.version_info >= (3, 10): 

29 from typing import TypeAlias 

30 else: 

31 # Python 3.9 and lower 

32 from typing_extensions import TypeAlias 

33 

34 from .algorithms import AllowedPrivateKeys, AllowedPublicKeys 

35 from .api_jwk import PyJWK 

36 from .types import FullOptions, Options, SigOptions 

37 

38 AllowedPrivateKeyTypes: TypeAlias = Union[AllowedPrivateKeys, PyJWK, str, bytes] 

39 AllowedPublicKeyTypes: TypeAlias = Union[AllowedPublicKeys, PyJWK, str, bytes] 

40 

41 

42class PyJWT: 

43 def __init__(self, options: Options | None = None) -> None: 

44 self.options: FullOptions 

45 self.options = self._get_default_options() 

46 if options is not None: 

47 self.options = self._merge_options(options) 

48 

49 self._jws = PyJWS(options=self._get_sig_options()) 

50 

51 @staticmethod 

52 def _get_default_options() -> FullOptions: 

53 return { 

54 "verify_signature": True, 

55 "verify_exp": True, 

56 "verify_nbf": True, 

57 "verify_iat": True, 

58 "verify_aud": True, 

59 "verify_iss": True, 

60 "verify_sub": True, 

61 "verify_jti": True, 

62 "require": [], 

63 "strict_aud": False, 

64 "enforce_minimum_key_length": False, 

65 } 

66 

67 def _get_sig_options(self) -> SigOptions: 

68 return { 

69 "verify_signature": self.options["verify_signature"], 

70 "enforce_minimum_key_length": self.options.get( 

71 "enforce_minimum_key_length", False 

72 ), 

73 } 

74 

75 def _merge_options(self, options: Options | None = None) -> FullOptions: 

76 if options is None: 

77 return self.options 

78 

79 merged_options = cast("Options", dict(options)) 

80 

81 # (defensive) set defaults for verify_x to False if verify_signature is False 

82 if not merged_options.get("verify_signature", True): 

83 merged_options["verify_exp"] = merged_options.get("verify_exp", False) 

84 merged_options["verify_nbf"] = merged_options.get("verify_nbf", False) 

85 merged_options["verify_iat"] = merged_options.get("verify_iat", False) 

86 merged_options["verify_aud"] = merged_options.get("verify_aud", False) 

87 merged_options["verify_iss"] = merged_options.get("verify_iss", False) 

88 merged_options["verify_sub"] = merged_options.get("verify_sub", False) 

89 merged_options["verify_jti"] = merged_options.get("verify_jti", False) 

90 return {**self.options, **merged_options} 

91 

92 def encode( 

93 self, 

94 payload: dict[str, Any], 

95 key: AllowedPrivateKeyTypes, 

96 algorithm: str | None = _ALGORITHM_UNSET, # type: ignore[assignment] 

97 headers: dict[str, Any] | None = None, 

98 json_encoder: type[json.JSONEncoder] | None = None, 

99 sort_headers: bool = True, 

100 ) -> str: 

101 """Encode the ``payload`` as JSON Web Token. 

102 

103 :param payload: JWT claims, e.g. ``dict(iss=..., aud=..., sub=...)`` 

104 :type payload: dict[str, typing.Any] 

105 :param key: a key suitable for the chosen algorithm: 

106 

107 * for **asymmetric algorithms**: PEM-formatted private key, a multiline string 

108 * for **symmetric algorithms**: plain string, sufficiently long for security 

109 

110 :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPrivateKeys` 

111 :param algorithm: algorithm to sign the token with, e.g. ``"ES256"``. 

112 If ``headers`` includes ``alg``, it will be preferred to this parameter. 

113 If ``key`` is a :class:`PyJWK` object, by default the key algorithm will be used. 

114 :type algorithm: str or None 

115 :param headers: additional JWT header fields, e.g. ``dict(kid="my-key-id")``. 

116 :type headers: dict[str, typing.Any] or None 

117 :param json_encoder: custom JSON encoder for ``payload`` and ``headers`` 

118 :type json_encoder: json.JSONEncoder or None 

119 

120 :rtype: str 

121 :returns: a JSON Web Token 

122 

123 :raises TypeError: if ``payload`` is not a ``dict`` 

124 """ 

125 # Check that we get a dict 

126 if not isinstance(payload, dict): 

127 raise TypeError( 

128 "Expecting a dict object, as JWT only supports " 

129 "JSON objects as payloads." 

130 ) 

131 

132 # Payload 

133 payload = payload.copy() 

134 for time_claim in ["exp", "iat", "nbf"]: 

135 # Convert datetime to a intDate value in known time-format claims 

136 if isinstance(payload.get(time_claim), datetime): 

137 payload[time_claim] = timegm(payload[time_claim].utctimetuple()) 

138 

139 # Issue #1039, iss being set to non-string 

140 if "iss" in payload and not isinstance(payload["iss"], str): 

141 raise TypeError("Issuer (iss) must be a string.") 

142 

143 json_payload = self._encode_payload( 

144 payload, 

145 headers=headers, 

146 json_encoder=json_encoder, 

147 ) 

148 

149 return self._jws.encode( 

150 json_payload, 

151 key, 

152 algorithm, 

153 headers, 

154 json_encoder, 

155 sort_headers=sort_headers, 

156 ) 

157 

158 def _encode_payload( 

159 self, 

160 payload: dict[str, Any], 

161 headers: dict[str, Any] | None = None, 

162 json_encoder: type[json.JSONEncoder] | None = None, 

163 ) -> bytes: 

164 """ 

165 Encode a given payload to the bytes to be signed. 

166 

167 This method is intended to be overridden by subclasses that need to 

168 encode the payload in a different way, e.g. compress the payload. 

169 """ 

170 return json.dumps( 

171 payload, 

172 separators=(",", ":"), 

173 cls=json_encoder, 

174 ).encode("utf-8") 

175 

176 def decode_complete( 

177 self, 

178 jwt: str | bytes, 

179 key: AllowedPublicKeyTypes = "", 

180 algorithms: Sequence[str] | None = None, 

181 options: Options | None = None, 

182 # deprecated arg, remove in pyjwt3 

183 verify: bool | None = None, 

184 # could be used as passthrough to api_jws, consider removal in pyjwt3 

185 detached_payload: bytes | None = None, 

186 # passthrough arguments to _validate_claims 

187 # consider putting in options 

188 audience: str | Iterable[str] | None = None, 

189 issuer: str | Container[str] | None = None, 

190 subject: str | None = None, 

191 leeway: float | timedelta = 0, 

192 # kwargs 

193 **kwargs: Any, 

194 ) -> dict[str, Any]: 

195 """Identical to ``jwt.decode`` except for return value which is a dictionary containing the token header (JOSE Header), 

196 the token payload (JWT Payload), and token signature (JWT Signature) on the keys "header", "payload", 

197 and "signature" respectively. 

198 

199 :param jwt: the token to be decoded 

200 :type jwt: str or bytes 

201 :param key: the key suitable for the allowed algorithm 

202 :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` 

203 

204 :param algorithms: allowed algorithms, e.g. ``["ES256"]`` 

205 

206 .. warning:: 

207 

208 Do **not** compute the ``algorithms`` parameter based on 

209 the ``alg`` from the token itself, or on any other data 

210 that an attacker may be able to influence, as that might 

211 expose you to various vulnerabilities (see `RFC 8725 §2.1 

212 <https://www.rfc-editor.org/rfc/rfc8725.html#section-2.1>`_). Instead, 

213 either hard-code a fixed value for ``algorithms``, or 

214 configure it in the same place you configure the 

215 ``key``. Make sure not to mix symmetric and asymmetric 

216 algorithms that interpret the ``key`` in different ways 

217 (e.g. HS\\* and RS\\*). 

218 :type algorithms: typing.Sequence[str] or None 

219 

220 :param jwt.types.Options options: extended decoding and validation options 

221 Refer to :py:class:`jwt.types.Options` for more information. 

222 

223 :param audience: optional, the value for ``verify_aud`` check 

224 :type audience: str or typing.Iterable[str] or None 

225 :param issuer: optional, the value for ``verify_iss`` check 

226 :type issuer: str or typing.Container[str] or None 

227 :param leeway: a time margin in seconds for the expiration check 

228 :type leeway: float or datetime.timedelta 

229 :rtype: dict[str, typing.Any] 

230 :returns: Decoded JWT with the JOSE Header on the key ``header``, the JWS 

231 Payload on the key ``payload``, and the JWS Signature on the key ``signature``. 

232 """ 

233 if kwargs: 

234 warnings.warn( 

235 "passing additional kwargs to decode_complete() is deprecated " 

236 "and will be removed in pyjwt version 3. " 

237 f"Unsupported kwargs: {tuple(kwargs.keys())}", 

238 RemovedInPyjwt3Warning, 

239 stacklevel=2, 

240 ) 

241 

242 if options is None: 

243 verify_signature = True 

244 else: 

245 verify_signature = options.get("verify_signature", True) 

246 

247 # If the user has set the legacy `verify` argument, and it doesn't match 

248 # what the relevant `options` entry for the argument is, inform the user 

249 # that they're likely making a mistake. 

250 if verify is not None and verify != verify_signature: 

251 warnings.warn( 

252 "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " 

253 "The equivalent is setting `verify_signature` to False in the `options` dictionary. " 

254 "This invocation has a mismatch between the kwarg and the option entry.", 

255 category=DeprecationWarning, 

256 stacklevel=2, 

257 ) 

258 

259 merged_options = self._merge_options(options) 

260 

261 sig_options: SigOptions = { 

262 "verify_signature": verify_signature, 

263 "enforce_minimum_key_length": merged_options.get( 

264 "enforce_minimum_key_length", False 

265 ), 

266 } 

267 decoded = self._jws.decode_complete( 

268 jwt, 

269 key=key, 

270 algorithms=algorithms, 

271 options=sig_options, 

272 detached_payload=detached_payload, 

273 ) 

274 

275 payload = self._decode_payload(decoded) 

276 

277 self._validate_claims( 

278 payload, 

279 merged_options, 

280 audience=audience, 

281 issuer=issuer, 

282 leeway=leeway, 

283 subject=subject, 

284 ) 

285 

286 decoded["payload"] = payload 

287 return decoded 

288 

289 def _decode_payload(self, decoded: dict[str, Any]) -> dict[str, Any]: 

290 """ 

291 Decode the payload from a JWS dictionary (payload, signature, header). 

292 

293 This method is intended to be overridden by subclasses that need to 

294 decode the payload in a different way, e.g. decompress compressed 

295 payloads. 

296 """ 

297 try: 

298 payload: dict[str, Any] = json.loads(decoded["payload"]) 

299 except ValueError as e: 

300 raise DecodeError(f"Invalid payload string: {e}") from e 

301 if not isinstance(payload, dict): 

302 raise DecodeError("Invalid payload string: must be a json object") 

303 return payload 

304 

305 def decode( 

306 self, 

307 jwt: str | bytes, 

308 key: AllowedPublicKeys | PyJWK | str | bytes = "", 

309 algorithms: Sequence[str] | None = None, 

310 options: Options | None = None, 

311 # deprecated arg, remove in pyjwt3 

312 verify: bool | None = None, 

313 # could be used as passthrough to api_jws, consider removal in pyjwt3 

314 detached_payload: bytes | None = None, 

315 # passthrough arguments to _validate_claims 

316 # consider putting in options 

317 audience: str | Iterable[str] | None = None, 

318 subject: str | None = None, 

319 issuer: str | Container[str] | None = None, 

320 leeway: float | timedelta = 0, 

321 # kwargs 

322 **kwargs: Any, 

323 ) -> dict[str, Any]: 

324 """Verify the ``jwt`` token signature and return the token claims. 

325 

326 :param jwt: the token to be decoded 

327 :type jwt: str or bytes 

328 :param key: the key suitable for the allowed algorithm 

329 :type key: str or bytes or PyJWK or :py:class:`jwt.algorithms.AllowedPublicKeys` 

330 

331 :param algorithms: allowed algorithms, e.g. ``["ES256"]`` 

332 If ``key`` is a :class:`PyJWK` object, allowed algorithms will default to the key algorithm. 

333 

334 .. warning:: 

335 

336 Do **not** compute the ``algorithms`` parameter based on 

337 the ``alg`` from the token itself, or on any other data 

338 that an attacker may be able to influence, as that might 

339 expose you to various vulnerabilities (see `RFC 8725 §2.1 

340 <https://www.rfc-editor.org/rfc/rfc8725.html#section-2.1>`_). Instead, 

341 either hard-code a fixed value for ``algorithms``, or 

342 configure it in the same place you configure the 

343 ``key``. Make sure not to mix symmetric and asymmetric 

344 algorithms that interpret the ``key`` in different ways 

345 (e.g. HS\\* and RS\\*). 

346 :type algorithms: typing.Sequence[str] or None 

347 

348 :param jwt.types.Options options: extended decoding and validation options 

349 Refer to :py:class:`jwt.types.Options` for more information. 

350 

351 :param audience: optional, the value for ``verify_aud`` check 

352 :type audience: str or typing.Iterable[str] or None 

353 :param subject: optional, the value for ``verify_sub`` check 

354 :type subject: str or None 

355 :param issuer: optional, the value for ``verify_iss`` check 

356 :type issuer: str or typing.Container[str] or None 

357 :param leeway: a time margin in seconds for the expiration check 

358 :type leeway: float or datetime.timedelta 

359 :rtype: dict[str, typing.Any] 

360 :returns: the JWT claims 

361 """ 

362 if kwargs: 

363 warnings.warn( 

364 "passing additional kwargs to decode() is deprecated " 

365 "and will be removed in pyjwt version 3. " 

366 f"Unsupported kwargs: {tuple(kwargs.keys())}", 

367 RemovedInPyjwt3Warning, 

368 stacklevel=2, 

369 ) 

370 decoded = self.decode_complete( 

371 jwt, 

372 key, 

373 algorithms, 

374 options, 

375 verify=verify, 

376 detached_payload=detached_payload, 

377 audience=audience, 

378 subject=subject, 

379 issuer=issuer, 

380 leeway=leeway, 

381 ) 

382 return cast(dict[str, Any], decoded["payload"]) 

383 

384 def _validate_claims( 

385 self, 

386 payload: dict[str, Any], 

387 options: FullOptions, 

388 audience: Iterable[str] | str | None = None, 

389 issuer: Container[str] | str | None = None, 

390 subject: str | None = None, 

391 leeway: float | timedelta = 0, 

392 ) -> None: 

393 if isinstance(leeway, timedelta): 

394 leeway = leeway.total_seconds() 

395 

396 if audience is not None and not isinstance(audience, (str, Iterable)): 

397 raise TypeError("audience must be a string, iterable or None") 

398 

399 self._validate_required_claims(payload, options["require"]) 

400 

401 now = datetime.now(tz=timezone.utc).timestamp() 

402 

403 if "iat" in payload and options["verify_iat"]: 

404 self._validate_iat(payload, now, leeway) 

405 

406 if "nbf" in payload and options["verify_nbf"]: 

407 self._validate_nbf(payload, now, leeway) 

408 

409 if "exp" in payload and options["verify_exp"]: 

410 self._validate_exp(payload, now, leeway) 

411 

412 if options["verify_iss"]: 

413 self._validate_iss(payload, issuer) 

414 

415 if options["verify_aud"]: 

416 self._validate_aud( 

417 payload, audience, strict=options.get("strict_aud", False) 

418 ) 

419 

420 if options["verify_sub"]: 

421 self._validate_sub(payload, subject) 

422 

423 if options["verify_jti"]: 

424 self._validate_jti(payload) 

425 

426 def _validate_required_claims( 

427 self, 

428 payload: dict[str, Any], 

429 claims: Iterable[str], 

430 ) -> None: 

431 for claim in claims: 

432 if payload.get(claim) is None: 

433 raise MissingRequiredClaimError(claim) 

434 

435 def _validate_sub( 

436 self, payload: dict[str, Any], subject: str | None = None 

437 ) -> None: 

438 """ 

439 Checks whether "sub" if in the payload is valid or not. 

440 This is an Optional claim 

441 

442 :param payload(dict): The payload which needs to be validated 

443 :param subject(str): The subject of the token 

444 """ 

445 

446 if "sub" not in payload: 

447 return 

448 

449 if not isinstance(payload["sub"], str): 

450 raise InvalidSubjectError("Subject must be a string") 

451 

452 if subject is not None: 

453 if payload.get("sub") != subject: 

454 raise InvalidSubjectError("Invalid subject") 

455 

456 def _validate_jti(self, payload: dict[str, Any]) -> None: 

457 """ 

458 Checks whether "jti" if in the payload is valid or not 

459 This is an Optional claim 

460 

461 :param payload(dict): The payload which needs to be validated 

462 """ 

463 

464 if "jti" not in payload: 

465 return 

466 

467 if not isinstance(payload.get("jti"), str): 

468 raise InvalidJTIError("JWT ID must be a string") 

469 

470 def _validate_iat( 

471 self, 

472 payload: dict[str, Any], 

473 now: float, 

474 leeway: float, 

475 ) -> None: 

476 try: 

477 iat = int(payload["iat"]) 

478 except ValueError: 

479 raise InvalidIssuedAtError( 

480 "Issued At claim (iat) must be an integer." 

481 ) from None 

482 if iat > (now + leeway): 

483 raise ImmatureSignatureError("The token is not yet valid (iat)") 

484 

485 def _validate_nbf( 

486 self, 

487 payload: dict[str, Any], 

488 now: float, 

489 leeway: float, 

490 ) -> None: 

491 try: 

492 nbf = int(payload["nbf"]) 

493 except ValueError: 

494 raise DecodeError("Not Before claim (nbf) must be an integer.") from None 

495 

496 if nbf > (now + leeway): 

497 raise ImmatureSignatureError("The token is not yet valid (nbf)") 

498 

499 def _validate_exp( 

500 self, 

501 payload: dict[str, Any], 

502 now: float, 

503 leeway: float, 

504 ) -> None: 

505 try: 

506 exp = int(payload["exp"]) 

507 except ValueError: 

508 raise DecodeError( 

509 "Expiration Time claim (exp) must be an integer." 

510 ) from None 

511 

512 if exp <= (now - leeway): 

513 raise ExpiredSignatureError("Signature has expired") 

514 

515 def _validate_aud( 

516 self, 

517 payload: dict[str, Any], 

518 audience: str | Iterable[str] | None, 

519 *, 

520 strict: bool = False, 

521 ) -> None: 

522 if audience is None: 

523 if "aud" not in payload or not payload["aud"]: 

524 return 

525 # Application did not specify an audience, but 

526 # the token has the 'aud' claim 

527 raise InvalidAudienceError("Invalid audience") 

528 

529 if "aud" not in payload or not payload["aud"]: 

530 # Application specified an audience, but it could not be 

531 # verified since the token does not contain a claim. 

532 raise MissingRequiredClaimError("aud") 

533 

534 audience_claims = payload["aud"] 

535 

536 # In strict mode, we forbid list matching: the supplied audience 

537 # must be a string, and it must exactly match the audience claim. 

538 if strict: 

539 # Only a single audience is allowed in strict mode. 

540 if not isinstance(audience, str): 

541 raise InvalidAudienceError("Invalid audience (strict)") 

542 

543 # Only a single audience claim is allowed in strict mode. 

544 if not isinstance(audience_claims, str): 

545 raise InvalidAudienceError("Invalid claim format in token (strict)") 

546 

547 if audience != audience_claims: 

548 raise InvalidAudienceError("Audience doesn't match (strict)") 

549 

550 return 

551 

552 if isinstance(audience_claims, str): 

553 audience_claims = [audience_claims] 

554 if not isinstance(audience_claims, list): 

555 raise InvalidAudienceError("Invalid claim format in token") 

556 if any(not isinstance(c, str) for c in audience_claims): 

557 raise InvalidAudienceError("Invalid claim format in token") 

558 

559 if isinstance(audience, str): 

560 audience = [audience] 

561 

562 if all(aud not in audience_claims for aud in audience): 

563 raise InvalidAudienceError("Audience doesn't match") 

564 

565 def _validate_iss( 

566 self, payload: dict[str, Any], issuer: Container[str] | str | None 

567 ) -> None: 

568 if issuer is None: 

569 return 

570 

571 if "iss" not in payload: 

572 raise MissingRequiredClaimError("iss") 

573 

574 iss = payload["iss"] 

575 if not isinstance(iss, str): 

576 raise InvalidIssuerError("Payload Issuer (iss) must be a string") 

577 

578 if isinstance(issuer, str): 

579 if iss != issuer: 

580 raise InvalidIssuerError("Invalid issuer") 

581 else: 

582 try: 

583 if iss not in issuer: 

584 raise InvalidIssuerError("Invalid issuer") 

585 except TypeError: 

586 raise InvalidIssuerError( 

587 'Issuer param must be "str" or "Container[str]"' 

588 ) from None 

589 

590 

591_jwt_global_obj = PyJWT() 

592_jwt_global_obj._jws = _jws_global_obj 

593encode = _jwt_global_obj.encode 

594decode_complete = _jwt_global_obj.decode_complete 

595decode = _jwt_global_obj.decode