pypomes-jwt 0.9.6__tar.gz → 0.9.8__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 pypomes-jwt might be problematic. Click here for more details.
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/PKG-INFO +1 -1
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/pyproject.toml +1 -1
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/src/pypomes_jwt/__init__.py +2 -2
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/src/pypomes_jwt/jwt_pomes.py +49 -69
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/src/pypomes_jwt/jwt_registry.py +88 -170
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/.gitignore +0 -0
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/LICENSE +0 -0
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/README.md +0 -0
- {pypomes_jwt-0.9.6 → pypomes_jwt-0.9.8}/src/pypomes_jwt/jwt_constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -4,7 +4,7 @@ from .jwt_constants import (
|
|
|
4
4
|
JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
|
|
5
5
|
JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN,
|
|
6
6
|
JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
|
|
7
|
-
JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
|
|
7
|
+
JWT_DEFAULT_ALGORITHM, JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
|
|
8
8
|
)
|
|
9
9
|
from .jwt_pomes import (
|
|
10
10
|
jwt_needed, jwt_verify_request,
|
|
@@ -20,7 +20,7 @@ __all__ = [
|
|
|
20
20
|
"JWT_DB_TABLE", "JWT_DB_COL_KID", "JWT_DB_COL_ACCOUNT",
|
|
21
21
|
"JWT_DB_COL_ALGORITHM", "JWT_DB_COL_DECODER", "JWT_DB_COL_TOKEN",
|
|
22
22
|
"JWT_ACCOUNT_LIMIT", "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
|
|
23
|
-
"JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
|
|
23
|
+
"JWT_DEFAULT_ALGORITHM", "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
|
|
24
24
|
# jwt_pomes
|
|
25
25
|
"jwt_needed", "jwt_verify_request",
|
|
26
26
|
"jwt_assert_account", "jwt_set_account", "jwt_remove_account",
|
|
@@ -5,11 +5,11 @@ from logging import Logger
|
|
|
5
5
|
from pypomes_db import db_select, db_delete
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from . import
|
|
9
|
-
from .jwt_constants import (
|
|
8
|
+
from . import (
|
|
10
9
|
JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
|
|
11
10
|
JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
|
|
12
|
-
JWT_DB_TABLE, JWT_DB_COL_KID,
|
|
11
|
+
JWT_DB_TABLE, JWT_DB_COL_KID,
|
|
12
|
+
JWT_DB_COL_ACCOUNT, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
|
|
13
13
|
)
|
|
14
14
|
from .jwt_registry import JwtRegistry
|
|
15
15
|
|
|
@@ -85,7 +85,7 @@ def jwt_assert_account(account_id: str) -> bool:
|
|
|
85
85
|
:param account_id: the account identification
|
|
86
86
|
:return: *True* if access data exists for *account_id*, *False* otherwise
|
|
87
87
|
"""
|
|
88
|
-
return __jwt_registry.
|
|
88
|
+
return __jwt_registry.access_registry.get(account_id) is not None
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def jwt_set_account(account_id: str,
|
|
@@ -93,44 +93,31 @@ def jwt_set_account(account_id: str,
|
|
|
93
93
|
access_max_age: int = JWT_ACCESS_MAX_AGE,
|
|
94
94
|
refresh_max_age: int = JWT_REFRESH_MAX_AGE,
|
|
95
95
|
grace_interval: int = None,
|
|
96
|
-
request_timeout: int = None,
|
|
97
|
-
remote_url: str = None,
|
|
98
96
|
logger: Logger = None) -> None:
|
|
99
97
|
"""
|
|
100
98
|
Establish the data needed to obtain JWT tokens for *account_id*.
|
|
101
99
|
|
|
100
|
+
The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
|
|
101
|
+
at a minimum, *iss*, *birthdate*, *email*, *gender*, *name*, and *roles*.
|
|
102
|
+
It is enforced that the parameter *refresh_max_age* should be at least 300 seconds greater
|
|
103
|
+
than *access-max-age*.
|
|
104
|
+
|
|
102
105
|
:param account_id: the account identification
|
|
103
106
|
:param claims: the JWT claimset, as key-value pairs
|
|
104
107
|
:param access_max_age: access token duration, in seconds
|
|
105
108
|
:param refresh_max_age: refresh token duration, in seconds
|
|
106
109
|
:param grace_interval: optional time to wait for token to be valid, in seconds
|
|
107
|
-
:param request_timeout: timeout for the requests to the reference URL
|
|
108
|
-
:param remote_url: remote server's URL to obtain the JWT tokens from
|
|
109
110
|
:param logger: optional logger
|
|
110
111
|
"""
|
|
111
112
|
if logger:
|
|
112
|
-
logger.debug(msg=f"
|
|
113
|
-
|
|
114
|
-
# extract the claims provided in the remote_url URL's query string
|
|
115
|
-
if remote_url:
|
|
116
|
-
pos: int = remote_url.find("?")
|
|
117
|
-
if pos > 0:
|
|
118
|
-
claims = claims or {}
|
|
119
|
-
params: list[str] = remote_url[pos+1:].split(sep="&")
|
|
120
|
-
for param in params:
|
|
121
|
-
key: str = param.split("=")[0]
|
|
122
|
-
value: str = param.split("=")[1]
|
|
123
|
-
claims[key] = value
|
|
124
|
-
remote_url = remote_url[:pos]
|
|
113
|
+
logger.debug(msg=f"Registering account data for '{account_id}'")
|
|
125
114
|
|
|
126
115
|
# register the JWT service
|
|
127
116
|
__jwt_registry.add_account(account_id=account_id,
|
|
128
117
|
claims=claims,
|
|
129
118
|
access_max_age=access_max_age,
|
|
130
|
-
refresh_max_age=refresh_max_age,
|
|
119
|
+
refresh_max_age=max(refresh_max_age, access_max_age + 300),
|
|
131
120
|
grace_interval=grace_interval,
|
|
132
|
-
request_timeout=request_timeout,
|
|
133
|
-
remote_url=remote_url,
|
|
134
121
|
logger=logger)
|
|
135
122
|
|
|
136
123
|
|
|
@@ -158,10 +145,12 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
158
145
|
"""
|
|
159
146
|
Verify if *token* ia a valid JWT token.
|
|
160
147
|
|
|
161
|
-
Raise an appropriate exception if validation failed.
|
|
162
|
-
if *nature* is provided,
|
|
148
|
+
Raise an appropriate exception if validation failed. Attempt to validate non locally issued tokens
|
|
149
|
+
will not succeed. if *nature* is provided, validate whether *token* is of that nature.
|
|
163
150
|
A token issued locally has the header claim *kid* starting with *A* (for *Access*) or *R* (for *Refresh*),
|
|
164
151
|
followed by its id in the token database, or as a single letter in the range *[B-Z]*, less *R*.
|
|
152
|
+
If the *kid* claim contains such an id, then the cryptographic key needed for validation
|
|
153
|
+
will be obtained from the token database. Otherwise, the current decoding key is used.
|
|
165
154
|
|
|
166
155
|
:param errors: incidental error messages
|
|
167
156
|
:param token: the token to be validated
|
|
@@ -250,7 +239,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
250
239
|
|
|
251
240
|
def jwt_revoke_token(errors: list[str] | None,
|
|
252
241
|
account_id: str,
|
|
253
|
-
|
|
242
|
+
token: str,
|
|
254
243
|
logger: Logger = None) -> bool:
|
|
255
244
|
"""
|
|
256
245
|
Revoke the *refresh_token* associated with *account_id*.
|
|
@@ -259,7 +248,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
259
248
|
|
|
260
249
|
:param errors: incidental error messages
|
|
261
250
|
:param account_id: the account identification
|
|
262
|
-
:param
|
|
251
|
+
:param token: the token to be revoked
|
|
263
252
|
:param logger: optional logger
|
|
264
253
|
:return: *True* if operation could be performed, *False* otherwise
|
|
265
254
|
"""
|
|
@@ -271,7 +260,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
271
260
|
|
|
272
261
|
op_errors: list[str] = []
|
|
273
262
|
token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
|
|
274
|
-
token=
|
|
263
|
+
token=token,
|
|
275
264
|
account_id=account_id,
|
|
276
265
|
logger=logger)
|
|
277
266
|
if not op_errors:
|
|
@@ -362,10 +351,10 @@ def jwt_issue_tokens(errors: list[str] | None,
|
|
|
362
351
|
|
|
363
352
|
Structure of the return data:
|
|
364
353
|
{
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
"
|
|
354
|
+
"access-token": <jwt-token>,
|
|
355
|
+
"created-in": <timestamp>,
|
|
356
|
+
"expires-in": <seconds-to-expiration>,
|
|
357
|
+
"refresh-token": <jwt-token>
|
|
369
358
|
}
|
|
370
359
|
|
|
371
360
|
:param errors: incidental error messages
|
|
@@ -411,10 +400,10 @@ def jwt_refresh_tokens(errors: list[str] | None,
|
|
|
411
400
|
|
|
412
401
|
Structure of the return data:
|
|
413
402
|
{
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"
|
|
403
|
+
"access-token": <jwt-token>,
|
|
404
|
+
"created-in": <timestamp>,
|
|
405
|
+
"expires-in": <seconds-to-expiration>,
|
|
406
|
+
"refresh-token": <jwt-token>
|
|
418
407
|
}
|
|
419
408
|
|
|
420
409
|
:param errors: incidental error messages
|
|
@@ -430,24 +419,26 @@ def jwt_refresh_tokens(errors: list[str] | None,
|
|
|
430
419
|
logger.debug(msg=f"Refreshing a JWT token pair for '{account_id}'")
|
|
431
420
|
op_errors: list[str] = []
|
|
432
421
|
|
|
433
|
-
#
|
|
422
|
+
# assert the refresh token
|
|
434
423
|
if refresh_token:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if
|
|
424
|
+
# is the refresh token valid ?
|
|
425
|
+
account_claims = jwt_validate_token(errors=op_errors,
|
|
426
|
+
token=refresh_token,
|
|
427
|
+
nature="R",
|
|
428
|
+
account_id=account_id,
|
|
429
|
+
logger=logger)
|
|
430
|
+
# if it is, revoke current refresh token
|
|
431
|
+
if account_claims and jwt_revoke_token(errors=op_errors,
|
|
442
432
|
account_id=account_id,
|
|
443
|
-
|
|
433
|
+
token=refresh_token,
|
|
444
434
|
logger=logger):
|
|
445
435
|
# issue tokens
|
|
446
|
-
result = jwt_issue_tokens(errors=
|
|
436
|
+
result = jwt_issue_tokens(errors=op_errors,
|
|
447
437
|
account_id=account_id,
|
|
448
438
|
account_claims=account_claims,
|
|
449
439
|
logger=logger)
|
|
450
440
|
else:
|
|
441
|
+
# refresh token not found
|
|
451
442
|
op_errors.append("Refresh token was not provided")
|
|
452
443
|
|
|
453
444
|
if op_errors:
|
|
@@ -461,17 +452,13 @@ def jwt_refresh_tokens(errors: list[str] | None,
|
|
|
461
452
|
|
|
462
453
|
def jwt_get_claims(errors: list[str] | None,
|
|
463
454
|
token: str,
|
|
464
|
-
validate: bool = False,
|
|
465
455
|
logger: Logger = None) -> dict[str, Any] | None:
|
|
466
456
|
"""
|
|
467
|
-
|
|
457
|
+
Retrieve and return the claims set of a JWT *token*.
|
|
468
458
|
|
|
469
|
-
|
|
470
|
-
- the token was issued and signed by the local provider, and is not corrupted
|
|
471
|
-
- the claim 'exp' is present and is in the future
|
|
472
|
-
- the claim 'nbf' is present and is in the past
|
|
459
|
+
Any valid JWT token may be provided in *token*, as this operation is not restricted to locally issued tokens.
|
|
473
460
|
|
|
474
|
-
Structure of the returned data:
|
|
461
|
+
Structure of the returned data, for locally issued tokens:
|
|
475
462
|
{
|
|
476
463
|
"header": {
|
|
477
464
|
"alg": "RS256",
|
|
@@ -485,7 +472,7 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
485
472
|
"email": "jdoe@mail.com",
|
|
486
473
|
"exp": 1516640454,
|
|
487
474
|
"iat": 1516239022,
|
|
488
|
-
"iss": "
|
|
475
|
+
"iss": "my_jwt_provider.com",
|
|
489
476
|
"jti": "Uhsdfgr67FGH567qwSDF33er89retert",
|
|
490
477
|
"gender": "M",
|
|
491
478
|
"name": "John Doe",
|
|
@@ -500,7 +487,6 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
500
487
|
|
|
501
488
|
:param errors: incidental error messages
|
|
502
489
|
:param token: the token to be inspected for claims
|
|
503
|
-
:param validate: If *True*, verifies the token's data (defaults to *False*)
|
|
504
490
|
:param logger: optional logger
|
|
505
491
|
:return: the token's claimset, or *None* if error
|
|
506
492
|
"""
|
|
@@ -511,19 +497,13 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
511
497
|
logger.debug(msg="Retrieve claims for token")
|
|
512
498
|
|
|
513
499
|
try:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
522
|
-
options={"verify_signature": False})
|
|
523
|
-
result = {
|
|
524
|
-
"header": header,
|
|
525
|
-
"payload": payload
|
|
526
|
-
}
|
|
500
|
+
header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
501
|
+
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
502
|
+
options={"verify_signature": False})
|
|
503
|
+
result = {
|
|
504
|
+
"header": header,
|
|
505
|
+
"payload": payload
|
|
506
|
+
}
|
|
527
507
|
except Exception as e:
|
|
528
508
|
if logger:
|
|
529
509
|
logger.error(msg=str(e))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import jwt
|
|
2
|
-
import requests
|
|
3
2
|
import string
|
|
4
3
|
import sys
|
|
5
4
|
from base64 import urlsafe_b64encode
|
|
@@ -7,12 +6,12 @@ from datetime import datetime, timezone
|
|
|
7
6
|
from logging import Logger
|
|
8
7
|
from pypomes_core import str_random
|
|
9
8
|
from pypomes_db import db_connect, db_commit, db_update, db_delete
|
|
10
|
-
from requests import Response
|
|
11
9
|
from threading import Lock
|
|
12
10
|
from typing import Any
|
|
13
11
|
|
|
14
|
-
from .
|
|
15
|
-
JWT_DEFAULT_ALGORITHM, JWT_ACCOUNT_LIMIT,
|
|
12
|
+
from . import (
|
|
13
|
+
JWT_DEFAULT_ALGORITHM, JWT_ACCOUNT_LIMIT,
|
|
14
|
+
JWT_ENCODING_KEY, JWT_DECODING_KEY,
|
|
16
15
|
JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
|
|
17
16
|
JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN
|
|
18
17
|
)
|
|
@@ -27,22 +26,20 @@ class JwtRegistry:
|
|
|
27
26
|
- access_data: dictionary holding the JWT token data, organized by account id:
|
|
28
27
|
{
|
|
29
28
|
<account-id>: {
|
|
30
|
-
"access-max-age": <int>,
|
|
31
|
-
"refresh-max-age": <int>,
|
|
32
|
-
"grace-interval": <int>,
|
|
33
|
-
"request-timeout": <int>, # timeout for the requests to the reference URL (in seconds)
|
|
34
|
-
"remote_url": <string>, # remote server's URL to obtain the JWT tokens from
|
|
29
|
+
"access-max-age": <int>, # defaults to JWT_ACCESS_MAX_AGE (in seconds)
|
|
30
|
+
"refresh-max-age": <int>, # defaults to JWT_REFRESH_MAX_AGE (in seconds)
|
|
31
|
+
"grace-interval": <int>, # time to wait for token to be valid, in seconds
|
|
35
32
|
"claims": {
|
|
36
|
-
"iss": <string>,
|
|
37
|
-
"birthdate": <string>,
|
|
38
|
-
"email": <string>,
|
|
39
|
-
"gender": <string>,
|
|
40
|
-
"name": <string>,
|
|
41
|
-
"roles": <List[str]>,
|
|
42
|
-
"nonce": <string>,
|
|
33
|
+
"iss": <string>, # token'ss issuer
|
|
34
|
+
"birthdate": <string>, # subject's birth date
|
|
35
|
+
"email": <string>, # subject's email
|
|
36
|
+
"gender": <string>, # subject's gender
|
|
37
|
+
"name": <string>, # subject's name
|
|
38
|
+
"roles": <List[str]>, # subject roles
|
|
39
|
+
"nonce": <string>, # used to associate a Client session with a token
|
|
43
40
|
# output, only
|
|
44
|
-
"valid-from": <string>,
|
|
45
|
-
"valid-until": <string>,
|
|
41
|
+
"valid-from": <string>, # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
42
|
+
"valid-until": <string>, # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
46
43
|
...
|
|
47
44
|
}
|
|
48
45
|
},
|
|
@@ -79,8 +76,8 @@ class JwtRegistry:
|
|
|
79
76
|
|
|
80
77
|
The token header has these items:
|
|
81
78
|
"alg": <string> the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
|
|
82
|
-
"typ": <string> the token type (fixed to *JWT*
|
|
83
|
-
"kid": <string> a
|
|
79
|
+
"typ": <string> the token type (fixed to *JWT*)
|
|
80
|
+
"kid": <string> a token type and key to its location in the token database
|
|
84
81
|
|
|
85
82
|
If issued by the local server, "kid" holds the key to the corresponding record in the token database,
|
|
86
83
|
if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
|
|
@@ -90,7 +87,7 @@ class JwtRegistry:
|
|
|
90
87
|
Initizalize the token access data.
|
|
91
88
|
"""
|
|
92
89
|
self.access_lock: Lock = Lock()
|
|
93
|
-
self.
|
|
90
|
+
self.access_registry: dict[str, Any] = {}
|
|
94
91
|
|
|
95
92
|
def add_account(self,
|
|
96
93
|
account_id: str,
|
|
@@ -98,35 +95,28 @@ class JwtRegistry:
|
|
|
98
95
|
access_max_age: int,
|
|
99
96
|
refresh_max_age: int,
|
|
100
97
|
grace_interval: int | None,
|
|
101
|
-
request_timeout: int | None,
|
|
102
|
-
remote_url: str | None,
|
|
103
98
|
logger: Logger = None) -> None:
|
|
104
99
|
"""
|
|
105
100
|
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
106
101
|
|
|
107
102
|
The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
|
|
108
|
-
at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
|
|
109
|
-
|
|
110
|
-
If the token provider is remote, all claims are sent to it at token request time.
|
|
103
|
+
at a minimum, *iss*, *birthdate*, *email*, *gender*, *name*, and *roles*.
|
|
104
|
+
The parameter *refresh_max_age* should be at least 300 seconds greater than *access-max-age*.
|
|
111
105
|
|
|
112
106
|
:param account_id: the account identification
|
|
113
107
|
:param claims: the JWT claimset, as key-value pairs
|
|
114
|
-
:param access_max_age: access token duration, in seconds
|
|
115
|
-
:param refresh_max_age: refresh token duration, in seconds
|
|
108
|
+
:param access_max_age: access token duration, in seconds (at least 60 seconds)
|
|
109
|
+
:param refresh_max_age: refresh token duration, in seconds (greater than *access_max_age*)
|
|
116
110
|
:param grace_interval: time to wait for token to be valid, in seconds
|
|
117
|
-
:param request_timeout: timeout for the requests to the reference URL (in seconds)
|
|
118
|
-
:param remote_url: remote server's URL to obtain the JWT tokens from
|
|
119
111
|
:param logger: optional logger
|
|
120
112
|
"""
|
|
121
113
|
# build and store the access data for the account
|
|
122
114
|
with self.access_lock:
|
|
123
|
-
if account_id not in self.
|
|
124
|
-
self.
|
|
115
|
+
if account_id not in self.access_registry:
|
|
116
|
+
self.access_registry[account_id] = {
|
|
125
117
|
"access-max-age": access_max_age,
|
|
126
118
|
"refresh-max-age": refresh_max_age,
|
|
127
119
|
"grace-interval": grace_interval,
|
|
128
|
-
"request-timeout": request_timeout,
|
|
129
|
-
"remote-url": remote_url,
|
|
130
120
|
"claims": claims or {}
|
|
131
121
|
}
|
|
132
122
|
if logger:
|
|
@@ -147,7 +137,7 @@ class JwtRegistry:
|
|
|
147
137
|
# remove from internal storage
|
|
148
138
|
account_data: dict[str, Any] | None
|
|
149
139
|
with self.access_lock:
|
|
150
|
-
account_data = self.
|
|
140
|
+
account_data = self.access_registry.pop(account_id, None)
|
|
151
141
|
|
|
152
142
|
# remove from database
|
|
153
143
|
db_delete(errors=None,
|
|
@@ -242,10 +232,10 @@ class JwtRegistry:
|
|
|
242
232
|
|
|
243
233
|
Structure of the return data:
|
|
244
234
|
{
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
235
|
+
"access-token": <jwt-token>,
|
|
236
|
+
"created-in": <timestamp>,
|
|
237
|
+
"expires-in": <seconds-to-expiration>,
|
|
238
|
+
"refresh-token": <jwt-token>
|
|
249
239
|
}
|
|
250
240
|
|
|
251
241
|
:param account_id: the account identification
|
|
@@ -254,9 +244,6 @@ class JwtRegistry:
|
|
|
254
244
|
:return: the JWT token data
|
|
255
245
|
:raises RuntimeError: invalid account id, or error accessing the token database
|
|
256
246
|
"""
|
|
257
|
-
# initialize the return variable
|
|
258
|
-
result: dict[str, Any] | None = None
|
|
259
|
-
|
|
260
247
|
# process the account data in storage
|
|
261
248
|
with (self.access_lock):
|
|
262
249
|
account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
|
|
@@ -269,81 +256,67 @@ class JwtRegistry:
|
|
|
269
256
|
current_claims["sub"] = account_id
|
|
270
257
|
errors: list[str] = []
|
|
271
258
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logger=logger)
|
|
280
|
-
if errors:
|
|
281
|
-
raise RuntimeError("; ".join(errors))
|
|
259
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
260
|
+
current_claims["iat"] = just_now
|
|
261
|
+
grace_interval = account_data.get("grace-interval")
|
|
262
|
+
if grace_interval:
|
|
263
|
+
current_claims["nbf"] = just_now + grace_interval
|
|
264
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
265
|
+
tz=timezone.utc).isoformat()
|
|
282
266
|
else:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
267
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
268
|
+
tz=timezone.utc).isoformat()
|
|
269
|
+
# issue a candidate refresh token first, and persist it
|
|
270
|
+
current_claims["exp"] = just_now + account_data.get("refresh-max-age")
|
|
271
|
+
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
272
|
+
tz=timezone.utc).isoformat()
|
|
273
|
+
# may raise an exception
|
|
274
|
+
refresh_token: str = jwt.encode(payload=current_claims,
|
|
275
|
+
key=JWT_ENCODING_KEY,
|
|
276
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
277
|
+
headers={"kid": "R0"})
|
|
278
|
+
# obtain a DB connection (may raise an exception)
|
|
279
|
+
db_conn: Any = db_connect(errors=errors,
|
|
280
|
+
logger=logger)
|
|
281
|
+
# persist the candidate token (may raise an exception)
|
|
282
|
+
token_id: int = _jwt_persist_token(errors=errors,
|
|
283
|
+
account_id=account_id,
|
|
284
|
+
jwt_token=refresh_token,
|
|
285
|
+
db_conn=db_conn,
|
|
286
|
+
logger=logger)
|
|
287
|
+
# issue the definitive refresh token
|
|
288
|
+
refresh_token = jwt.encode(payload=current_claims,
|
|
289
|
+
key=JWT_ENCODING_KEY,
|
|
290
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
291
|
+
headers={"kid": f"R{token_id}"})
|
|
292
|
+
# persist it
|
|
293
|
+
db_update(errors=errors,
|
|
294
|
+
update_stmt=f"UPDATE {JWT_DB_TABLE}",
|
|
295
|
+
update_data={JWT_DB_COL_TOKEN: refresh_token},
|
|
296
|
+
where_data={JWT_DB_COL_KID: token_id},
|
|
297
|
+
connection=db_conn,
|
|
298
|
+
logger=logger)
|
|
299
|
+
# commit the transaction
|
|
300
|
+
db_commit(errors=errors,
|
|
301
|
+
connection=db_conn,
|
|
302
|
+
logger=logger)
|
|
303
|
+
if errors:
|
|
304
|
+
raise RuntimeError("; ".join(errors))
|
|
305
|
+
|
|
306
|
+
# issue the access token
|
|
307
|
+
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
308
|
+
# may raise an exception
|
|
309
|
+
access_token: str = jwt.encode(payload=current_claims,
|
|
314
310
|
key=JWT_ENCODING_KEY,
|
|
315
311
|
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
316
|
-
headers={"kid": f"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
# commit the transaction
|
|
325
|
-
db_commit(errors=errors,
|
|
326
|
-
connection=db_conn,
|
|
327
|
-
logger=logger)
|
|
328
|
-
if errors:
|
|
329
|
-
raise RuntimeError("; ".join(errors))
|
|
330
|
-
|
|
331
|
-
# issue the access token
|
|
332
|
-
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
333
|
-
# may raise an exception
|
|
334
|
-
access_token: str = jwt.encode(payload=current_claims,
|
|
335
|
-
key=JWT_ENCODING_KEY,
|
|
336
|
-
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
337
|
-
headers={"kid": f"A{token_id}"})
|
|
338
|
-
# return the token data
|
|
339
|
-
result = {
|
|
340
|
-
"access_token": access_token,
|
|
341
|
-
"created_in": current_claims.get("iat"),
|
|
342
|
-
"expires_in": current_claims.get("exp"),
|
|
343
|
-
"refresh_token": refresh_token
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return result
|
|
312
|
+
headers={"kid": f"A{token_id}"})
|
|
313
|
+
# return the token data
|
|
314
|
+
return {
|
|
315
|
+
"access-token": access_token,
|
|
316
|
+
"created-in": current_claims.get("iat"),
|
|
317
|
+
"expires-in": current_claims.get("exp"),
|
|
318
|
+
"refresh-token": refresh_token
|
|
319
|
+
}
|
|
347
320
|
|
|
348
321
|
def __get_account_data(self,
|
|
349
322
|
account_id: str,
|
|
@@ -355,7 +328,7 @@ class JwtRegistry:
|
|
|
355
328
|
:raises RuntimeError: No JWT access data exists for *account_id*
|
|
356
329
|
"""
|
|
357
330
|
# retrieve the access data
|
|
358
|
-
result: dict[str, Any] = self.
|
|
331
|
+
result: dict[str, Any] = self.access_registry.get(account_id)
|
|
359
332
|
if not result:
|
|
360
333
|
# JWT access data not found
|
|
361
334
|
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
@@ -366,60 +339,6 @@ class JwtRegistry:
|
|
|
366
339
|
return result
|
|
367
340
|
|
|
368
341
|
|
|
369
|
-
def _jwt_request_token(errors: list[str],
|
|
370
|
-
remote_url: str,
|
|
371
|
-
claims: dict[str, Any],
|
|
372
|
-
timeout: int = None,
|
|
373
|
-
logger: Logger = None) -> dict[str, Any]:
|
|
374
|
-
"""
|
|
375
|
-
Obtain and return the JWT token from *reference_url*, along with its duration.
|
|
376
|
-
|
|
377
|
-
Expected structure of the return data:
|
|
378
|
-
{
|
|
379
|
-
"access_token": <jwt-token>,
|
|
380
|
-
"created_in": <timestamp>,
|
|
381
|
-
"expires_in": <seconds-to-expiration>,
|
|
382
|
-
"refresh_token": <token>
|
|
383
|
-
}
|
|
384
|
-
It is up to the invoker to make sure that the *claims* data conform to the requirements
|
|
385
|
-
of the provider issuing the JWT token.
|
|
386
|
-
|
|
387
|
-
:param errors: incidental errors
|
|
388
|
-
:param remote_url: the reference URL for obtaining JWT tokens
|
|
389
|
-
:param claims: the JWT claimset, as expected by the issuing server
|
|
390
|
-
:param timeout: request timeout, in seconds (defaults to *None*)
|
|
391
|
-
:param logger: optional logger
|
|
392
|
-
"""
|
|
393
|
-
# initialize the return variable
|
|
394
|
-
result: dict[str, Any] | None = None
|
|
395
|
-
|
|
396
|
-
# request the JWT token
|
|
397
|
-
if logger:
|
|
398
|
-
logger.debug(f"POST request JWT token to '{remote_url}'")
|
|
399
|
-
response: Response = requests.post(
|
|
400
|
-
url=remote_url,
|
|
401
|
-
json=claims,
|
|
402
|
-
timeout=timeout
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
# was the request successful ?
|
|
406
|
-
if response.status_code in [200, 201, 202]:
|
|
407
|
-
# yes, save the access token data returned
|
|
408
|
-
result = response.json()
|
|
409
|
-
if logger:
|
|
410
|
-
logger.debug(f"JWT token obtained: {result}")
|
|
411
|
-
else:
|
|
412
|
-
# no, report the problem
|
|
413
|
-
err_msg: str = f"POST request to '{remote_url}' failed: {response.reason}"
|
|
414
|
-
if response.text:
|
|
415
|
-
err_msg += f" - {response.text}"
|
|
416
|
-
if logger:
|
|
417
|
-
logger.error(err_msg)
|
|
418
|
-
errors.append(err_msg)
|
|
419
|
-
|
|
420
|
-
return result
|
|
421
|
-
|
|
422
|
-
|
|
423
342
|
def _jwt_persist_token(errors: list[str],
|
|
424
343
|
account_id: str,
|
|
425
344
|
jwt_token: str,
|
|
@@ -468,7 +387,6 @@ def _jwt_persist_token(errors: list[str],
|
|
|
468
387
|
token_kid: int = rec[0]
|
|
469
388
|
token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
|
|
470
389
|
token=token,
|
|
471
|
-
validate=False,
|
|
472
390
|
logger=logger)
|
|
473
391
|
if errors:
|
|
474
392
|
raise RuntimeError("; ".join(errors))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|