pypomes-jwt 0.7.6__py3-none-any.whl → 0.7.8__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_constants.py +2 -0
- pypomes_jwt/jwt_data.py +79 -8
- pypomes_jwt/jwt_pomes.py +97 -53
- {pypomes_jwt-0.7.6.dist-info → pypomes_jwt-0.7.8.dist-info}/METADATA +1 -1
- pypomes_jwt-0.7.8.dist-info/RECORD +8 -0
- pypomes_jwt-0.7.6.dist-info/RECORD +0 -8
- {pypomes_jwt-0.7.6.dist-info → pypomes_jwt-0.7.8.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.7.6.dist-info → pypomes_jwt-0.7.8.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/jwt_constants.py
CHANGED
|
@@ -18,6 +18,7 @@ JWT_DB_CLIENT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_CLIENT") # fo
|
|
|
18
18
|
JWT_DB_DRIVER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_DRIVER") # for SQLServer, only
|
|
19
19
|
JWT_DB_TABLE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
|
|
20
20
|
JWT_DB_COL_ACCOUNT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ACCOUNT")
|
|
21
|
+
JWT_DB_COL_HASH: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_HASH")
|
|
21
22
|
JWT_DB_COL_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_TOKEN")
|
|
22
23
|
# define the database engine
|
|
23
24
|
__db_engine: str | None = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
|
|
@@ -55,6 +56,7 @@ JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX
|
|
|
55
56
|
def_value=86400)
|
|
56
57
|
JWT_ROTATE_TOKENS: Final[bool] = env_get_bool(key=f"{APP_PREFIX}_JWT_ROTATE_TOKENS",
|
|
57
58
|
def_value=True)
|
|
59
|
+
JWT_ACCOUNT_LIMIT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCOUNT_LIMIT")
|
|
58
60
|
|
|
59
61
|
# recommended: allow the encode and decode keys to be generated anew when app starts
|
|
60
62
|
__encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODE_KEY")
|
pypomes_jwt/jwt_data.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import jwt
|
|
2
3
|
import requests
|
|
3
4
|
import string
|
|
@@ -9,8 +10,9 @@ from threading import Lock
|
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from .jwt_constants import (
|
|
12
|
-
JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY,
|
|
13
|
-
|
|
13
|
+
JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY, JWT_DECODING_KEY,
|
|
14
|
+
JWT_ROTATE_TOKENS, JWT_ACCOUNT_LIMIT, JWT_DB_ENGINE,
|
|
15
|
+
JWT_DB_TABLE, JWT_DB_COL_ACCOUNT, JWT_DB_COL_HASH, JWT_DB_COL_TOKEN
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -200,6 +202,7 @@ class JwtData:
|
|
|
200
202
|
# obtain new tokens
|
|
201
203
|
current_claims["jti"] = str_random(size=32,
|
|
202
204
|
chars=string.ascii_letters + string.digits)
|
|
205
|
+
current_claims["sub"] = account_id
|
|
203
206
|
current_claims["iss"] = account_data.get("reference-url")
|
|
204
207
|
|
|
205
208
|
# where is the JWT service provider ?
|
|
@@ -224,6 +227,8 @@ class JwtData:
|
|
|
224
227
|
# JWT service is being provided locally
|
|
225
228
|
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
226
229
|
current_claims["iat"] = just_now
|
|
230
|
+
if JWT_DEFAULT_ALGORITHM in []:
|
|
231
|
+
current_claims["kid"] = JWT_DECODING_KEY
|
|
227
232
|
|
|
228
233
|
# retrieve the refresh token associated with the account id
|
|
229
234
|
refresh_token: str | None = None
|
|
@@ -257,12 +262,10 @@ class JwtData:
|
|
|
257
262
|
algorithm=JWT_DEFAULT_ALGORITHM)
|
|
258
263
|
# persist the new refresh token
|
|
259
264
|
if JWT_DB_ENGINE:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
JWT_DB_COL_TOKEN: refresh_token},
|
|
265
|
-
logger=logger)
|
|
265
|
+
# persist the refresh token
|
|
266
|
+
_jwt_persist_token(account_id=account_id,
|
|
267
|
+
jwt_token=refresh_token,
|
|
268
|
+
logger=logger)
|
|
266
269
|
if errors:
|
|
267
270
|
raise RuntimeError(" - ".join(errors))
|
|
268
271
|
|
|
@@ -340,3 +343,71 @@ def _jwt_request_token(errors: list[str],
|
|
|
340
343
|
errors.append(err_msg)
|
|
341
344
|
|
|
342
345
|
return result
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _jwt_persist_token(account_id: str,
|
|
349
|
+
jwt_token: str,
|
|
350
|
+
logger: Logger = None) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Persist the given token, making sure that the account limit is adhered to.
|
|
353
|
+
|
|
354
|
+
:param jwt_token: the JWT token to persist
|
|
355
|
+
:param account_id: the account identification
|
|
356
|
+
:returns: *True* if token was persisted, *False* otherwise
|
|
357
|
+
"""
|
|
358
|
+
from pypomes_db import db_select, db_insert, db_delete
|
|
359
|
+
from .jwt_pomes import jwt_get_claims
|
|
360
|
+
|
|
361
|
+
# retrieve the account's tokens
|
|
362
|
+
errors: list[str] = []
|
|
363
|
+
recs: list[tuple[str]] = db_select(errors=errors,
|
|
364
|
+
sel_stmt=f"SELECT {JWT_DB_COL_HASH}, {JWT_DB_COL_TOKEN} FROM {JWT_DB_TABLE} ",
|
|
365
|
+
where_data={JWT_DB_COL_ACCOUNT: f"'{account_id}'"})
|
|
366
|
+
if not errors:
|
|
367
|
+
if logger:
|
|
368
|
+
logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
|
|
369
|
+
# remove the expired tokens
|
|
370
|
+
expired: list[str] = []
|
|
371
|
+
for rec in recs:
|
|
372
|
+
token: str = rec[1]
|
|
373
|
+
token_hash: str = rec[0]
|
|
374
|
+
op_errors: list[str] = []
|
|
375
|
+
token_claims: dict[str, Any] = jwt_get_claims(errors=op_errors,
|
|
376
|
+
token=token,
|
|
377
|
+
validate=False,
|
|
378
|
+
logger=logger)
|
|
379
|
+
if op_errors:
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
exp: int = token_claims["payload"]["exp"]
|
|
383
|
+
if exp < datetime.now(tz=timezone.utc).timestamp():
|
|
384
|
+
expired.append(token_hash)
|
|
385
|
+
|
|
386
|
+
if not errors:
|
|
387
|
+
# remove expired tokens from persistence
|
|
388
|
+
# ruff: noqa: SIM102
|
|
389
|
+
if expired:
|
|
390
|
+
if db_delete(errors=errors,
|
|
391
|
+
delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
|
|
392
|
+
where_data={
|
|
393
|
+
JWT_DB_COL_ACCOUNT: f"'{account_id}'",
|
|
394
|
+
JWT_DB_COL_HASH: expired
|
|
395
|
+
},
|
|
396
|
+
logger=logger) is not None:
|
|
397
|
+
if logger:
|
|
398
|
+
logger.debug(msg=f"{len(expired)} tokens removed from storage")
|
|
399
|
+
if 0 < JWT_ACCOUNT_LIMIT <= len(recs) - len(expired):
|
|
400
|
+
errors.append("Maximum number of active sessions "
|
|
401
|
+
f"({JWT_ACCOUNT_LIMIT}) exceeded for account '{account_id}'")
|
|
402
|
+
# persist token
|
|
403
|
+
if not errors:
|
|
404
|
+
# ruff: noqa: S324
|
|
405
|
+
hasher = hashlib.new(name="md5")
|
|
406
|
+
hasher.update(jwt_token.encode())
|
|
407
|
+
token_hash: str = hasher.digest().decode()
|
|
408
|
+
db_insert(errors=errors,
|
|
409
|
+
insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
|
|
410
|
+
insert_data={"ds_hash": token_hash,
|
|
411
|
+
"ds_token": jwt_token})
|
|
412
|
+
|
|
413
|
+
return len(errors) == 0
|
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -31,6 +31,51 @@ def jwt_needed(func: callable) -> callable:
|
|
|
31
31
|
return wrapper
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def jwt_verify_request(request: Request,
|
|
35
|
+
logger: Logger = None) -> Response:
|
|
36
|
+
"""
|
|
37
|
+
Verify wheher the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
38
|
+
|
|
39
|
+
:param request: the request to be verified
|
|
40
|
+
:param logger: optional logger
|
|
41
|
+
:return: *None* if the request is valid, otherwise a *Response* object reporting the error
|
|
42
|
+
"""
|
|
43
|
+
# initialize the return variable
|
|
44
|
+
result: Response | None = None
|
|
45
|
+
|
|
46
|
+
if logger:
|
|
47
|
+
logger.debug(msg="Validate a JWT token")
|
|
48
|
+
err_msg: str | None = None
|
|
49
|
+
|
|
50
|
+
# retrieve the authorization from the request header
|
|
51
|
+
auth_header: str = request.headers.get("Authorization")
|
|
52
|
+
|
|
53
|
+
# was a 'Bearer' authorization obtained ?
|
|
54
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
55
|
+
# yes, extract and validate the JWT access token
|
|
56
|
+
token: str = auth_header.split(" ")[1]
|
|
57
|
+
if logger:
|
|
58
|
+
logger.debug(msg=f"Token is '{token}'")
|
|
59
|
+
errors: list[str] = []
|
|
60
|
+
jwt_validate_token(errors=errors,
|
|
61
|
+
nature="A",
|
|
62
|
+
token=token)
|
|
63
|
+
if errors:
|
|
64
|
+
err_msg = "; ".join(errors)
|
|
65
|
+
else:
|
|
66
|
+
# no 'Bearer' found, report the error
|
|
67
|
+
err_msg = "Request header has no 'Bearer' data"
|
|
68
|
+
|
|
69
|
+
# log the error and deny the authorization
|
|
70
|
+
if err_msg:
|
|
71
|
+
if logger:
|
|
72
|
+
logger.error(msg=err_msg)
|
|
73
|
+
result = Response(response="Authorization failed",
|
|
74
|
+
status=401)
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
34
79
|
def jwt_assert_access(account_id: str) -> bool:
|
|
35
80
|
"""
|
|
36
81
|
Determine whether access for *account_id* has been established.
|
|
@@ -247,7 +292,8 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
247
292
|
if not op_errors:
|
|
248
293
|
try:
|
|
249
294
|
result = __jwt_data.issue_tokens(account_id=account_id,
|
|
250
|
-
account_claims=account_claims
|
|
295
|
+
account_claims=account_claims,
|
|
296
|
+
logger=logger)
|
|
251
297
|
if logger:
|
|
252
298
|
logger.debug(msg=f"Data is '{result}'")
|
|
253
299
|
except Exception as e:
|
|
@@ -265,12 +311,43 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
265
311
|
|
|
266
312
|
def jwt_get_claims(errors: list[str] | None,
|
|
267
313
|
token: str,
|
|
314
|
+
validate: bool = True,
|
|
268
315
|
logger: Logger = None) -> dict[str, Any]:
|
|
269
316
|
"""
|
|
270
317
|
Obtain and return the claims set of a JWT *token*.
|
|
271
318
|
|
|
319
|
+
If *validate* is set to *True*, tha following pieces of information are verified:
|
|
320
|
+
- the token was issued and signed by the local provider, and is not corrupted
|
|
321
|
+
- the claim 'exp' is present and is in the future
|
|
322
|
+
- the claim 'nbf' is present and is in the past
|
|
323
|
+
|
|
324
|
+
Structure of the returned data:
|
|
325
|
+
{
|
|
326
|
+
"header": {
|
|
327
|
+
"alg": "HS256",
|
|
328
|
+
"typ": "JWT"
|
|
329
|
+
},
|
|
330
|
+
"payload": {
|
|
331
|
+
"birthdate": "1980-01-01",
|
|
332
|
+
"email": "jdoe@mail.com",
|
|
333
|
+
"exp": 1516640454,
|
|
334
|
+
"iat": 1516239022,
|
|
335
|
+
"iss": "https://my_id_provider/issue",
|
|
336
|
+
"jti": "Uhsdfgr67FGH567qwSDF33er89retert",
|
|
337
|
+
"gender": "M,
|
|
338
|
+
"name": "John Doe",
|
|
339
|
+
"nbt": 1516249022
|
|
340
|
+
"sub": "1234567890",
|
|
341
|
+
"roles": [
|
|
342
|
+
"administrator",
|
|
343
|
+
"operator"
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
272
348
|
:param errors: incidental error messages
|
|
273
349
|
:param token: the token to be inspected for claims
|
|
350
|
+
:param validate: If *True*, verifies the token's data
|
|
274
351
|
:param logger: optional logger
|
|
275
352
|
:return: the token's claimset, or *None* if error
|
|
276
353
|
"""
|
|
@@ -281,14 +358,26 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
281
358
|
logger.debug(msg=f"Retrieve claims for token '{token}'")
|
|
282
359
|
|
|
283
360
|
try:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
361
|
+
# retrieve the token's payload
|
|
362
|
+
if validate:
|
|
363
|
+
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
364
|
+
options={
|
|
365
|
+
"verify_signature": True,
|
|
366
|
+
"verify_exp": True,
|
|
367
|
+
"verify_nbf": True
|
|
368
|
+
},
|
|
369
|
+
key=JWT_DECODING_KEY,
|
|
370
|
+
require=["exp", "nbf"],
|
|
371
|
+
algorithms=[JWT_DEFAULT_ALGORITHM])
|
|
290
372
|
else:
|
|
291
|
-
|
|
373
|
+
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
374
|
+
options={"verify_signature": False})
|
|
375
|
+
# retrieve the token's header
|
|
376
|
+
header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
377
|
+
result = {
|
|
378
|
+
"header": header,
|
|
379
|
+
"payload": payload
|
|
380
|
+
}
|
|
292
381
|
except Exception as e:
|
|
293
382
|
if logger:
|
|
294
383
|
logger.error(msg=str(e))
|
|
@@ -296,48 +385,3 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
296
385
|
errors.append(str(e))
|
|
297
386
|
|
|
298
387
|
return result
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def jwt_verify_request(request: Request,
|
|
302
|
-
logger: Logger = None) -> Response:
|
|
303
|
-
"""
|
|
304
|
-
Verify wheher the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
305
|
-
|
|
306
|
-
:param request: the request to be verified
|
|
307
|
-
:param logger: optional logger
|
|
308
|
-
:return: *None* if the request is valid, otherwise a *Response* object reporting the error
|
|
309
|
-
"""
|
|
310
|
-
# initialize the return variable
|
|
311
|
-
result: Response | None = None
|
|
312
|
-
|
|
313
|
-
if logger:
|
|
314
|
-
logger.debug(msg="Validate a JWT token")
|
|
315
|
-
err_msg: str | None = None
|
|
316
|
-
|
|
317
|
-
# retrieve the authorization from the request header
|
|
318
|
-
auth_header: str = request.headers.get("Authorization")
|
|
319
|
-
|
|
320
|
-
# was a 'Bearer' authorization obtained ?
|
|
321
|
-
if auth_header and auth_header.startswith("Bearer "):
|
|
322
|
-
# yes, extract and validate the JWT access token
|
|
323
|
-
token: str = auth_header.split(" ")[1]
|
|
324
|
-
if logger:
|
|
325
|
-
logger.debug(msg=f"Token is '{token}'")
|
|
326
|
-
errors: list[str] = []
|
|
327
|
-
jwt_validate_token(errors=errors,
|
|
328
|
-
nature="A",
|
|
329
|
-
token=token)
|
|
330
|
-
if errors:
|
|
331
|
-
err_msg = "; ".join(errors)
|
|
332
|
-
else:
|
|
333
|
-
# no 'Bearer' found, report the error
|
|
334
|
-
err_msg = "Request header has no 'Bearer' data"
|
|
335
|
-
|
|
336
|
-
# log the error and deny the authorization
|
|
337
|
-
if err_msg:
|
|
338
|
-
if logger:
|
|
339
|
-
logger.error(msg=err_msg)
|
|
340
|
-
result = Response(response="Authorization failed",
|
|
341
|
-
status=401)
|
|
342
|
-
|
|
343
|
-
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pypomes_jwt/__init__.py,sha256=dkWeYPNwypjwFuTjx4YtC8QV9ihykF4xcJJ7x86Wc5g,1130
|
|
2
|
+
pypomes_jwt/jwt_constants.py,sha256=8QYpYbpuNfeq15Gwkww2VtEXHFOIkZ1pPS-GU_hifs4,4491
|
|
3
|
+
pypomes_jwt/jwt_data.py,sha256=aZCb6SAgZEz5dFhLWcinr1mc9HZndYQ1gSw5z-5cnCA,19622
|
|
4
|
+
pypomes_jwt/jwt_pomes.py,sha256=nrvu7zIAp0Blq3wAHbvtKLKOVBGlqCttTDOu-LsZxrE,14650
|
|
5
|
+
pypomes_jwt-0.7.8.dist-info/METADATA,sha256=k_n1-ruFPdSXdEHhrAvLDNFoxeXlMAuHZh7Qg5X7Dy0,599
|
|
6
|
+
pypomes_jwt-0.7.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pypomes_jwt-0.7.8.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
+
pypomes_jwt-0.7.8.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=dkWeYPNwypjwFuTjx4YtC8QV9ihykF4xcJJ7x86Wc5g,1130
|
|
2
|
-
pypomes_jwt/jwt_constants.py,sha256=xUX2raEaDUPJsjAm78lQ0APs4KSs5GYBxSPC5QKrrFE,4327
|
|
3
|
-
pypomes_jwt/jwt_data.py,sha256=YH3v8zvOURkB_o0XLMu2y2sFkKCxZBmbyWU5rC2gre4,16419
|
|
4
|
-
pypomes_jwt/jwt_pomes.py,sha256=eRQSLA8DVzr9MfVOh5bl6Zz3iOQtRIAQjzIfoz6Fj9o,12915
|
|
5
|
-
pypomes_jwt-0.7.6.dist-info/METADATA,sha256=5N0G1VHBXKHiH-QAnUBC9UgeqpjOh9-4-BYMtAl680U,599
|
|
6
|
-
pypomes_jwt-0.7.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
pypomes_jwt-0.7.6.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
-
pypomes_jwt-0.7.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|