Functional Weave
Code in Rust

auth.jwt@1.0.0

impl/python/verify_jwt.py

2,679 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Sequence

from .auth_jwt_decode_jwt import check_jwt_secret, split_jwt  ← decodeJwt, another function of this group · built into the same file, even by a slim install
from .auth_jwt_types import JwtVerification
from .crypto_constant_time_equal import constant_time_equal  ← from crypto.constant-time-equal ^1.0.0 · built alongside by fune
from .crypto_hmac_sha256 import hmac_sha256  ← from crypto.hmac-sha256 ^1.0.0 · built alongside by fune
from .encoding_utf8_utf8_encode import utf8_encode


def _fail(error: str, message: str) -> JwtVerification:
    return JwtVerification(valid=False, claims=None, error=error, message=message)


def verify_jwt(token: str, secret: Sequence[int], now: int, leeway_seconds: int) -> JwtVerification:
    """Check an HS256 token: shape, algorithm (so "none" and algorithm-confusion
    tokens are refused before any key is used), signature in constant time,
    then exp, nbf and iat against now with leeway. A bad token is an answer,
    never an exception; only a bad secret, now or leeway raises."""
    check_jwt_secret(secret)
    if isinstance(now, bool) or not isinstance(now, int):
        raise TypeError("now must be a whole number of Unix seconds")
    if isinstance(leeway_seconds, bool) or not isinstance(leeway_seconds, int) or leeway_seconds < 0:
        raise ValueError("leewaySeconds must be a whole number, 0 or more")

    parts = split_jwt(token)
    if parts is None:
        return _fail("malformed_token", "the token is not a well-formed JWT")
    header, claims, signing_input, signature = parts
    if header.get("alg") != "HS256":
        return _fail("unsupported_algorithm", "only HS256 tokens are accepted")
    if "crit" in header:
        return _fail("unsupported_algorithm", "the token requires header extensions (crit) this verifier does not support")

    expected = hmac_sha256(secret, utf8_encode(signing_input))
    if not constant_time_equal(expected, signature):
        return _fail("invalid_signature", "the token's signature does not match")

    for name in ("exp", "nbf", "iat"):
        if name in claims and (isinstance(claims[name], bool) or not isinstance(claims[name], (int, float))):
            return _fail("invalid_claims", "the token's %s claim is not a number" % name)
    # RFC 7519 4.1.4: the current time MUST be before exp.
    if "exp" in claims and now >= claims["exp"] + leeway_seconds:
        return _fail("token_expired", "the token has expired")
    # RFC 7519 4.1.5: the current time MUST be at or after nbf.
    if "nbf" in claims and now + leeway_seconds < claims["nbf"]:
        return _fail("token_not_yet_valid", "the token is not valid yet")
    if "iat" in claims and claims["iat"] > now + leeway_seconds:
        return _fail("token_issued_in_future", "the token was issued in the future")
    return JwtVerification(valid=True, claims=claims, error=None, message=None)