import json from typing import Any, Mapping, Sequence from .auth_jwt_decode_jwt import check_jwt_secret from .crypto_hmac_sha256 import hmac_sha256 from .encoding_base64_base64_url_encode import base64_url_encode from .encoding_utf8_utf8_encode import utf8_encode #: base64url of {"alg":"HS256","typ":"JWT"}: the header is fixed. HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" MAX_DEPTH = 32 MAX_SAFE = 9007199254740991 def _canonical(value: Any, depth: int) -> Any: """The value with every number made a whole int, checked the way the other languages check it; json.dumps then writes it with sorted keys.""" if value is None or isinstance(value, (bool, str)): return value if isinstance(value, (int, float)): if isinstance(value, float) and not value.is_integer(): raise ValueError("claims may only hold whole numbers from -(2^53 - 1) to 2^53 - 1") if abs(value) > MAX_SAFE: raise ValueError("claims may only hold whole numbers from -(2^53 - 1) to 2^53 - 1") return int(value) if isinstance(value, (list, tuple)): if depth > MAX_DEPTH: raise ValueError("claims are nested more than 32 deep") return [_canonical(item, depth + 1) for item in value] if isinstance(value, Mapping): if depth > MAX_DEPTH: raise ValueError("claims are nested more than 32 deep") out = {} for key, item in value.items(): if not isinstance(key, str): raise TypeError("claims must hold only JSON values") out[key] = _canonical(item, depth + 1) return out raise TypeError("claims must hold only JSON values") def sign_jwt(claims: Mapping[str, Any], secret: Sequence[int]) -> str: """An HS256 JWT for the claims: header {"alg":"HS256","typ":"JWT"}, the claims as canonical JSON (no spaces, keys sorted by code point, UTF-8 rather than \\u escapes), and the HMAC-SHA256 of the two.""" check_jwt_secret(secret) if not isinstance(claims, Mapping): raise TypeError("claims must be a JSON object") payload = json.dumps(_canonical(claims, 1), sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False) signing_input = HEADER + "." + base64_url_encode(utf8_encode(payload)) return signing_input + "." + base64_url_encode(hmac_sha256(secret, utf8_encode(signing_input)))