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.