pypomes-jwt 0.8.9__py3-none-any.whl → 0.9.0__py3-none-any.whl
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 pypomes-jwt might be problematic. Click here for more details.
- pypomes_jwt/jwt_data.py +21 -11
- pypomes_jwt/jwt_pomes.py +19 -19
- {pypomes_jwt-0.8.9.dist-info → pypomes_jwt-0.9.0.dist-info}/METADATA +2 -2
- pypomes_jwt-0.9.0.dist-info/RECORD +8 -0
- pypomes_jwt-0.8.9.dist-info/RECORD +0 -8
- {pypomes_jwt-0.8.9.dist-info → pypomes_jwt-0.9.0.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.8.9.dist-info → pypomes_jwt-0.9.0.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/jwt_data.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import base64
|
|
2
1
|
import jwt
|
|
3
2
|
import requests
|
|
4
3
|
import string
|
|
5
4
|
import sys
|
|
5
|
+
from base64 import urlsafe_b64encode
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from logging import Logger
|
|
8
8
|
from pypomes_core import str_random
|
|
@@ -33,11 +33,13 @@ class JwtData:
|
|
|
33
33
|
"access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
|
|
34
34
|
"refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
|
|
35
35
|
"grace-interval": <int> # time to wait for token to be valid, in seconds
|
|
36
|
+
# optional
|
|
36
37
|
"token-audience": <string> # the audience the token is intended for
|
|
37
38
|
"token_nonce": <string> # value used to associate a client session with a token
|
|
38
39
|
"claims": {
|
|
39
40
|
"valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
40
41
|
"valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
42
|
+
# optional
|
|
41
43
|
"birthdate": <string>, # subject's birth date
|
|
42
44
|
"email": <string>, # subject's email
|
|
43
45
|
"gender": <string>, # subject's gender
|
|
@@ -63,7 +65,6 @@ class JwtData:
|
|
|
63
65
|
"jti": <string> # JWT id
|
|
64
66
|
"sub": <string> # subject (the account identification)
|
|
65
67
|
"nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
|
|
66
|
-
# optional:
|
|
67
68
|
"aud": <string> # token audience
|
|
68
69
|
"nbt": <timestamp> # not before time
|
|
69
70
|
|
|
@@ -81,8 +82,10 @@ class JwtData:
|
|
|
81
82
|
The token header has these items:
|
|
82
83
|
"alg": <string> # the algorithm used to sign the token (one of 'HS256', 'HS512', 'RSA256', 'RSA512')
|
|
83
84
|
"typ": <string> # the token type (fixed to 'JWT'
|
|
84
|
-
"kid": <string> # a reference to the
|
|
85
|
-
|
|
85
|
+
"kid": <string> # a reference to the token type and the key to its location in the token database
|
|
86
|
+
|
|
87
|
+
If issued by the local server, "kid" holds the key to the corresponding record in the token database.
|
|
88
|
+
It starts with *A* for (*Access*) or *R* (for *Refresh*) followed its integer key.
|
|
86
89
|
"""
|
|
87
90
|
def __init__(self) -> None:
|
|
88
91
|
"""
|
|
@@ -242,17 +245,23 @@ class JwtData:
|
|
|
242
245
|
# JWT service is being provided locally
|
|
243
246
|
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
244
247
|
current_claims["iat"] = just_now
|
|
245
|
-
|
|
248
|
+
grace_interval = account_data.get("grace-interval")
|
|
249
|
+
if grace_interval:
|
|
250
|
+
account_data["nbf"] = just_now + grace_interval
|
|
251
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
252
|
+
tz=timezone.utc).isoformat()
|
|
253
|
+
else:
|
|
254
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
255
|
+
tz=timezone.utc).isoformat()
|
|
246
256
|
# issue a candidate refresh token first, and persist it
|
|
247
257
|
current_claims["exp"] = just_now + account_data.get("refresh-max-age")
|
|
248
|
-
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
249
|
-
tz=timezone.utc).isoformat()
|
|
250
258
|
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
251
259
|
tz=timezone.utc).isoformat()
|
|
252
260
|
# may raise an exception
|
|
253
261
|
refresh_token: str = jwt.encode(payload=current_claims,
|
|
254
262
|
key=JWT_ENCODING_KEY,
|
|
255
|
-
algorithm=JWT_DEFAULT_ALGORITHM
|
|
263
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
264
|
+
headers={"kid": "R0"})
|
|
256
265
|
# obtain a DB connection (may raise an exception)
|
|
257
266
|
db_conn: Any = db_connect(errors=errors,
|
|
258
267
|
logger=logger)
|
|
@@ -266,7 +275,7 @@ class JwtData:
|
|
|
266
275
|
refresh_token = jwt.encode(payload=current_claims,
|
|
267
276
|
key=JWT_ENCODING_KEY,
|
|
268
277
|
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
269
|
-
headers={"kid":
|
|
278
|
+
headers={"kid": f"R{token_id}"})
|
|
270
279
|
# persist it
|
|
271
280
|
db_update(errors=errors,
|
|
272
281
|
update_stmt=f"UPDATE {JWT_DB_TABLE}",
|
|
@@ -286,7 +295,8 @@ class JwtData:
|
|
|
286
295
|
# may raise an exception
|
|
287
296
|
access_token: str = jwt.encode(payload=current_claims,
|
|
288
297
|
key=JWT_ENCODING_KEY,
|
|
289
|
-
algorithm=JWT_DEFAULT_ALGORITHM
|
|
298
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
299
|
+
headers={"kid": f"A{token_id}"})
|
|
290
300
|
# return the token data
|
|
291
301
|
result = {
|
|
292
302
|
"access_token": access_token,
|
|
@@ -453,7 +463,7 @@ def _jwt_persist_token(errors: list[str],
|
|
|
453
463
|
insert_data={JWT_DB_COL_ACCOUNT: account_id,
|
|
454
464
|
JWT_DB_COL_TOKEN: jwt_token,
|
|
455
465
|
JWT_DB_COL_ALGORITHM: JWT_DEFAULT_ALGORITHM,
|
|
456
|
-
JWT_DB_COL_DECODER:
|
|
466
|
+
JWT_DB_COL_DECODER: urlsafe_b64encode(JWT_DECODING_KEY).decode()},
|
|
457
467
|
connection=db_conn,
|
|
458
468
|
logger=logger)
|
|
459
469
|
if errors:
|
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import base64
|
|
2
1
|
import jwt
|
|
2
|
+
from base64 import urlsafe_b64decode
|
|
3
3
|
from flask import Request, Response, request
|
|
4
4
|
from logging import Logger
|
|
5
5
|
from pypomes_db import db_select, db_delete
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from . import JWT_DB_COL_ACCOUNT
|
|
9
9
|
from .jwt_constants import (
|
|
@@ -61,7 +61,7 @@ def jwt_verify_request(request: Request,
|
|
|
61
61
|
logger.debug(msg="Token was found")
|
|
62
62
|
errors: list[str] = []
|
|
63
63
|
jwt_validate_token(errors=errors,
|
|
64
|
-
nature="A",
|
|
64
|
+
nature=["A"],
|
|
65
65
|
token=token)
|
|
66
66
|
if errors:
|
|
67
67
|
err_msg = "; ".join(errors)
|
|
@@ -157,17 +157,20 @@ def jwt_remove_account(account_id: str,
|
|
|
157
157
|
|
|
158
158
|
def jwt_validate_token(errors: list[str] | None,
|
|
159
159
|
token: str,
|
|
160
|
-
nature:
|
|
160
|
+
nature: list[str] = None,
|
|
161
161
|
account_id: str = None,
|
|
162
162
|
logger: Logger = None) -> dict[str, Any] | None:
|
|
163
163
|
"""
|
|
164
164
|
Verify if *token* ia a valid JWT token.
|
|
165
165
|
|
|
166
166
|
Raise an appropriate exception if validation failed.
|
|
167
|
+
if *nature* is provided, it is checked whether *token* has been locally issued and is of a appropriate nature.
|
|
168
|
+
A token issued locally has the header claim *kid* starting with "A" (for *Access*) or "R" (for *Refresh*),
|
|
169
|
+
followed by its id in the token database.
|
|
167
170
|
|
|
168
171
|
:param errors: incidental error messages
|
|
169
172
|
:param token: the token to be validated
|
|
170
|
-
:param nature:
|
|
173
|
+
:param nature: one of more prefixes identifying the nature of locally issued tokens
|
|
171
174
|
:param account_id: optionally, validate the token's account owner
|
|
172
175
|
:param logger: optional logger
|
|
173
176
|
:return: The token's claims (header and payload) if if is valid, *None* otherwise
|
|
@@ -179,17 +182,16 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
179
182
|
|
|
180
183
|
# extract needed data from token header
|
|
181
184
|
token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
182
|
-
token_kid:
|
|
185
|
+
token_kid: str = token_header.get("kid")
|
|
183
186
|
token_alg: str | None = None
|
|
184
187
|
token_decoder: bytes | None = None
|
|
185
188
|
op_errors: list[str] = []
|
|
186
189
|
|
|
187
190
|
# retrieve token data from database
|
|
188
|
-
if
|
|
189
|
-
|
|
190
|
-
op_errors.append(f"Token is not {nat} token")
|
|
191
|
+
if nature and not (token_kid and token_kid[0:1] in nature):
|
|
192
|
+
op_errors.append("Invalid token")
|
|
191
193
|
elif token_kid:
|
|
192
|
-
where_data: dict[str,
|
|
194
|
+
where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
|
|
193
195
|
if account_id:
|
|
194
196
|
where_data[JWT_DB_COL_ACCOUNT] = account_id
|
|
195
197
|
recs: list[tuple[str]] = db_select(errors=op_errors,
|
|
@@ -199,7 +201,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
199
201
|
logger=logger)
|
|
200
202
|
if recs:
|
|
201
203
|
token_alg = recs[0][0]
|
|
202
|
-
token_decoder =
|
|
204
|
+
token_decoder = urlsafe_b64decode(recs[0][1])
|
|
203
205
|
else:
|
|
204
206
|
op_errors.append("Invalid token")
|
|
205
207
|
else:
|
|
@@ -215,9 +217,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
215
217
|
# ExpiredSignatureError: token and refresh period have expired
|
|
216
218
|
# InvalidSignatureError: signature does not match the one provided as part of the token
|
|
217
219
|
# ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
|
|
218
|
-
# InvalidAudienceError: 'aud' claim does not match one of the expected audience
|
|
219
220
|
# InvalidAlgorithmError: the specified algorithm is not recognized
|
|
220
|
-
# InvalidIssuerError: 'iss' claim does not match the expected issuer
|
|
221
221
|
# InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
222
222
|
# MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
223
223
|
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
@@ -227,7 +227,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
227
227
|
"verify_nbf": True
|
|
228
228
|
},
|
|
229
229
|
key=token_decoder,
|
|
230
|
-
require=["exp"],
|
|
230
|
+
require=["iat", "iss", "exp", "sub"],
|
|
231
231
|
algorithms=token_alg)
|
|
232
232
|
if account_id and payload.get("sub") != account_id:
|
|
233
233
|
op_errors.append("Token does not belong to account")
|
|
@@ -275,15 +275,15 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
275
275
|
op_errors: list[str] = []
|
|
276
276
|
token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
|
|
277
277
|
token=refresh_token,
|
|
278
|
-
nature="R",
|
|
278
|
+
nature=["A", "R"],
|
|
279
279
|
account_id=account_id,
|
|
280
280
|
logger=logger)
|
|
281
281
|
if not op_errors:
|
|
282
|
-
token_kid:
|
|
282
|
+
token_kid: str = token_claims["header"].get("kid")
|
|
283
283
|
db_delete(errors=op_errors,
|
|
284
284
|
delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
|
|
285
285
|
where_data={
|
|
286
|
-
JWT_DB_COL_KID: token_kid,
|
|
286
|
+
JWT_DB_COL_KID: int(token_kid[1:]),
|
|
287
287
|
JWT_DB_COL_ACCOUNT: account_id
|
|
288
288
|
},
|
|
289
289
|
logger=logger)
|
|
@@ -334,7 +334,7 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
334
334
|
# verify whether this refresh token is legitimate
|
|
335
335
|
account_claims = (jwt_validate_token(errors=op_errors,
|
|
336
336
|
token=refresh_token,
|
|
337
|
-
nature="R",
|
|
337
|
+
nature=["R"],
|
|
338
338
|
account_id=account_id,
|
|
339
339
|
logger=logger) or {}).get("payload")
|
|
340
340
|
if account_claims:
|
|
@@ -380,7 +380,7 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
380
380
|
"header": {
|
|
381
381
|
"alg": "RS256",
|
|
382
382
|
"typ": "JWT",
|
|
383
|
-
"kid": "
|
|
383
|
+
"kid": "A1234"
|
|
384
384
|
},
|
|
385
385
|
"payload": {
|
|
386
386
|
"valid-from": <YYYY-MM-DDThh:mm:ss+00:00>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A collection of Python pomes, penyeach (JWT module)
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-JWT
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-JWT/issues
|
|
@@ -13,4 +13,4 @@ Requires-Python: >=3.12
|
|
|
13
13
|
Requires-Dist: cryptography>=44.0.2
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
15
|
Requires-Dist: pypomes-core>=1.8.5
|
|
16
|
-
Requires-Dist: pypomes-db>=1.9.
|
|
16
|
+
Requires-Dist: pypomes-db>=1.9.7
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
|
|
2
|
+
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
+
pypomes_jwt/jwt_data.py,sha256=keC95-2DgWYZIzAtYEbtLtfkFSH-V_sTBiTbszsZqho,23286
|
|
4
|
+
pypomes_jwt/jwt_pomes.py,sha256=0C4u9UnzqM1ZWTW4rpw18kyvnR1biLEq5nAwZ8MMrz8,17244
|
|
5
|
+
pypomes_jwt-0.9.0.dist-info/METADATA,sha256=0KthAWPxm04v3KMnYfsaHal4_kN0tFj8er2a52A__AI,632
|
|
6
|
+
pypomes_jwt-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pypomes_jwt-0.9.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
+
pypomes_jwt-0.9.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
|
|
2
|
-
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
-
pypomes_jwt/jwt_data.py,sha256=FU4zvFr-1ZbClM8ozt9asR2EGZe-xYX3046dxWnGays,22529
|
|
4
|
-
pypomes_jwt/jwt_pomes.py,sha256=XWnP4U2K5FPbP-N5npQboNdMZ-MMLpXTfd8Yf82aFls,17217
|
|
5
|
-
pypomes_jwt-0.8.9.dist-info/METADATA,sha256=orVERU_gL14nmPjxxNMrxyEKy3q4LLBD2vMokmyECSQ,632
|
|
6
|
-
pypomes_jwt-0.8.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
pypomes_jwt-0.8.9.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
-
pypomes_jwt-0.8.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|