Metadata-Version: 2.5
Name: ndaedzo-shared-lib
Version: 0.1.0
Summary: Production-ready JWT access/refresh token issuance and verification library for microservices.
Requires-Python: >=3.10
Requires-Dist: pyjwt[crypto]>=2.10.1
Requires-Dist: python-dotenv>=1.0.1
Description-Content-Type: text/markdown

# shared-lib

A production-ready JWT authentication library for issuing and verifying access and refresh tokens across microservices, built on [PyJWT](https://pyjwt.readthedocs.io/) and managed with [uv](https://docs.astral.sh/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:

```bash
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:

   ```bash
   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:

   ```bash
   uv run main.py
   ```

3. Use it in code:

   ```python
   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:

   ```python
   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:

```python
# 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:

```python
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

```bash
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.
