dt360-py-jwt-manager 1.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dt360-py-jwt-manager might be problematic. Click here for more details.
- dt360_py_jwt_manager-1.1.3/PKG-INFO +185 -0
- dt360_py_jwt_manager-1.1.3/README.md +175 -0
- dt360_py_jwt_manager-1.1.3/docs/ARCHITECTURE.md +425 -0
- dt360_py_jwt_manager-1.1.3/pyproject.toml +21 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/__init__.py +9 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/core/FlaskJWTManager.py +206 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/core/JwtManager.py +296 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/core/__init__.py +6 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/helper/JwtKeyDerivation.py +85 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/helper/URLEncodedTokenUtilities.py +82 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/helper/__init__.py +6 -0
- dt360_py_jwt_manager-1.1.3/src/dt360_py_jwt_manager/jwt.py +343 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dt360-py-jwt-manager
|
|
3
|
+
Version: 1.1.3
|
|
4
|
+
Summary: A Python package for managing JWT tokens in the DT360 platform.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: cryptography>=42
|
|
7
|
+
Requires-Dist: flask<4,>=3.0
|
|
8
|
+
Requires-Dist: pyjwt<3,>=2.15.1
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# DT360 Python JWT Manager
|
|
12
|
+
|
|
13
|
+
JWT codec and Flask integration for the DT360 services. The package wraps
|
|
14
|
+
PyJWT's codec, derives short HMAC secrets to a 64-byte HS512 key, and can
|
|
15
|
+
resolve keys from a Flask app or request execution context.
|
|
16
|
+
|
|
17
|
+
## Project structure
|
|
18
|
+
|
|
19
|
+
The layers have separate responsibilities and share the same codec:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
src/dt360_py_jwt_manager/
|
|
23
|
+
jwt.py PyJWT-compatible codec and module functions
|
|
24
|
+
core/
|
|
25
|
+
FlaskJWTManager.py Flask config, key selection, and callbacks
|
|
26
|
+
JwtManager.py Docsmanager-specific token helpers
|
|
27
|
+
helper/
|
|
28
|
+
JwtKeyDerivation.py HS512 key normalization and derivation
|
|
29
|
+
URLEncodedTokenUtilities.py Java-compatible encrypted-token decoding
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For the full architecture, component responsibilities, key-resolution flows,
|
|
33
|
+
security behavior, packaging details, and end-to-end usage examples, see
|
|
34
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
35
|
+
|
|
36
|
+
`PyJWT` in `jwt.py` is the common signing and verification layer. It owns
|
|
37
|
+
algorithm-aware key preparation, the optional legacy-key fallback, strict
|
|
38
|
+
minimum-key handling, and the single candidate-key verification path. The
|
|
39
|
+
module-level `encode()` and `decode()` functions are convenience entry points
|
|
40
|
+
that use one default codec instance.
|
|
41
|
+
|
|
42
|
+
`JWTManager` (also exported as `FlaskJWTManager`) is the Flask adapter. It
|
|
43
|
+
selects keys from a context-local override, callback/provider, or Flask app
|
|
44
|
+
configuration and delegates signing and verification to `PyJWT`. If key
|
|
45
|
+
selection needs the header and claims, it reads them together in one unverified
|
|
46
|
+
parse, then verifies the signature before returning claims. It does not
|
|
47
|
+
construct application-specific claims. `JwtManager` is the Docsmanager
|
|
48
|
+
adapter: it maps V1, V2, Box, and Spring key types to the Java service's token
|
|
49
|
+
formats, builds the expected claims, and normalizes Bearer or URL-encrypted
|
|
50
|
+
tokens before delegating cryptographic work to `PyJWT`.
|
|
51
|
+
|
|
52
|
+
`JwtKeyDerivation` contains the shared key-format rules. Both adapters pass
|
|
53
|
+
normalized source secrets to the codec; neither reimplements PBKDF2 or stores
|
|
54
|
+
a parallel set of padded legacy keys. `URLEncodedTokenUtilities` handles only
|
|
55
|
+
the Java URL-token envelope and does not participate in JWT signing.
|
|
56
|
+
|
|
57
|
+
## Replace a direct PyJWT import
|
|
58
|
+
|
|
59
|
+
Replace `import jwt as jwt_lib` with:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dt360_py_jwt_manager import jwt as jwt_lib
|
|
63
|
+
|
|
64
|
+
claims = jwt_lib.decode(
|
|
65
|
+
token,
|
|
66
|
+
secret,
|
|
67
|
+
algorithms=["HS512"],
|
|
68
|
+
options={"enforce_minimum_key_length": True},
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The package's module-level `encode()` and `decode()` functions use a codec
|
|
73
|
+
configured with `enforce_minimum_key_length=True`. You can also create an
|
|
74
|
+
instance with `PyJWT(options={"enforce_minimum_key_length": True})`.
|
|
75
|
+
|
|
76
|
+
Short HMAC secrets are converted to a 64-byte key using the Java-compatible
|
|
77
|
+
PBKDF2-HMAC-SHA512 parameters: 220,000 iterations, the fixed salt, and the
|
|
78
|
+
`JWT-HS512-V1` context. This allows HS256, HS384, and HS512 operations to meet
|
|
79
|
+
their minimum key lengths. RSA keys are passed to PyJWT unchanged, where the
|
|
80
|
+
minimum RSA key size is enforced.
|
|
81
|
+
|
|
82
|
+
## Use from Flask
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from flask import current_app
|
|
86
|
+
from dt360_py_jwt_manager import JWTManager
|
|
87
|
+
|
|
88
|
+
jwt = JWTManager(app)
|
|
89
|
+
app.config["JWT_SECRET_KEY"] = Globals.key
|
|
90
|
+
app.config["JWT_SIGNING_KEYS"] = [
|
|
91
|
+
Globals.key,
|
|
92
|
+
Globals.user_key,
|
|
93
|
+
Globals.spring_key,
|
|
94
|
+
]
|
|
95
|
+
app.config["JWT_DECODE_ALGORITHMS"] = ["HS512", "HS384", "HS256", "RS256"]
|
|
96
|
+
|
|
97
|
+
# Optional provider for request or tenant-specific key selection.
|
|
98
|
+
app.config["JWT_KEY_PROVIDER"] = lambda purpose, header, payload: current_app.config[
|
|
99
|
+
"JWT_SECRET_KEY"
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`JWTManager` registers itself in `app.extensions` and reads configuration when
|
|
104
|
+
encoding or decoding, so keys can be set after `JWTManager(app)`. It supports
|
|
105
|
+
the `decode_key_loader`, `invalid_token_loader`, `expired_token_loader`, and
|
|
106
|
+
`unauthorized_loader` callback registration patterns. A key loader receives
|
|
107
|
+
the unverified header and payload; its returned key is still signature-verified
|
|
108
|
+
by the wrapper.
|
|
109
|
+
|
|
110
|
+
This package replaces the PyJWT codec and supplies Flask key configuration. If
|
|
111
|
+
the application uses Flask-JWT-Extended's `@jwt_required` or
|
|
112
|
+
`get_jwt_identity()`, keep that extension for request authentication and
|
|
113
|
+
register the wrapper's resolver as its decode-key callback. The callback must
|
|
114
|
+
return the derived key that matched the signature. Returning the original
|
|
115
|
+
short secret would make the decorator verify with that short secret again.
|
|
116
|
+
|
|
117
|
+
If Flask-JWT-Extended remains responsible for request decorators, keep its
|
|
118
|
+
manager and replace only the direct PyJWT import:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from flask_jwt_extended import JWTManager as FlaskJWTManager
|
|
122
|
+
from dt360_py_jwt_manager import jwt as jwt_lib
|
|
123
|
+
|
|
124
|
+
flask_jwt = FlaskJWTManager(app)
|
|
125
|
+
|
|
126
|
+
@flask_jwt.decode_key_loader
|
|
127
|
+
def load_decode_key(jwt_header, jwt_data):
|
|
128
|
+
return jwt_lib.resolve_verification_key(
|
|
129
|
+
extract_token_from_request(),
|
|
130
|
+
app.config["JWT_SIGNING_KEYS"],
|
|
131
|
+
algorithms=app.config["JWT_DECODE_ALGORITHMS"],
|
|
132
|
+
allow_legacy_key=app.config.get("JWT_ALLOW_LEGACY_KEY", False),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@flask_jwt.encode_key_loader
|
|
136
|
+
def load_encode_key(identity):
|
|
137
|
+
return jwt_lib.prepare_key(
|
|
138
|
+
app.config["JWT_SECRET_KEY"], app.config.get("JWT_ALGORITHM", "HS512")
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Flask-JWT-Extended's decode-key callback receives the unverified header and
|
|
143
|
+
claims and must return the key used by its decorator to verify the token. The
|
|
144
|
+
resolver checks which configured source key matches the token, returns its
|
|
145
|
+
derived key, and leaves expiration and claim checks to Flask-JWT-Extended.
|
|
146
|
+
This is the integration point that makes `@jwt_required()` use the same
|
|
147
|
+
derivation rules as direct calls to `jwt.encode()` and `jwt.decode()`. The
|
|
148
|
+
encode-key callback applies the same derivation to tokens created with
|
|
149
|
+
`create_access_token()`. Set `JWT_ALGORITHM="HS512"` explicitly when the
|
|
150
|
+
service must issue HS512 tokens.
|
|
151
|
+
|
|
152
|
+
For an explicit execution-context override, use:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
with jwt.key_context(tenant_secret):
|
|
156
|
+
claims = jwt.decode(token, algorithms=["HS512"])
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Keys resolve in this order: `key_context()`, a registered decode key loader,
|
|
160
|
+
`JWT_KEY_PROVIDER`, `JWT_SIGNING_KEYS` (decode only), and `JWT_SECRET_KEY`.
|
|
161
|
+
The provider receives `(purpose, header, payload)` and may return one key or an
|
|
162
|
+
ordered sequence of keys.
|
|
163
|
+
|
|
164
|
+
## Existing and legacy tokens
|
|
165
|
+
|
|
166
|
+
When decoding a short HMAC secret, the derived key is tried first. To continue
|
|
167
|
+
accepting tokens signed with the old short raw key, enable
|
|
168
|
+
`allow_legacy_key=True` on `PyJWT`/`JwtManager`, or set
|
|
169
|
+
`app.config["JWT_ALLOW_LEGACY_KEY"] = True` for the Flask manager. The legacy
|
|
170
|
+
key is used for verification only. PBKDF2 does not add entropy; new deployments
|
|
171
|
+
should use random secrets with at least 64 bytes.
|
|
172
|
+
|
|
173
|
+
The high-level `JwtManager` also provides V1, V2, and Box token helpers, along
|
|
174
|
+
with generic claims creation for application-specific JWTs. Session helpers
|
|
175
|
+
store `sessionData` as a JSON string, matching the Java format.
|
|
176
|
+
|
|
177
|
+
## Build artifacts
|
|
178
|
+
|
|
179
|
+
From the project root, build the source archive and wheel with:
|
|
180
|
+
|
|
181
|
+
```powershell
|
|
182
|
+
python -m build
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The artifacts are written to `dist/`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# DT360 Python JWT Manager
|
|
2
|
+
|
|
3
|
+
JWT codec and Flask integration for the DT360 services. The package wraps
|
|
4
|
+
PyJWT's codec, derives short HMAC secrets to a 64-byte HS512 key, and can
|
|
5
|
+
resolve keys from a Flask app or request execution context.
|
|
6
|
+
|
|
7
|
+
## Project structure
|
|
8
|
+
|
|
9
|
+
The layers have separate responsibilities and share the same codec:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
src/dt360_py_jwt_manager/
|
|
13
|
+
jwt.py PyJWT-compatible codec and module functions
|
|
14
|
+
core/
|
|
15
|
+
FlaskJWTManager.py Flask config, key selection, and callbacks
|
|
16
|
+
JwtManager.py Docsmanager-specific token helpers
|
|
17
|
+
helper/
|
|
18
|
+
JwtKeyDerivation.py HS512 key normalization and derivation
|
|
19
|
+
URLEncodedTokenUtilities.py Java-compatible encrypted-token decoding
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For the full architecture, component responsibilities, key-resolution flows,
|
|
23
|
+
security behavior, packaging details, and end-to-end usage examples, see
|
|
24
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
25
|
+
|
|
26
|
+
`PyJWT` in `jwt.py` is the common signing and verification layer. It owns
|
|
27
|
+
algorithm-aware key preparation, the optional legacy-key fallback, strict
|
|
28
|
+
minimum-key handling, and the single candidate-key verification path. The
|
|
29
|
+
module-level `encode()` and `decode()` functions are convenience entry points
|
|
30
|
+
that use one default codec instance.
|
|
31
|
+
|
|
32
|
+
`JWTManager` (also exported as `FlaskJWTManager`) is the Flask adapter. It
|
|
33
|
+
selects keys from a context-local override, callback/provider, or Flask app
|
|
34
|
+
configuration and delegates signing and verification to `PyJWT`. If key
|
|
35
|
+
selection needs the header and claims, it reads them together in one unverified
|
|
36
|
+
parse, then verifies the signature before returning claims. It does not
|
|
37
|
+
construct application-specific claims. `JwtManager` is the Docsmanager
|
|
38
|
+
adapter: it maps V1, V2, Box, and Spring key types to the Java service's token
|
|
39
|
+
formats, builds the expected claims, and normalizes Bearer or URL-encrypted
|
|
40
|
+
tokens before delegating cryptographic work to `PyJWT`.
|
|
41
|
+
|
|
42
|
+
`JwtKeyDerivation` contains the shared key-format rules. Both adapters pass
|
|
43
|
+
normalized source secrets to the codec; neither reimplements PBKDF2 or stores
|
|
44
|
+
a parallel set of padded legacy keys. `URLEncodedTokenUtilities` handles only
|
|
45
|
+
the Java URL-token envelope and does not participate in JWT signing.
|
|
46
|
+
|
|
47
|
+
## Replace a direct PyJWT import
|
|
48
|
+
|
|
49
|
+
Replace `import jwt as jwt_lib` with:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from dt360_py_jwt_manager import jwt as jwt_lib
|
|
53
|
+
|
|
54
|
+
claims = jwt_lib.decode(
|
|
55
|
+
token,
|
|
56
|
+
secret,
|
|
57
|
+
algorithms=["HS512"],
|
|
58
|
+
options={"enforce_minimum_key_length": True},
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The package's module-level `encode()` and `decode()` functions use a codec
|
|
63
|
+
configured with `enforce_minimum_key_length=True`. You can also create an
|
|
64
|
+
instance with `PyJWT(options={"enforce_minimum_key_length": True})`.
|
|
65
|
+
|
|
66
|
+
Short HMAC secrets are converted to a 64-byte key using the Java-compatible
|
|
67
|
+
PBKDF2-HMAC-SHA512 parameters: 220,000 iterations, the fixed salt, and the
|
|
68
|
+
`JWT-HS512-V1` context. This allows HS256, HS384, and HS512 operations to meet
|
|
69
|
+
their minimum key lengths. RSA keys are passed to PyJWT unchanged, where the
|
|
70
|
+
minimum RSA key size is enforced.
|
|
71
|
+
|
|
72
|
+
## Use from Flask
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from flask import current_app
|
|
76
|
+
from dt360_py_jwt_manager import JWTManager
|
|
77
|
+
|
|
78
|
+
jwt = JWTManager(app)
|
|
79
|
+
app.config["JWT_SECRET_KEY"] = Globals.key
|
|
80
|
+
app.config["JWT_SIGNING_KEYS"] = [
|
|
81
|
+
Globals.key,
|
|
82
|
+
Globals.user_key,
|
|
83
|
+
Globals.spring_key,
|
|
84
|
+
]
|
|
85
|
+
app.config["JWT_DECODE_ALGORITHMS"] = ["HS512", "HS384", "HS256", "RS256"]
|
|
86
|
+
|
|
87
|
+
# Optional provider for request or tenant-specific key selection.
|
|
88
|
+
app.config["JWT_KEY_PROVIDER"] = lambda purpose, header, payload: current_app.config[
|
|
89
|
+
"JWT_SECRET_KEY"
|
|
90
|
+
]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`JWTManager` registers itself in `app.extensions` and reads configuration when
|
|
94
|
+
encoding or decoding, so keys can be set after `JWTManager(app)`. It supports
|
|
95
|
+
the `decode_key_loader`, `invalid_token_loader`, `expired_token_loader`, and
|
|
96
|
+
`unauthorized_loader` callback registration patterns. A key loader receives
|
|
97
|
+
the unverified header and payload; its returned key is still signature-verified
|
|
98
|
+
by the wrapper.
|
|
99
|
+
|
|
100
|
+
This package replaces the PyJWT codec and supplies Flask key configuration. If
|
|
101
|
+
the application uses Flask-JWT-Extended's `@jwt_required` or
|
|
102
|
+
`get_jwt_identity()`, keep that extension for request authentication and
|
|
103
|
+
register the wrapper's resolver as its decode-key callback. The callback must
|
|
104
|
+
return the derived key that matched the signature. Returning the original
|
|
105
|
+
short secret would make the decorator verify with that short secret again.
|
|
106
|
+
|
|
107
|
+
If Flask-JWT-Extended remains responsible for request decorators, keep its
|
|
108
|
+
manager and replace only the direct PyJWT import:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from flask_jwt_extended import JWTManager as FlaskJWTManager
|
|
112
|
+
from dt360_py_jwt_manager import jwt as jwt_lib
|
|
113
|
+
|
|
114
|
+
flask_jwt = FlaskJWTManager(app)
|
|
115
|
+
|
|
116
|
+
@flask_jwt.decode_key_loader
|
|
117
|
+
def load_decode_key(jwt_header, jwt_data):
|
|
118
|
+
return jwt_lib.resolve_verification_key(
|
|
119
|
+
extract_token_from_request(),
|
|
120
|
+
app.config["JWT_SIGNING_KEYS"],
|
|
121
|
+
algorithms=app.config["JWT_DECODE_ALGORITHMS"],
|
|
122
|
+
allow_legacy_key=app.config.get("JWT_ALLOW_LEGACY_KEY", False),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@flask_jwt.encode_key_loader
|
|
126
|
+
def load_encode_key(identity):
|
|
127
|
+
return jwt_lib.prepare_key(
|
|
128
|
+
app.config["JWT_SECRET_KEY"], app.config.get("JWT_ALGORITHM", "HS512")
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Flask-JWT-Extended's decode-key callback receives the unverified header and
|
|
133
|
+
claims and must return the key used by its decorator to verify the token. The
|
|
134
|
+
resolver checks which configured source key matches the token, returns its
|
|
135
|
+
derived key, and leaves expiration and claim checks to Flask-JWT-Extended.
|
|
136
|
+
This is the integration point that makes `@jwt_required()` use the same
|
|
137
|
+
derivation rules as direct calls to `jwt.encode()` and `jwt.decode()`. The
|
|
138
|
+
encode-key callback applies the same derivation to tokens created with
|
|
139
|
+
`create_access_token()`. Set `JWT_ALGORITHM="HS512"` explicitly when the
|
|
140
|
+
service must issue HS512 tokens.
|
|
141
|
+
|
|
142
|
+
For an explicit execution-context override, use:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
with jwt.key_context(tenant_secret):
|
|
146
|
+
claims = jwt.decode(token, algorithms=["HS512"])
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Keys resolve in this order: `key_context()`, a registered decode key loader,
|
|
150
|
+
`JWT_KEY_PROVIDER`, `JWT_SIGNING_KEYS` (decode only), and `JWT_SECRET_KEY`.
|
|
151
|
+
The provider receives `(purpose, header, payload)` and may return one key or an
|
|
152
|
+
ordered sequence of keys.
|
|
153
|
+
|
|
154
|
+
## Existing and legacy tokens
|
|
155
|
+
|
|
156
|
+
When decoding a short HMAC secret, the derived key is tried first. To continue
|
|
157
|
+
accepting tokens signed with the old short raw key, enable
|
|
158
|
+
`allow_legacy_key=True` on `PyJWT`/`JwtManager`, or set
|
|
159
|
+
`app.config["JWT_ALLOW_LEGACY_KEY"] = True` for the Flask manager. The legacy
|
|
160
|
+
key is used for verification only. PBKDF2 does not add entropy; new deployments
|
|
161
|
+
should use random secrets with at least 64 bytes.
|
|
162
|
+
|
|
163
|
+
The high-level `JwtManager` also provides V1, V2, and Box token helpers, along
|
|
164
|
+
with generic claims creation for application-specific JWTs. Session helpers
|
|
165
|
+
store `sessionData` as a JSON string, matching the Java format.
|
|
166
|
+
|
|
167
|
+
## Build artifacts
|
|
168
|
+
|
|
169
|
+
From the project root, build the source archive and wheel with:
|
|
170
|
+
|
|
171
|
+
```powershell
|
|
172
|
+
python -m build
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The artifacts are written to `dist/`.
|