Elegant JWT
JSON Web Tokens in the Elegant Objects style.
The library hides pyjwt behind small immutable objects: a Token, its
Claims, and a Signature that owns the algorithm and the key.
Table of Contents
- Installation
- Quick Start
- Tokens That Expire
- Refusing Expired Tokens
- Stamping the Issuer
- Tokens Valid Only Later
- Tokens for One Audience
- Asymmetric Algorithms
- Testing Without Waiting
- Your Own Signature
- Errors
- Design
- Development
- How to Report Issues
Installation
uv add elegant-jwt
or
pip install elegant-jwt
Quick Start
Create a token and read it back:
from elegant_jwt import Hs256, JwtClaims, JwtToken
signature = Hs256("a-secret-of-at-least-thirty-two-bytes!")
raw = JwtClaims({"sub": "42"}).token(signature).value()
print(raw) # => "eyJhbGciOiJIUzI1NiIs..."
claims = JwtToken(raw, signature).claims()
print(claims.json()) # => {"sub": "42"}
The algorithm is an object, never a hardcoded string. Pick Hs256, Rs256,
or Es256, or implement the Signature interface yourself.
Tokens That Expire
Wrap your claims in ExpiringClaims to add an exp claim. The lifetime is
in seconds:
from elegant_jwt import ExpiringClaims, Hs256, JwtClaims
signature = Hs256("a-secret-of-at-least-thirty-two-bytes!")
token = ExpiringClaims(
JwtClaims({"sub": "42"}),
3600,
).token(signature)
print(token.expired()) # => False
print(token.validity()) # => 3600 (seconds left until expiration)
Refusing Expired Tokens
StrictToken is a decorator that refuses to give claims from an expired
token. Use it wherever an expired token must be treated as an error:
from elegant_jwt import ExpiringClaims, Hs256, JwtClaims, JwtToken, StrictToken
signature = Hs256("a-secret-of-at-least-thirty-two-bytes!")
raw = ExpiringClaims(JwtClaims({"sub": "42"}), 3600).token(signature).value()
token = StrictToken(JwtToken(raw, signature))
token.claims() # raises Exception once the token has expired
A strict token needs an exp claim to judge freshness, so create it with
ExpiringClaims.
Stamping the Issuer
IssuedClaims adds iat (issued at) and iss (issuer) claims. Decorators
stack, each one adding its own claims on top:
from elegant_jwt import ExpiringClaims, Hs256, IssuedClaims, JwtClaims
token = IssuedClaims(
ExpiringClaims(
JwtClaims({"sub": "42"}),
3600,
),
"my-service",
).token(Hs256("a-secret-of-at-least-thirty-two-bytes!"))
print(token.claims().json())
# => {"sub": "42", "exp": 1788094023, "iat": 1788090423, "iss": "my-service"}
Tokens Valid Only Later
NotBeforeClaims adds an nbf (not before) claim. The delay is in seconds
from now; pyjwt refuses the token until that moment arrives:
from elegant_jwt import Hs256, JwtClaims, JwtToken, NotBeforeClaims
signature = Hs256("a-secret-of-at-least-thirty-two-bytes!")
raw = NotBeforeClaims(JwtClaims({"sub": "42"}), 300).token(signature).value()
JwtToken(raw, signature).claims() # raises Exception for the next five minutes
Tokens for One Audience
A token that carries an aud (audience) claim is read back through
AudienceSignature, a decorator that names the audience the reader expects.
It refuses tokens addressed to anyone else, and tokens with no audience at
all:
from elegant_jwt import AudienceSignature, Hs256, JwtClaims, JwtToken
signature = AudienceSignature(
Hs256("a-secret-of-at-least-thirty-two-bytes!"),
"ledger-service",
)
raw = JwtClaims({"sub": "42", "aud": "ledger-service"}).token(signature).value()
print(JwtToken(raw, signature).claims().json())
# => {"sub": "42", "aud": "ledger-service"}
The aud claim may also be a list; the token is accepted when the expected
audience is one of its entries.
Asymmetric Algorithms
Rs256 and Es256 sign with a private key and verify with a public key,
both in PEM format:
from elegant_jwt import JwtClaims, JwtToken, Rs256
signature = Rs256(private_pem, public_pem)
raw = JwtClaims({"sub": "42"}).token(signature).value()
claims = JwtToken(raw, signature).claims()
A service that only verifies tokens holds just the public key and never
calls encoded.
Testing Without Waiting
Time is an input, not a hidden call. Every object that needs the current
time accepts a Clock, so tests never sleep and never patch:
from elegant_jwt import Clock, ExpiringClaims, JwtClaims
class FrozenClock(Clock):
def __init__(self, instant: int):
self.instant = instant
def moment(self) -> int:
return self.instant
claims = ExpiringClaims(JwtClaims({"sub": "42"}), 60, FrozenClock(1000))
print(claims.json()) # => {"sub": "42", "exp": 1060}
Your Own Signature
Need a key from a JWKS endpoint, a vault, or a database? Implement the
Signature interface and keep the policy (cache, retry, timeout) on your
side; the library stays free of I/O:
from elegant_jwt import Signature
class VaultSignature(Signature):
def __init__(self, vault: Vault):
self.vault = vault
def encoded(self, payload: dict) -> str:
return Hs256(self.vault.secret()).encoded(payload)
def decoded(self, raw: str, options: dict) -> dict:
return Hs256(self.vault.secret()).decoded(raw, options)
Errors
Every failure raises a plain Exception with a human message, chained to
the original cause:
try:
JwtToken("not-a-token", signature).claims()
except Exception as trouble:
print(trouble) # => "The access token is not valid."
Design
- Every class is immutable; a change produces a new object.
- New behavior comes from decorators (
StrictToken,ExpiringClaims,IssuedClaims,NotBeforeClaims,AudienceSignature), not from modification of existing classes. - The library performs no network and no filesystem access.
Development
make unit # tests with coverage
make black # formatting
make flake8 # style
make ruff # lint
How to Report Issues
Three rules, depending on what you found.
Enhancements. Open a GitHub issue and label it enhancement. Describe the
desired behaviour and why it is useful; no code is required.
Bugs in code. Open a pull request, not an issue. The PR must contain a
test that reproduces the bug and fails against the current code. Mark the
test as disabled with pytest.mark.skip and a short reason, so CI stays green
while the failing case is on record:
@pytest.mark.skip(reason="Reproduces #99, not fixed yet")
def test_keeps_claims_of_token_without_expiration():
...
The fix can arrive in the same PR or in a follow-up one, which removes the skip. Contributors without push rights fork the repository first.
Bugs outside code. If the bug cannot be reproduced with a test
(documentation, packaging, CI configuration, and so on), open a GitHub issue
and label it bug. Describe the expected and actual behaviour and how to
observe it.
Release files for elegant-jwt 0.0.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| elegant_jwt-0.0.4.tar.gz | 75.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elegant_jwt-0.0.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.9 kB
Release files / elegant_jwt-0.0.4.tar.gz
| Download URL | elegant_jwt-0.0.4.tar.gz |
|---|---|
| Size | 75.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e73fd50a0c17e54179bda5a96455c3cb73bca4d28e1b46eef7a4442604ad72a
|
|
BLAKE2b-256 checksum How to use checksums |
689ce6da62bdd5cf4284dcb10568bf3905feb70ac58c982b045eb154b6892b13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / elegant_jwt-0.0.4-py3-none-any.whl
| Download URL | elegant_jwt-0.0.4-py3-none-any.whl |
|---|---|
| Size | 7.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b1e0a8a23dfe666ec7918c2d7aa3fb4d0a1e5148960500101587871f82c5d615
|
|
BLAKE2b-256 checksum How to use checksums |
d9b54ac660dd847d9fc6d3820801706d1c3045d0d8ad919b577e6d1234c4ad2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|