Skip to main content

shared-lib

A production-ready JWT authentication library for issuing and verifying access and refresh tokens across microservices, built on PyJWT and managed with uv.

Features

  • Access token + refresh token issuance, individually or as a pair
  • Token verification with signature, expiry, not-before, issuer, and audience checks
  • Strict token-type separation - a refresh token can never be used where an access token is expected, and vice versa
  • Refresh token rotation (refresh_access_token(..., rotate_refresh_token=True))
  • Pluggable revocation - pass an is_revoked(jti) -> bool callback backed by whatever store you use (Redis, a database, ...) to reject tokens by ID before they expire
  • Asymmetric (RS256/ES256/PS256) and symmetric (HS256) algorithm support, configured entirely via environment variables
  • Typed dataclasses (TokenPair, TokenPayload) and a small, specific exception hierarchy instead of stringly-typed errors
  • Full unittest suite and GitHub Actions CI

Why RS256 for microservices

With a symmetric algorithm (HS256), every service that needs to verify a token must hold the exact same secret used to sign it - so the secret has to be distributed to every service, and any one of them leaking it lets an attacker forge tokens for the whole system.

With an asymmetric algorithm (RS256), only the service that issues tokens (e.g. an auth service) holds the private key. Every other service is configured with just the public key, which is enough to verify a token's signature but not to create new ones. This is the recommended default for a microservices setup and is what this library uses out of the box.

Installation

This repo is managed with uv. From the project root:

uv sync

To use jwt_auth from another project in this workspace/monorepo, add it as a path or git dependency with uv add.

Quickstart

  1. Copy the example environment file and generate a dev key pair:

    cp .env.example .env
    uv run python scripts/generate_keys.py
    

    This writes keys/private_key.pem and keys/public_key.pem (both gitignored). The default .env already points JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH at these files.

  2. Run the demo:

    uv run main.py
    
  3. Use it in code:

    from jwt_auth import JWTManager, TokenExpiredError, InvalidTokenError
    
    manager = JWTManager()  # reads configuration from the environment
    
    tokens = manager.create_token_pair(subject="user-123", extra_claims={"role": "trainer"})
    # tokens.access_token, tokens.refresh_token, tokens.expires_in
    
    try:
        payload = manager.verify_access_token(tokens.access_token)
        user_id = payload.sub
    except TokenExpiredError:
        ...  # ask the client to hit the refresh endpoint
    except InvalidTokenError:
        ...  # reject the request, log the attempt
    
  4. Refreshing an access token:

    new_tokens = manager.refresh_access_token(refresh_token, rotate_refresh_token=True)
    

Auth service vs. downstream services

Because RS256 keys are asymmetric, a downstream service that only ever needs to verify tokens should be configured with just the public key - it will raise ConfigurationError if you try to issue a token with it:

# Auth service - has both keys, can issue and verify.
issuer = JWTManager()  # JWT_PRIVATE_KEY_PATH and JWT_PUBLIC_KEY_PATH set

# Downstream service - only distribute the public key.
verifier = JWTManager(JWTSettings(algorithm="RS256", public_key=public_key_pem))
verifier.verify_access_token(incoming_token)  # OK
verifier.create_access_token("user-1")        # raises ConfigurationError

Revocation

This library doesn't ship a storage backend, since that choice (Redis, Postgres, ...) belongs to the application. Instead, JWTManager accepts an is_revoked callback:

def is_revoked(jti: str) -> bool:
    return redis_client.sismember("revoked-jtis", jti)

manager = JWTManager(is_revoked=is_revoked)

Every verify_access_token / verify_refresh_token call runs the token's jti through this callback, so revoking a token (on logout, on rotation, or by an admin) just means adding its jti to your store.

Configuration reference

All configuration is read from the environment (optionally via a .env file, loaded automatically the first time JWTSettings.from_env() runs).

Variable Required Default Notes
JWT_ALGORITHM no RS256 Any PyJWT-supported algorithm: RS256/384/512, ES256/384/512, PS256/384/512, HS256/384/512
JWT_PRIVATE_KEY_PATH / JWT_PRIVATE_KEY for asymmetric algorithms, to issue tokens - Path to a PEM file, or the raw PEM (with \n escapes)
JWT_PUBLIC_KEY_PATH / JWT_PUBLIC_KEY for asymmetric algorithms, to verify tokens - Path to a PEM file, or the raw PEM (with \n escapes)
JWT_SECRET_KEY for symmetric algorithms - Shared secret
JWT_ACCESS_TOKEN_EXPIRE_MINUTES no 15 Keep this short - access tokens are bearer credentials
JWT_REFRESH_TOKEN_EXPIRE_DAYS no 7
JWT_ISSUER no - Stamped as iss and enforced on verify when set
JWT_AUDIENCE no - Stamped as aud and enforced on verify when set

See .env.example for a template.

Testing

uv run python -m unittest discover -s tests -t . -v

Tests cover both HS256 and RS256 code paths, token-type separation, tampering/wrong-key/wrong-audience rejection, expiry, refresh rotation, revocation, and configuration validation - no network or external services required.

CI

.github/workflows/ci.yml runs on every push and pull request to main: it lints with ruff, runs the full test suite across Python 3.10-3.13 via uv, and does a final build check. Update this library, push, and CI will catch regressions before they reach any service that depends on it.

Security notes

  • Keep access token lifetimes short (minutes) and refresh token lifetimes as short as your product allows (days, not months).
  • Prefer RS256 (or another asymmetric algorithm) over HS256 whenever more than one service needs to verify tokens.
  • Use rotate_refresh_token=True and pair it with the is_revoked callback so a stolen, already-rotated refresh token can be rejected on reuse.
  • Never log full tokens. TokenPayload.jti is safe to log; the raw token string is a bearer credential.
  • Always serve token endpoints over HTTPS.
  • keys/, .env, and *.pem are gitignored - do not commit real key material or secrets.

Release files for ndaedzo-shared-lib 0.1.0

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

Source distribution (sdist)

Source distribution for ndaedzo-shared-lib 0.1.0
File Size Uploaded
ndaedzo_shared_lib-0.1.0.tar.gz 61.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ndaedzo-shared-lib 0.1.0
File Interpreter ABI Platform
ndaedzo_shared_lib-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 71.4 kB

Release files / ndaedzo_shared_lib-0.1.0.tar.gz

Download URL ndaedzo_shared_lib-0.1.0.tar.gz
Size 61.7 kB
Tags Source
SHA-256 checksum
How to use checksums
bb003363f49084efe686d1842e2d236da2017676d039cf0039927ee25802c8b9
BLAKE2b-256 checksum
How to use checksums
e9fbaed3a9432506b3b7ec0a4be1bde3e7fb38933a98f6c83eb6b1ce1bf67474
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / ndaedzo_shared_lib-0.1.0-py3-none-any.whl

Download URL ndaedzo_shared_lib-0.1.0-py3-none-any.whl
Size 9.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8990e0a75a1b3da4b2556b9c4ccde399100f8f357d56ff5c477327dba3873867
BLAKE2b-256 checksum
How to use checksums
4315789cf91dcc57dcb5f71bfac452545ac3f17bffd7abcf02127b818f43c63a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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