Skip to main content

Elegant JWT

EO principles respected here Downloads

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

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 With a Unique ID

JtiClaims adds a jti (JWT ID) claim, the key a service uses for a revocation list or a one-time-use record. By default the ID is a random version 4 UUID:

from elegant_jwt import Hs256, JtiClaims, JwtClaims

token = JtiClaims(JwtClaims({"sub": "42"})).token(
    Hs256("a-secret-of-at-least-thirty-two-bytes!")
)

print(token.claims().json())
# => {"sub": "42", "jti": "b3f2c1d4-8a6e-4f0b-9c1d-2e3f4a5b6c7d"}

Each JtiClaims object carries one ID; build a new object for every token that needs its own. To use an ID of your own, pass it as the second argument:

claims = JtiClaims(JwtClaims({"sub": "42"}), "order-7781-refund")

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, JtiClaims, 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.

Metadata

Release files for elegant-jwt 0.0.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for elegant-jwt 0.0.6
File Size Uploaded Provenance Actions
elegant_jwt-0.0.6.tar.gz 77.0 kB Without provenance Details for elegant_jwt-0.0.6.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for elegant-jwt 0.0.6
File Interpreter ABI Platform Actions
elegant_jwt-0.0.6-py3-none-any.whl Python 3 none any Details for elegant_jwt-0.0.6-py3-none-any.whl

Total release size: 84.8 kB

Release files / elegant_jwt-0.0.6.tar.gz

Download URL elegant_jwt-0.0.6.tar.gz
Size 77.0 kB
Tags Source
SHA-256 checksum
How to use checksums
b60211d8584ccab080294eb4e0f1459f6ae5e58d5c6b52c48c02bbfc6a4d09f8
BLAKE2b-256 checksum
How to use checksums
5d5e71b7e01a769d4c3c2520c1f544005faa159b3e7453512b78195d7f76ab6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","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.6-py3-none-any.whl

Download URL elegant_jwt-0.0.6-py3-none-any.whl
Size 7.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8936f94f8d1efcb6059a489d6c6253ad218e0f8b95ce02723f68d58daf90efd7
BLAKE2b-256 checksum
How to use checksums
2517d309db8222303c07e9c0c8be21f929f8e90f2f51645987202759246a6c49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","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}

Security for elegant-jwt 0.0.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page