Metadata-Version: 2.5
Name: dt360-py-jwt-manager
Version: 1.1.4
Summary: A Python package for managing JWT tokens in the DT360 platform.
Requires-Python: >=3.12
Requires-Dist: cryptography>=42
Requires-Dist: flask<4,>=3.0
Requires-Dist: pyjwt<3,>=2.15.1
Description-Content-Type: text/markdown

# DT360 Python JWT Manager

JWT codec and Flask integration for the DT360 services. The package wraps
PyJWT's codec, derives short HMAC secrets to a 64-byte HS512 key, and can
resolve keys from a Flask app or request execution context.

## Project structure

The layers have separate responsibilities and share the same codec:

```text
src/dt360_py_jwt_manager/
  jwt.py                         PyJWT-compatible codec and module functions
  core/
    FlaskJWTManager.py           Flask config, key selection, and callbacks
    JwtManager.py                Docsmanager-specific token helpers
  helper/
    JwtKeyDerivation.py          HS512 key normalization and derivation
    URLEncodedTokenUtilities.py  Java-compatible encrypted-token decoding
```

For the full architecture, component responsibilities, key-resolution flows,
security behavior, packaging details, and end-to-end usage examples, see
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).

`PyJWT` in `jwt.py` is the common signing and verification layer. It owns
algorithm-aware key preparation, the optional legacy-key fallback, strict
minimum-key handling, and the single candidate-key verification path. The
module-level `encode()` and `decode()` functions are convenience entry points
that use one default codec instance.

`JWTManager` (also exported as `FlaskJWTManager`) is the Flask adapter. It
selects keys from a context-local override, callback/provider, or Flask app
configuration and delegates signing and verification to `PyJWT`. If key
selection needs the header and claims, it reads them together in one unverified
parse, then verifies the signature before returning claims. It does not
construct application-specific claims. `JwtManager` is the Docsmanager
adapter: it maps V1, V2, Box, and Spring key types to the Java service's token
formats, builds the expected claims, and normalizes Bearer or URL-encrypted
tokens before delegating cryptographic work to `PyJWT`.

`JwtKeyDerivation` contains the shared key-format rules. Both adapters pass
normalized source secrets to the codec; neither reimplements PBKDF2 or stores
a parallel set of padded legacy keys. `URLEncodedTokenUtilities` handles only
the Java URL-token envelope and does not participate in JWT signing.

## Replace a direct PyJWT import

Replace `import jwt as jwt_lib` with:

```python
from dt360_py_jwt_manager import jwt as jwt_lib

claims = jwt_lib.decode(
    token,
    secret,
    algorithms=["HS512"],
    options={"enforce_minimum_key_length": True},
)
```

The package's module-level `encode()` and `decode()` functions use a codec
configured with `enforce_minimum_key_length=True`. You can also create an
instance with `PyJWT(options={"enforce_minimum_key_length": True})`.

Short HMAC secrets are converted to a 64-byte key using the Java-compatible
PBKDF2-HMAC-SHA512 parameters: 220,000 iterations, the fixed salt, and the
`JWT-HS512-V1` context. This allows HS256, HS384, and HS512 operations to meet
their minimum key lengths. RSA keys are passed to PyJWT unchanged, where the
minimum RSA key size is enforced.

## Use from Flask

```python
from flask import current_app
from dt360_py_jwt_manager import JWTManager

jwt = JWTManager(app)
app.config["JWT_SECRET_KEY"] = Globals.key
app.config["JWT_SIGNING_KEYS"] = [
    Globals.key,
    Globals.user_key,
    Globals.spring_key,
]
app.config["JWT_DECODE_ALGORITHMS"] = ["HS512", "HS384", "HS256", "RS256"]

# Optional provider for request or tenant-specific key selection.
app.config["JWT_KEY_PROVIDER"] = lambda purpose, header, payload: current_app.config[
    "JWT_SECRET_KEY"
]
```

`JWTManager` registers itself in `app.extensions` and reads configuration when
encoding or decoding, so keys can be set after `JWTManager(app)`. It supports
the `decode_key_loader`, `invalid_token_loader`, `expired_token_loader`, and
`unauthorized_loader` callback registration patterns. A key loader receives
the unverified header and payload; its returned key is still signature-verified
by the wrapper.

This package replaces the PyJWT codec and supplies Flask key configuration. If
the application uses Flask-JWT-Extended's `@jwt_required` or
`get_jwt_identity()`, keep that extension for request authentication and
register the wrapper's resolver as its decode-key callback. The callback must
return the derived key that matched the signature. Returning the original
short secret would make the decorator verify with that short secret again.

If Flask-JWT-Extended remains responsible for request decorators, keep its
manager and replace only the direct PyJWT import:

```python
from flask_jwt_extended import JWTManager as FlaskJWTManager
from dt360_py_jwt_manager import jwt as jwt_lib

flask_jwt = FlaskJWTManager(app)

@flask_jwt.decode_key_loader
def load_decode_key(jwt_header, jwt_data):
    return jwt_lib.resolve_verification_key(
        extract_token_from_request(),
        app.config["JWT_SIGNING_KEYS"],
        algorithms=app.config["JWT_DECODE_ALGORITHMS"],
        allow_legacy_key=app.config.get("JWT_ALLOW_LEGACY_KEY", False),
    )

@flask_jwt.encode_key_loader
def load_encode_key(identity):
    return jwt_lib.prepare_key(
        app.config["JWT_SECRET_KEY"], app.config.get("JWT_ALGORITHM", "HS512")
    )
```

Flask-JWT-Extended's decode-key callback receives the unverified header and
claims and must return the key used by its decorator to verify the token. The
resolver checks which configured source key matches the token, returns its
derived key, and leaves expiration and claim checks to Flask-JWT-Extended.
This is the integration point that makes `@jwt_required()` use the same
derivation rules as direct calls to `jwt.encode()` and `jwt.decode()`. The
encode-key callback applies the same derivation to tokens created with
`create_access_token()`. Set `JWT_ALGORITHM="HS512"` explicitly when the
service must issue HS512 tokens.

For an explicit execution-context override, use:

```python
with jwt.key_context(tenant_secret):
    claims = jwt.decode(token, algorithms=["HS512"])
```

Keys resolve in this order: `key_context()`, a registered decode key loader,
`JWT_KEY_PROVIDER`, `JWT_SIGNING_KEYS` (decode only), and `JWT_SECRET_KEY`.
The provider receives `(purpose, header, payload)` and may return one key or an
ordered sequence of keys.

## Existing and legacy tokens

When decoding a short HMAC secret, the derived key is tried first. To continue
accepting tokens signed with the old short raw key, enable
`allow_legacy_key=True` on `PyJWT`/`JwtManager`, or set
`app.config["JWT_ALLOW_LEGACY_KEY"] = True` for the Flask manager. The legacy
key is used for verification only. PBKDF2 does not add entropy; new deployments
should use random secrets with at least 64 bytes.

The high-level `JwtManager` also provides V1, V2, and Box token helpers, along
with generic claims creation for application-specific JWTs. Session helpers
store `sessionData` as a JSON string, matching the Java format.

## Build artifacts

From the project root, build the source archive and wheel with:

```powershell
python -m build
```

The artifacts are written to `dist/`.
