4,227 bytes · the Python implementation · view raw
import json
import math
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
from .encoding_base64_base64_url_decode import base64_url_decode
#: Containers nested deeper than this are refused, in every language, rather#: than left to each parser's own limit.
MAX_DEPTH = 32
MAX_SAFE = 9007199254740991def _sextet(ch: str) -> int:
if"A" <= ch <= "Z":
return ord(ch) - 65if"a" <= ch <= "z":
return ord(ch) - 71if"0" <= ch <= "9":
return ord(ch) + 4if ch == "-":
return62if ch == "_":
return63return -1def _is_segment(text: str, allow_empty: bool) -> bool:
"""One canonical, unpadded base64url segment? Checked before decoding so a hostile token is an answer (None), never an exception."""if len(text) == 0:
return allow_empty
if len(text) % 4 == 1:
returnFalse
last = 0for ch in text:
last = _sextet(ch)
if last < 0:
returnFalseif len(text) % 4 == 2:
return last & 15 == 0if len(text) % 4 == 3:
return last & 3 == 0returnTruedef _reject_constant(name: str) -> Any:
# json.loads accepts NaN and Infinity, which are not JSON.raise ValueError("not JSON: %s" % name)
def _has_surrogate(text: str) -> bool:
return any(0xD800 <= ord(ch) <= 0xDFFFfor ch in text)
def _is_portable(value: Any, depth: int) -> bool:
"""Refuse what the three languages' parsers would answer differently: integers beyond 2^53, non-finite numbers, lone surrogates, deep nesting."""if isinstance(value, bool) or value isNone:
returnTrueif isinstance(value, int):
return abs(value) <= MAX_SAFE
if isinstance(value, float):
return math.isfinite(value) and (not value.is_integer() or abs(value) <= MAX_SAFE)
if isinstance(value, str):
returnnot _has_surrogate(value)
if depth > MAX_DEPTH:
returnFalseif isinstance(value, list):
return all(_is_portable(item, depth + 1) for item in value)
return all(not _has_surrogate(k) and _is_portable(v, depth + 1) for k, v in value.items())
def _parse_object(segment: str) -> Optional[Dict[str, Any]]:
try:
text = bytes(base64_url_decode(segment)).decode("utf-8")
parsed = json.loads(text, parse_constant=_reject_constant)
except (ValueError, RecursionError):
returnNoneifnot isinstance(parsed, dict):
returnNonereturn parsed if _is_portable(parsed, 1) elseNonedef split_jwt(token: str) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], str, List[int]]]:
"""(header, claims, signing input, signature bytes) of a compact JWT, or None when it is not three canonical base64url segments whose first two are JSON objects. Nothing is verified here; verify_jwt builds on it."""ifnot isinstance(token, str):
returnNone
parts = token.split(".")
if len(parts) != 3:
returnNoneifnot _is_segment(parts[0], False) ornot _is_segment(parts[1], False) ornot _is_segment(parts[2], True):
returnNone
header = _parse_object(parts[0])
claims = _parse_object(parts[1])
if header isNoneor claims isNone:
returnNonereturn header, claims, parts[0] + "." + parts[1], base64_url_decode(parts[2])
def check_jwt_secret(secret: Sequence[int]) -> None:
"""RFC 7518 section 3.2: an HS256 key MUST be at least 256 bits."""if isinstance(secret, (str, dict)) ornot hasattr(secret, "__len__"):
raise TypeError("secret must be a list of integers from 0 to 255")
for b in secret:
if type(b) isnot int or b < 0or b > 255:
raise ValueError("secret must be a list of integers from 0 to 255")
if len(secret) < 32:
raise ValueError("secret must be at least 32 bytes (RFC 7518 section 3.2), received %d" % len(secret))
def decode_jwt(token: str) -> Optional[Mapping[str, Any]]:
"""The claims of a JWT WITHOUT checking its signature, or None when it is malformed. For reading exp or name from a token you hold; never for deciding whether to trust one."""
parts = split_jwt(token)
returnNoneif parts isNoneelse parts[1]