JWT

3 endpoints for the full JSON Web Token lifecycle — signing, verifying, and decoding.

Method Endpoint Purpose
POST /v1/auth/jwt-generate Generate a signed JWT
POST /v1/auth/jwt-verify Verify signature and decode claims
POST /v1/auth/jwt-decode Decode without verifying the signature

Python SDK Examples

Generate a JWT

jwt_generate supports all standard JWT signing algorithms. HMAC algorithms (HS256/HS384/HS512) use a shared secret; asymmetric algorithms (RS256/RS384/RS512, ES256/ES384/ES512) require a private key in PEM format.

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

# HMAC — shared secret, suitable for internal services
result = auth.jwt_generate(
    payload={"sub": "user_123", "role": "admin", "org": "acme"},
    secret="my-signing-key",
    algorithm="HS256",
    expires_in=3600,  # seconds
)
print(result["token"])
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

print(result["expires_at"])  # ISO 8601 expiry timestamp
print(result["header"])      # {"alg": "HS256", "typ": "JWT"}

Generate a non-expiring JWT

Omit expires_in to create a token with no expiry (useful for machine-to-machine API keys, but use with care).

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

result = auth.jwt_generate(
    payload={"sub": "service-worker", "scope": "read:metrics"},
    secret="my-signing-key",
)
print(result["token"])

Generate a JWT with an asymmetric key (RS256)

Asymmetric signing means the public key can be distributed to verifiers without exposing the signing private key.

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

# Load your RSA private key (PEM format)
with open("private_key.pem") as f:
    private_key = f.read()

result = auth.jwt_generate(
    payload={"sub": "user_456", "iat": None},
    secret=private_key,
    algorithm="RS256",
    expires_in=86400,  # 24 hours
)
print(result["token"])

Issue access and refresh tokens

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

SIGNING_KEY = "change-me-in-production"

def issue_tokens(user_id: str) -> dict:
    """Return an access token (15 min) and refresh token (7 days)."""
    access = auth.jwt_generate(
        {"sub": user_id, "type": "access"},
        secret=SIGNING_KEY,
        expires_in=900,
    )
    refresh = auth.jwt_generate(
        {"sub": user_id, "type": "refresh"},
        secret=SIGNING_KEY,
        expires_in=604800,
    )
    return {
        "access_token": access["token"],
        "refresh_token": refresh["token"],
        "expires_at": access["expires_at"],
    }

Verify a JWT

jwt_verify validates the signature and checks the exp claim by default. The response distinguishes between invalid signatures and valid-but-expired tokens so you can return the correct HTTP status code.

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

def authenticate_request(token: str, secret: str) -> dict | None:
    """Return the decoded payload or None if the token is invalid."""
    result = auth.jwt_verify(token, secret, algorithm="HS256")

    if not result["valid"]:
        if result["expired"]:
            # 401 with a token-refresh prompt
            raise ValueError("Token has expired — please refresh")
        # 401 with a generic auth failure
        raise ValueError(f"Invalid token: {result['error']}")

    return result["payload"]

Allow expired tokens during a refresh flow

Pass verify_exp=False when a refresh endpoint intentionally accepts expired access tokens to issue new ones.

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

def refresh_access_token(
    expired_access_token: str,
    refresh_token: str,
    secret: str,
) -> str:
    """Exchange a valid refresh token for a new access token."""
    # Verify the refresh token first (must not be expired)
    refresh_result = auth.jwt_verify(refresh_token, secret)
    if not refresh_result["valid"]:
        raise ValueError("Invalid or expired refresh token")

    # Decode the expired access token without expiry check
    access_result = auth.jwt_verify(
        expired_access_token,
        secret,
        verify_exp=False,
    )
    payload = access_result["payload"]

    # Issue a new access token
    new_token = auth.jwt_generate(
        {"sub": payload["sub"], "type": "access"},
        secret=secret,
        expires_in=900,
    )
    return new_token["token"]

Decode without verification

jwt_decode splits and base64-decodes the token without checking the signature — useful for reading claims from a token you received but whose issuer key you do not have locally.

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

decoded = auth.jwt_decode(token)
print(decoded)  # {"sub": "user_123", "role": "admin", "exp": 1719000000, ...}

Debug incoming tokens in a proxy or middleware

from toolkitapi import Auth

auth = Auth(api_key="tk_...")

def log_token_metadata(authorization_header: str) -> None:
    """Log non-sensitive JWT metadata for debugging (never log sensitive claims)."""
    if not authorization_header.startswith("Bearer "):
        return
    token = authorization_header.removeprefix("Bearer ")
    payload = auth.jwt_decode(token)
    safe_fields = {k: v for k, v in payload.items() if k in ("sub", "iat", "exp", "iss")}
    print("Token metadata:", safe_fields)

Response Fields

jwt-generate response:

Field Type Description
token string The signed JWT string
header object JWT header (alg, typ)
expires_at string | null ISO 8601 expiry timestamp, or null if no expiry

jwt-verify response:

Field Type Description
valid bool Whether the token signature and claims are valid
payload object Decoded claims (only present if valid)
header object JWT header (alg, typ)
expired bool true if the token signature is valid but exp has passed
error string | null Human-readable error message if valid is false

jwt-decode response:

Returns the decoded payload object directly (same as payload in the verify response, but without signature validation).

Tip

Use jwt_decode only for debugging or routing purposes — never use it to authorise access. A decoded-but-unverified token could have been forged. Always call jwt_verify before trusting any claim for access-control decisions.