pypomes-jwt 0.9.5__py3-none-any.whl → 0.9.7__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/__init__.py +2 -2
- pypomes_jwt/jwt_pomes.py +42 -61
- pypomes_jwt/jwt_registry.py +107 -193
- {pypomes_jwt-0.9.5.dist-info → pypomes_jwt-0.9.7.dist-info}/METADATA +1 -1
- pypomes_jwt-0.9.7.dist-info/RECORD +8 -0
- pypomes_jwt-0.9.5.dist-info/RECORD +0 -8
- {pypomes_jwt-0.9.5.dist-info → pypomes_jwt-0.9.7.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.9.5.dist-info → pypomes_jwt-0.9.7.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/__init__.py
CHANGED
|
@@ -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",
|
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -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,51 +85,39 @@ 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,
|
|
92
|
-
reference_url: str,
|
|
93
92
|
claims: dict[str, Any],
|
|
94
93
|
access_max_age: int = JWT_ACCESS_MAX_AGE,
|
|
95
94
|
refresh_max_age: int = JWT_REFRESH_MAX_AGE,
|
|
96
95
|
grace_interval: int = None,
|
|
97
|
-
request_timeout: int = None,
|
|
98
|
-
remote_provider: bool = None,
|
|
99
96
|
logger: Logger = None) -> None:
|
|
100
97
|
"""
|
|
101
|
-
|
|
98
|
+
Establish the data needed to obtain JWT tokens for *account_id*.
|
|
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*.
|
|
102
104
|
|
|
103
105
|
:param account_id: the account identification
|
|
104
|
-
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
105
106
|
:param claims: the JWT claimset, as key-value pairs
|
|
106
107
|
:param access_max_age: access token duration, in seconds
|
|
107
108
|
:param refresh_max_age: refresh token duration, in seconds
|
|
108
109
|
:param grace_interval: optional time to wait for token to be valid, in seconds
|
|
109
|
-
:param request_timeout: timeout for the requests to the reference URL
|
|
110
|
-
:param remote_provider: whether the JWT provider is a remote server
|
|
111
110
|
:param logger: optional logger
|
|
112
111
|
"""
|
|
113
112
|
if logger:
|
|
114
|
-
logger.debug(msg=f"
|
|
115
|
-
|
|
116
|
-
# extract the claims provided in the reference URL's query string
|
|
117
|
-
pos: int = reference_url.find("?")
|
|
118
|
-
if pos > 0:
|
|
119
|
-
params: list[str] = reference_url[pos+1:].split(sep="&")
|
|
120
|
-
for param in params:
|
|
121
|
-
claims[param.split("=")[0]] = param.split("=")[1]
|
|
122
|
-
reference_url = reference_url[:pos]
|
|
113
|
+
logger.debug(msg=f"Registering account data for '{account_id}'")
|
|
123
114
|
|
|
124
115
|
# register the JWT service
|
|
125
116
|
__jwt_registry.add_account(account_id=account_id,
|
|
126
|
-
reference_url=reference_url,
|
|
127
117
|
claims=claims,
|
|
128
118
|
access_max_age=access_max_age,
|
|
129
|
-
refresh_max_age=refresh_max_age,
|
|
119
|
+
refresh_max_age=max(refresh_max_age, access_max_age + 300),
|
|
130
120
|
grace_interval=grace_interval,
|
|
131
|
-
request_timeout=request_timeout,
|
|
132
|
-
remote_provider=remote_provider,
|
|
133
121
|
logger=logger)
|
|
134
122
|
|
|
135
123
|
|
|
@@ -157,10 +145,12 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
157
145
|
"""
|
|
158
146
|
Verify if *token* ia a valid JWT token.
|
|
159
147
|
|
|
160
|
-
Raise an appropriate exception if validation failed.
|
|
161
|
-
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.
|
|
162
150
|
A token issued locally has the header claim *kid* starting with *A* (for *Access*) or *R* (for *Refresh*),
|
|
163
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.
|
|
164
154
|
|
|
165
155
|
:param errors: incidental error messages
|
|
166
156
|
:param token: the token to be validated
|
|
@@ -249,7 +239,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
249
239
|
|
|
250
240
|
def jwt_revoke_token(errors: list[str] | None,
|
|
251
241
|
account_id: str,
|
|
252
|
-
|
|
242
|
+
token: str,
|
|
253
243
|
logger: Logger = None) -> bool:
|
|
254
244
|
"""
|
|
255
245
|
Revoke the *refresh_token* associated with *account_id*.
|
|
@@ -258,7 +248,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
258
248
|
|
|
259
249
|
:param errors: incidental error messages
|
|
260
250
|
:param account_id: the account identification
|
|
261
|
-
:param
|
|
251
|
+
:param token: the token to be revoked
|
|
262
252
|
:param logger: optional logger
|
|
263
253
|
:return: *True* if operation could be performed, *False* otherwise
|
|
264
254
|
"""
|
|
@@ -270,7 +260,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
270
260
|
|
|
271
261
|
op_errors: list[str] = []
|
|
272
262
|
token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
|
|
273
|
-
token=
|
|
263
|
+
token=token,
|
|
274
264
|
account_id=account_id,
|
|
275
265
|
logger=logger)
|
|
276
266
|
if not op_errors:
|
|
@@ -429,24 +419,26 @@ def jwt_refresh_tokens(errors: list[str] | None,
|
|
|
429
419
|
logger.debug(msg=f"Refreshing a JWT token pair for '{account_id}'")
|
|
430
420
|
op_errors: list[str] = []
|
|
431
421
|
|
|
432
|
-
#
|
|
422
|
+
# assert the refresh token
|
|
433
423
|
if refresh_token:
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
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,
|
|
441
432
|
account_id=account_id,
|
|
442
|
-
|
|
433
|
+
token=refresh_token,
|
|
443
434
|
logger=logger):
|
|
444
435
|
# issue tokens
|
|
445
|
-
result = jwt_issue_tokens(errors=
|
|
436
|
+
result = jwt_issue_tokens(errors=op_errors,
|
|
446
437
|
account_id=account_id,
|
|
447
438
|
account_claims=account_claims,
|
|
448
439
|
logger=logger)
|
|
449
440
|
else:
|
|
441
|
+
# refresh token not found
|
|
450
442
|
op_errors.append("Refresh token was not provided")
|
|
451
443
|
|
|
452
444
|
if op_errors:
|
|
@@ -460,17 +452,13 @@ def jwt_refresh_tokens(errors: list[str] | None,
|
|
|
460
452
|
|
|
461
453
|
def jwt_get_claims(errors: list[str] | None,
|
|
462
454
|
token: str,
|
|
463
|
-
validate: bool = False,
|
|
464
455
|
logger: Logger = None) -> dict[str, Any] | None:
|
|
465
456
|
"""
|
|
466
|
-
|
|
457
|
+
Retrieve and return the claims set of a JWT *token*.
|
|
467
458
|
|
|
468
|
-
|
|
469
|
-
- the token was issued and signed by the local provider, and is not corrupted
|
|
470
|
-
- the claim 'exp' is present and is in the future
|
|
471
|
-
- 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.
|
|
472
460
|
|
|
473
|
-
Structure of the returned data:
|
|
461
|
+
Structure of the returned data, for locally issued tokens:
|
|
474
462
|
{
|
|
475
463
|
"header": {
|
|
476
464
|
"alg": "RS256",
|
|
@@ -484,7 +472,7 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
484
472
|
"email": "jdoe@mail.com",
|
|
485
473
|
"exp": 1516640454,
|
|
486
474
|
"iat": 1516239022,
|
|
487
|
-
"iss": "
|
|
475
|
+
"iss": "my_jwt_provider.com",
|
|
488
476
|
"jti": "Uhsdfgr67FGH567qwSDF33er89retert",
|
|
489
477
|
"gender": "M",
|
|
490
478
|
"name": "John Doe",
|
|
@@ -499,7 +487,6 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
499
487
|
|
|
500
488
|
:param errors: incidental error messages
|
|
501
489
|
:param token: the token to be inspected for claims
|
|
502
|
-
:param validate: If *True*, verifies the token's data (defaults to *False*)
|
|
503
490
|
:param logger: optional logger
|
|
504
491
|
:return: the token's claimset, or *None* if error
|
|
505
492
|
"""
|
|
@@ -510,19 +497,13 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
510
497
|
logger.debug(msg="Retrieve claims for token")
|
|
511
498
|
|
|
512
499
|
try:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
521
|
-
options={"verify_signature": False})
|
|
522
|
-
result = {
|
|
523
|
-
"header": header,
|
|
524
|
-
"payload": payload
|
|
525
|
-
}
|
|
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
|
+
}
|
|
526
507
|
except Exception as e:
|
|
527
508
|
if logger:
|
|
528
509
|
logger.error(msg=str(e))
|
pypomes_jwt/jwt_registry.py
CHANGED
|
@@ -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,23 +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
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
|
|
34
|
-
"refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
|
|
35
|
-
"grace-interval": <int> # time to wait for token to be valid, in seconds
|
|
36
|
-
"request-timeout": <int> # timeout for the requests to the reference URL (in seconds)
|
|
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
|
|
37
32
|
"claims": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
#
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
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
|
|
40
|
+
# output, only
|
|
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>)
|
|
47
43
|
...
|
|
48
44
|
}
|
|
49
45
|
},
|
|
@@ -58,30 +54,30 @@ class JwtRegistry:
|
|
|
58
54
|
|
|
59
55
|
Token-related claims are mostly required claims, and convey information about the token itself:
|
|
60
56
|
# required
|
|
61
|
-
"exp": <timestamp>
|
|
62
|
-
"iat": <timestamp>
|
|
63
|
-
"iss": <string>
|
|
64
|
-
"jti": <string>
|
|
65
|
-
"sub": <string>
|
|
57
|
+
"exp": <timestamp> expiration time
|
|
58
|
+
"iat": <timestamp> issued at
|
|
59
|
+
"iss": <string> token's issuer
|
|
60
|
+
"jti": <string> JWT id
|
|
61
|
+
"sub": <string> subject (the account identification)
|
|
66
62
|
# optional
|
|
67
|
-
"aud": <string>
|
|
68
|
-
"nbt": <timestamp>
|
|
63
|
+
"aud": <string> token audience
|
|
64
|
+
"nbt": <timestamp> not before time
|
|
69
65
|
|
|
70
66
|
Account-related claims are optional claims, and convey information about the registered account they belong to.
|
|
71
67
|
Alhough they can be freely specified, these are some of the most commonly used claims:
|
|
72
|
-
"valid-from": <string>
|
|
73
|
-
"valid-until": <string>
|
|
74
|
-
"birthdate": <string>
|
|
75
|
-
"email": <string>
|
|
76
|
-
"gender": <string>
|
|
77
|
-
"name": <string>
|
|
78
|
-
"roles": <List[str]>
|
|
79
|
-
"nonce": <string>
|
|
68
|
+
"valid-from": <string> token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
69
|
+
"valid-until": <string> token's finish (<YYYY-MM-DDThh:mm:ss+00.00>)
|
|
70
|
+
"birthdate": <string> subject's birth date
|
|
71
|
+
"email": <string> subject's email
|
|
72
|
+
"gender": <string> subject's gender
|
|
73
|
+
"name": <string> subject's name
|
|
74
|
+
"roles": <List[str]> subject roles
|
|
75
|
+
"nonce": <string> used to associate a client session with a token
|
|
80
76
|
|
|
81
77
|
The token header has these items:
|
|
82
|
-
"alg": <string>
|
|
83
|
-
"typ": <string>
|
|
84
|
-
"kid": <string>
|
|
78
|
+
"alg": <string> the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
|
|
79
|
+
"typ": <string> the token type (fixed to *JWT*)
|
|
80
|
+
"kid": <string> a token type and key to its location in the token database
|
|
85
81
|
|
|
86
82
|
If issued by the local server, "kid" holds the key to the corresponding record in the token database,
|
|
87
83
|
if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
|
|
@@ -91,46 +87,36 @@ class JwtRegistry:
|
|
|
91
87
|
Initizalize the token access data.
|
|
92
88
|
"""
|
|
93
89
|
self.access_lock: Lock = Lock()
|
|
94
|
-
self.
|
|
90
|
+
self.access_registry: dict[str, Any] = {}
|
|
95
91
|
|
|
96
92
|
def add_account(self,
|
|
97
93
|
account_id: str,
|
|
98
|
-
reference_url: str,
|
|
99
94
|
claims: dict[str, Any],
|
|
100
95
|
access_max_age: int,
|
|
101
96
|
refresh_max_age: int,
|
|
102
97
|
grace_interval: int | None,
|
|
103
|
-
request_timeout: int | None,
|
|
104
|
-
remote_provider: bool | None,
|
|
105
98
|
logger: Logger = None) -> None:
|
|
106
99
|
"""
|
|
107
100
|
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
108
101
|
|
|
109
102
|
The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
|
|
110
|
-
at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
|
|
111
|
-
|
|
112
|
-
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*.
|
|
113
105
|
|
|
114
106
|
:param account_id: the account identification
|
|
115
|
-
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
116
107
|
:param claims: the JWT claimset, as key-value pairs
|
|
117
|
-
:param access_max_age: access token duration, in seconds
|
|
118
|
-
: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*)
|
|
119
110
|
:param grace_interval: time to wait for token to be valid, in seconds
|
|
120
|
-
:param request_timeout: timeout for the requests to the reference URL (in seconds)
|
|
121
|
-
:param remote_provider: whether the JWT provider is a remote server
|
|
122
111
|
:param logger: optional logger
|
|
123
112
|
"""
|
|
124
113
|
# build and store the access data for the account
|
|
125
114
|
with self.access_lock:
|
|
126
|
-
if account_id not in self.
|
|
127
|
-
self.
|
|
128
|
-
"reference-url": reference_url,
|
|
115
|
+
if account_id not in self.access_registry:
|
|
116
|
+
self.access_registry[account_id] = {
|
|
129
117
|
"access-max-age": access_max_age,
|
|
130
118
|
"refresh-max-age": refresh_max_age,
|
|
131
119
|
"grace-interval": grace_interval,
|
|
132
|
-
"request-timeout": request_timeout,
|
|
133
|
-
"remote-provider": remote_provider,
|
|
134
120
|
"claims": claims or {}
|
|
135
121
|
}
|
|
136
122
|
if logger:
|
|
@@ -151,7 +137,7 @@ class JwtRegistry:
|
|
|
151
137
|
# remove from internal storage
|
|
152
138
|
account_data: dict[str, Any] | None
|
|
153
139
|
with self.access_lock:
|
|
154
|
-
account_data = self.
|
|
140
|
+
account_data = self.access_registry.pop(account_id, None)
|
|
155
141
|
|
|
156
142
|
# remove from database
|
|
157
143
|
db_delete(errors=None,
|
|
@@ -195,8 +181,6 @@ class JwtRegistry:
|
|
|
195
181
|
if not isinstance(nature, str) or \
|
|
196
182
|
len(nature) != 1 or nature < "A" or nature > "Z":
|
|
197
183
|
err_msg: str = f"Invalid nature '{nature}'"
|
|
198
|
-
elif not isinstance(claims, dict) or "iss" not in claims:
|
|
199
|
-
err_msg = f"invalid claims '{claims}'"
|
|
200
184
|
elif not isinstance(duration, int) or duration < 60:
|
|
201
185
|
err_msg = f"Invalid duration '{duration}'"
|
|
202
186
|
if err_msg:
|
|
@@ -209,11 +193,14 @@ class JwtRegistry:
|
|
|
209
193
|
logger=logger)
|
|
210
194
|
# issue the token
|
|
211
195
|
current_claims: dict[str, Any] = {}
|
|
196
|
+
iss: str = account_data["claims"].get("iss")
|
|
197
|
+
if iss:
|
|
198
|
+
current_claims["iss"] = iss
|
|
212
199
|
if claims:
|
|
213
200
|
current_claims.update(claims)
|
|
201
|
+
|
|
214
202
|
current_claims["jti"] = str_random(size=32,
|
|
215
203
|
chars=string.ascii_letters + string.digits)
|
|
216
|
-
current_claims["iss"] = account_data.get("reference-url")
|
|
217
204
|
current_claims["sub"] = account_id
|
|
218
205
|
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
219
206
|
current_claims["iat"] = just_now
|
|
@@ -240,7 +227,7 @@ class JwtRegistry:
|
|
|
240
227
|
"""
|
|
241
228
|
Issue and return a JWT token pair associated with *account_id*.
|
|
242
229
|
|
|
243
|
-
These claims are ignored, if specified in *account_claims*: *iat*, *
|
|
230
|
+
These claims are ignored, if specified in *account_claims*: *iat*, *exp*, *jti*, *nbf*, and *sub*.
|
|
244
231
|
Other claims specified therein may supercede registered account-related claims.
|
|
245
232
|
|
|
246
233
|
Structure of the return data:
|
|
@@ -257,97 +244,79 @@ class JwtRegistry:
|
|
|
257
244
|
:return: the JWT token data
|
|
258
245
|
:raises RuntimeError: invalid account id, or error accessing the token database
|
|
259
246
|
"""
|
|
260
|
-
# initialize the return variable
|
|
261
|
-
result: dict[str, Any] | None = None
|
|
262
|
-
|
|
263
247
|
# process the account data in storage
|
|
264
248
|
with (self.access_lock):
|
|
265
249
|
account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
|
|
266
250
|
logger=logger)
|
|
267
|
-
current_claims: dict[str, Any] = account_data
|
|
251
|
+
current_claims: dict[str, Any] = account_data["claims"].copy()
|
|
268
252
|
if account_claims:
|
|
269
253
|
current_claims.update(account_claims)
|
|
270
254
|
current_claims["jti"] = str_random(size=32,
|
|
271
255
|
chars=string.ascii_letters + string.digits)
|
|
272
256
|
current_claims["sub"] = account_id
|
|
273
|
-
current_claims["iss"] = account_data.get("reference-url")
|
|
274
257
|
errors: list[str] = []
|
|
275
258
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
logger=logger)
|
|
284
|
-
if errors:
|
|
285
|
-
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()
|
|
286
266
|
else:
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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,
|
|
318
310
|
key=JWT_ENCODING_KEY,
|
|
319
311
|
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
320
|
-
headers={"kid": f"
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
# commit the transaction
|
|
329
|
-
db_commit(errors=errors,
|
|
330
|
-
connection=db_conn,
|
|
331
|
-
logger=logger)
|
|
332
|
-
if errors:
|
|
333
|
-
raise RuntimeError("; ".join(errors))
|
|
334
|
-
|
|
335
|
-
# issue the access token
|
|
336
|
-
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
337
|
-
# may raise an exception
|
|
338
|
-
access_token: str = jwt.encode(payload=current_claims,
|
|
339
|
-
key=JWT_ENCODING_KEY,
|
|
340
|
-
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
341
|
-
headers={"kid": f"A{token_id}"})
|
|
342
|
-
# return the token data
|
|
343
|
-
result = {
|
|
344
|
-
"access_token": access_token,
|
|
345
|
-
"created_in": current_claims.get("iat"),
|
|
346
|
-
"expires_in": current_claims.get("exp"),
|
|
347
|
-
"refresh_token": refresh_token
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
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
|
+
}
|
|
351
320
|
|
|
352
321
|
def __get_account_data(self,
|
|
353
322
|
account_id: str,
|
|
@@ -359,7 +328,7 @@ class JwtRegistry:
|
|
|
359
328
|
:raises RuntimeError: No JWT access data exists for *account_id*
|
|
360
329
|
"""
|
|
361
330
|
# retrieve the access data
|
|
362
|
-
result: dict[str, Any] = self.
|
|
331
|
+
result: dict[str, Any] = self.access_registry.get(account_id)
|
|
363
332
|
if not result:
|
|
364
333
|
# JWT access data not found
|
|
365
334
|
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
@@ -370,60 +339,6 @@ class JwtRegistry:
|
|
|
370
339
|
return result
|
|
371
340
|
|
|
372
341
|
|
|
373
|
-
def _jwt_request_token(errors: list[str],
|
|
374
|
-
reference_url: str,
|
|
375
|
-
claims: dict[str, Any],
|
|
376
|
-
timeout: int = None,
|
|
377
|
-
logger: Logger = None) -> dict[str, Any]:
|
|
378
|
-
"""
|
|
379
|
-
Obtain and return the JWT token from *reference_url*, along with its duration.
|
|
380
|
-
|
|
381
|
-
Expected structure of the return data:
|
|
382
|
-
{
|
|
383
|
-
"access_token": <jwt-token>,
|
|
384
|
-
"created_in": <timestamp>,
|
|
385
|
-
"expires_in": <seconds-to-expiration>,
|
|
386
|
-
"refresh_token": <token>
|
|
387
|
-
}
|
|
388
|
-
It is up to the invoker to make sure that the *claims* data conform to the requirements
|
|
389
|
-
of the provider issuing the JWT token.
|
|
390
|
-
|
|
391
|
-
:param errors: incidental errors
|
|
392
|
-
:param reference_url: the reference URL for obtaining JWT tokens
|
|
393
|
-
:param claims: the JWT claimset, as expected by the issuing server
|
|
394
|
-
:param timeout: request timeout, in seconds (defaults to *None*)
|
|
395
|
-
:param logger: optional logger
|
|
396
|
-
"""
|
|
397
|
-
# initialize the return variable
|
|
398
|
-
result: dict[str, Any] | None = None
|
|
399
|
-
|
|
400
|
-
# request the JWT token
|
|
401
|
-
if logger:
|
|
402
|
-
logger.debug(f"POST request JWT token to '{reference_url}'")
|
|
403
|
-
response: Response = requests.post(
|
|
404
|
-
url=reference_url,
|
|
405
|
-
json=claims,
|
|
406
|
-
timeout=timeout
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# was the request successful ?
|
|
410
|
-
if response.status_code in [200, 201, 202]:
|
|
411
|
-
# yes, save the access token data returned
|
|
412
|
-
result = response.json()
|
|
413
|
-
if logger:
|
|
414
|
-
logger.debug(f"JWT token obtained: {result}")
|
|
415
|
-
else:
|
|
416
|
-
# no, report the problem
|
|
417
|
-
err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
|
|
418
|
-
if response.text:
|
|
419
|
-
err_msg += f" - {response.text}"
|
|
420
|
-
if logger:
|
|
421
|
-
logger.error(err_msg)
|
|
422
|
-
errors.append(err_msg)
|
|
423
|
-
|
|
424
|
-
return result
|
|
425
|
-
|
|
426
|
-
|
|
427
342
|
def _jwt_persist_token(errors: list[str],
|
|
428
343
|
account_id: str,
|
|
429
344
|
jwt_token: str,
|
|
@@ -472,7 +387,6 @@ def _jwt_persist_token(errors: list[str],
|
|
|
472
387
|
token_kid: int = rec[0]
|
|
473
388
|
token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
|
|
474
389
|
token=token,
|
|
475
|
-
validate=False,
|
|
476
390
|
logger=logger)
|
|
477
391
|
if errors:
|
|
478
392
|
raise RuntimeError("; ".join(errors))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.7
|
|
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=fLr_M8yXlcmSTNPMdJOJQlMmtaiK5YKh0vKjOp3z2E4,1446
|
|
2
|
+
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
+
pypomes_jwt/jwt_pomes.py,sha256=4-8qrfXt4y-auigVKPC_H6tTWBUZTjIPQM6yASnAgmc,20064
|
|
4
|
+
pypomes_jwt/jwt_registry.py,sha256=UgJ8eEE8odXUE57yXeozoHJjBDmNVZQCs4qee5IARJ8,21425
|
|
5
|
+
pypomes_jwt-0.9.7.dist-info/METADATA,sha256=SXGugukd0a4ghPNchPAL563zbxau5dggEjalvq7G0Wc,632
|
|
6
|
+
pypomes_jwt-0.9.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pypomes_jwt-0.9.7.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
+
pypomes_jwt-0.9.7.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=t6TzpvttDuLMaKSGuBicOf9cZU4Y0N9mtby3ThS4lt8,1398
|
|
2
|
-
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
-
pypomes_jwt/jwt_pomes.py,sha256=ZQ-x9nJqRqSfLXcoN0crh4a-BhT1MNOMvZkFTsaQsuE,21069
|
|
4
|
-
pypomes_jwt/jwt_registry.py,sha256=27Z0wbDCNcy_Klm50dGhJ1ZVYznj0SNdMjzHVT_Uzzo,25588
|
|
5
|
-
pypomes_jwt-0.9.5.dist-info/METADATA,sha256=IZT48rR9ftHECxA8Xy0HhhkHLX1rUQk-rDsdtMgb8TI,632
|
|
6
|
-
pypomes_jwt-0.9.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
pypomes_jwt-0.9.5.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
-
pypomes_jwt-0.9.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|