A signed JWT carries claims whose integrity can be checked without looking up the entire token in a session table. That convenience does not make its contents confidential, establish authorization, or prevent replay. Start with the token format and the application’s validation contract, then test each boundary independently. A successful cryptographic check is one input to an access decision, not the decision itself.

Readable JWS segments enter an application validation boundary: configured issuer, key source, algorithm and token type; signature or MAC verification; then issuer, audience, subject, claim types and time policy. An application access decision remains separate. The fixture is one HS256 profile with integer times, not production proof.
A signature protects integrity, not secrecy. This compact JWS model separates cryptographic checks, application policy, and the final access decision. Its time window excludes exact expiry; a valid signature alone does not make a token usable.

Signed tokens are one JWT form

This article concerns compact, signed or MAC-protected JWTs represented as JWS: three dot-separated segments containing a protected header, payload, and signature or MAC. Base64url is reversible encoding. JWT also supports encrypted JWE representations, so “every JWT is readable and has three segments” is incorrect. Even with encryption, minimize personal information and avoid embedding unrelated credentials.

HS256 uses a shared secret for HMAC. Every verifier holding that secret can also mint tokens, so it cannot prove which particular holder issued one. Asymmetric signatures separate signing and verification capabilities. Neither design permits accepting an arbitrary key supplied by an untrusted token. Configure trusted issuers, key sources, algorithms, and acceptable token types together.

Validation is an application contract

Parse enough untrusted data to locate a configured key, but do not act on claims until cryptographic verification and all policy checks succeed. Treat kid as an identifier within a trusted key set, never an unrestricted filename or fetching instruction. Bind each key to its intended algorithm. A token declaring HS256 must not cause an RSA public key to become an HMAC secret.

Check the intended issuer and audience as well as the subject and permissions needed for the operation. A genuine token for another API is still wrong here. Different token kinds may need mutually exclusive validation rules to prevent substitution. Use a maintained JWT library with explicit validation options in production; the small model below intentionally supports only one fixture profile.

Registered claims are not universally mandatory in the base JWT specification. An application profile decides what it requires. For a short-lived access token, require and enforce exp; nbf, when present, limits early use. The current time must be earlier than exp, with any explicitly configured, bounded clock allowance. iat records issuance time; it does not automatically expire a token or replace nbf. Requiring integer times is this exercise’s narrower policy, not the full NumericDate definition.

When it breaks

An expired token can have a perfectly valid signature. Conversely, a recent timestamp proves nothing if the signature is invalid. Test those failures separately, including the exact expiry instant. A blanket “token invalid” response may be suitable for callers, while internal diagnostics should distinguish causes without recording the token itself.

Expiration limits replay duration but is not the only possible revocation mechanism. A service can consult session state, a token identifier denylist, an account policy, or an introspection endpoint. Those choices introduce consistency and availability requirements. Rotating a signing key can affect many tokens and is not a convenient substitute for individual logout. A jti value alone neither stores server-side data nor implements revocation.

Do not use Python assert statements for authentication enforcement: optimization can remove them. Also reject invalid claim types rather than coercing a string or boolean into a timestamp. A readable demonstration secret must never become a deployed signing key. HS256 needs a sufficiently strong secret; this fixture generates thirty-two random bytes per run and discards them afterward.

Check it quickly

Save as jwt_check.py and run both python3 jwt_check.py and python3 -O jwt_check.py. The fixed synthetic time makes boundary tests deterministic without altering the system clock. Everything stays in memory; no issued token or key is printed, persisted, or sent anywhere.

import base64
import hashlib
import hmac
import json
import re
import secrets

# Ephemeral fixture key, never a production credential.
KEY = secrets.token_bytes(32)
NOW = 1_800_000_000


def encode(raw):
    return base64.urlsafe_b64encode(raw).rstrip(b'=').decode('ascii')


def decode(value):
    if not re.fullmatch(r'[A-Za-z0-9_-]+', value):
        raise ValueError('invalid base64url')
    return base64.b64decode(value + '=' * (-len(value) % 4), altchars=b'-_', validate=True)


def issue(claims, algorithm='HS256'):
    head = encode(json.dumps({'alg': algorithm, 'typ': 'JWT'}).encode())
    body = encode(json.dumps(claims).encode())
    signed = f'{head}.{body}'
    signature = encode(hmac.new(KEY, signed.encode(), hashlib.sha256).digest())
    return f'{signed}.{signature}'


def verify(token, now):
    head, body, signature = token.split('.')
    header = json.loads(decode(head))
    if not isinstance(header, dict) or header != {'alg': 'HS256', 'typ': 'JWT'}:
        raise ValueError('unsupported header')
    expected = hmac.new(KEY, f'{head}.{body}'.encode(), hashlib.sha256).digest()
    if not hmac.compare_digest(expected, decode(signature)):
        raise ValueError('bad signature')
    claims = json.loads(decode(body))
    if not isinstance(claims, dict):
        raise ValueError('claims must be an object')
    if claims.get('iss') != 'fixture-issuer' or claims.get('aud') != 'fixture-api':
        raise ValueError('issuer or audience mismatch')
    if any(type(claims.get(k)) is not int for k in ('iat', 'nbf', 'exp')):
        raise ValueError('integer times required by fixture')
    if not claims['iat'] <= now or not claims['nbf'] <= now < claims['exp']:
        raise ValueError('outside time policy')
    if not isinstance(claims.get('sub'), str) or not claims['sub']:
        raise ValueError('subject required')
    return claims


def rejected(label, token, now=NOW):
    try:
        verify(token, now)
    except ValueError:
        print('rejected:', label)
        return
    raise RuntimeError('unexpected acceptance: ' + label)


claims = {'iss': 'fixture-issuer', 'aud': 'fixture-api', 'sub': 'demo',
          'iat': NOW, 'nbf': NOW, 'exp': NOW + 60}
token = issue(claims)
if verify(token, NOW)['sub'] != 'demo':
    raise RuntimeError('valid token failed')
rejected('exact expiry', token, NOW + 60)
rejected('not yet valid', issue({**claims, 'nbf': NOW + 1}))
rejected('future issued-at', issue({**claims, 'iat': NOW + 1}))
rejected('wrong audience', issue({**claims, 'aud': 'other-api'}))
rejected('wrong issuer', issue({**claims, 'iss': 'other-issuer'}))
rejected('missing expiry', issue({k: v for k, v in claims.items() if k != 'exp'}))
rejected('boolean time', issue({**claims, 'exp': True}))
rejected('algorithm mismatch', issue(claims, 'none'))
head, body, signature = token.split('.')
forged_body = encode(json.dumps({**claims, 'sub': 'admin'}).encode())
rejected('tampered subject', f'{head}.{forged_body}.{signature}')
print('PASS: valid accepted; nine negative cases rejected')

Both invocations must print nine rejection lines and PASS. The successful token establishes the positive path. Negative cases cover the exact expiry, early use, future issuance, issuer, audience, absent expiry, a boolean time, algorithm mismatch, and a changed subject with the original signature. Enforcement and test failures use explicit exceptions, so optimized execution retains them.

This is an executable explanation, not a complete JWT implementation. It omits audience arrays, duplicate-member handling, key discovery and rotation, nested tokens, clock leeway, size limits, revocation, and authorization rules. Those are deliberate boundaries to review in the production library and application; passing these cases does not certify an authentication system.

Further reading

Sources