JWT Access Token - jbrucker/home-log GitHub Wiki

The backend service uses a JWT access token for protected endpoints.

The payload of the JWT contains (as of this writing) these fields:

  • user_id id of the authenticated user (Note: this field is usually named sub)
  • exp an integer timestamp containing a timezone-aware datetime value when token expires.

The expiration key must be exp or else the jose JWT library will not check for expiration.

The code in app/utils/auth.py uses 3 settings (app.core.config.settings). Settings reads the values from env vars.

Settings Env Var Explanation
jwt_algorithm JWT_ALGORITHM Hashing algorithm to use. "HS256", "ES256", or "RS256".
secret_key SECRET_KEY 256-bit secret key. For HS256 use a 32-byte random number.
access_token_expire_minutes same in uppercase Access token lifetime, in minutes.

Compute Timestamp for Expiration Date/time

Let: expire_minutes be the lifetime of the token, in minutes.

from datetime import datetime, timezone

# Set expiration time. Use UTC time
expire = datetime.now(timezone.utc) + timedelta(minutes=expire_minutes)
# Must be int value, in seconds. Use int(timestamp) to remove fractional seconds.
expire_timestamp = int(expire.timestamp())

# Payload with expiration
payload = {
    "sub": "user_id, username, or email",   # we don't record this.
    "user_id": user_id,
    "exp": expire_timestamp,
    # ... other claims ...
}
token = jwt.encode(payload, SECRET_KEY, algorithm=JWT_ALGORITHM)

To decode:

try:
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
except JWTClaimsError:
    # one of the claims in the token is invalid (see docs for available claims)
except ExpiredSignatureError:
    # the `exp` value has already expired
except JWTError:
    # the signature is invalid

To get a readable datetime from the payload["exp"] value:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

expiry =payload["exp"]
assert isinstance(expiry, int)
# timestamp was recorded in UTC time
expiry_datetime = datetime.fromtimestamp(expiry, tz=timezone.utc)
print("Expires:   ", expiry_datetime, "UTC")
# Get the local date/time
local_tz = datetime.now().aszoneinfo().tzinfo 
local_time = expiry_datetime.astimezone(tz=local_tz)
print("Local time:", local_time)

JWT Claims

There are several other standard "claims" that can be put in the payload: Here is a concise table of common JWT registered claims supported by the python-jose (jose.jwt) module and their meanings:

Claim Meaning
aud Audience — identifies the recipients the token is intended for
exp Expiration Time — token expiry as UNIX timestamp, must be int
iss Issuer — identifies who issued the token
sub Subject — identifies the subject of the token, e.g., user ID
nbf Not Before — token is not valid before this time
iat Issued At — time the token was issued
jti JWT ID — unique identifier for the token

jose.jwt.decode() automatically validates exp, nbf, and iat if present, and will raise exceptions ExpiredSignatureError or JWTClaimsError if any are invalid.

You can also include other standard claims or custom claims. Custom claims are application-specific and not interpreted by jose.

Verify Claims

jose.jwt.decode() automatically validates exp, nbf, and iat if present, and will raise exceptions ExpiredSignatureError or JWTClaimsError.

To verify other claims, use:

from jose import jwt

# Example inputs
token = "your.jwt.token"
key = "your-secret-key"
algorithms = ["HS256"]

# Decode with verification of specific claims
payload = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=[JWT_ALGORITHM], # can list multiple algorithms to try
    audience="your-audience",   # Verifies the 'aud' claim
    issuer="your-issuer",       # Verifies the 'iss' claim
    options={                   # configure claim verification
        "verify_exp": True,
        "verify_nbf": True,
        "verify_iat": False,
        "verify_aud": True,
        "verify_iss": True,
    }
)

How to Get a Random Number for HS256 Secret Key

The HS256 hash requires a 32-byte random number as secret key. Use any of these

  • Command line: openssl rand --hex 32
  • Python os.urandom(32)
  • Python secrets.token_hex(32) from Python secrets module

Important: In your unit tests, create a new SECRET_KEY in code; don't use the value from your .env file. Test output might expose the "real" secret key.

What to return if token expired?

The app should return HTTP 401 Unauthorized with this header field:

WWW-Authenticate: Bearer error="invalid_token", error_description="Token has expired"

according to RFC 7235.

Reference

https://jwt.io has a concise introduction to JWT and interactive JWT decoder.

Python 'jose' project on pypi provides JWE (Web Encryption), JWK (Web Key), JWS (Web Signatures), and JWT.

Python 'jose' on ReadTheDocs is outdated (2015) and incomplete.