pypomes-jwt 0.9.0__tar.gz → 0.9.2__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.0 → pypomes_jwt-0.9.2}/PKG-INFO +1 -1
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/pyproject.toml +1 -1
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/src/pypomes_jwt/__init__.py +7 -5
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/src/pypomes_jwt/jwt_pomes.py +101 -42
- pypomes_jwt-0.9.0/src/pypomes_jwt/jwt_data.py → pypomes_jwt-0.9.2/src/pypomes_jwt/jwt_registry.py +190 -126
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/.gitignore +0 -0
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/LICENSE +0 -0
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/README.md +0 -0
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/src/__init__.py +0 -0
- {pypomes_jwt-0.9.0 → pypomes_jwt-0.9.2}/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.2
|
|
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
|
|
@@ -3,13 +3,14 @@ from .jwt_constants import (
|
|
|
3
3
|
JWT_DB_PORT, JWT_DB_USER, JWT_DB_PWD,
|
|
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
|
-
|
|
7
|
-
|
|
6
|
+
JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
|
|
7
|
+
JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
|
|
8
8
|
)
|
|
9
9
|
from .jwt_pomes import (
|
|
10
10
|
jwt_needed, jwt_verify_request,
|
|
11
11
|
jwt_assert_account, jwt_set_account, jwt_remove_account,
|
|
12
|
-
|
|
12
|
+
jwt_issue_token, jwt_issue_tokens, jwt_get_claims,
|
|
13
|
+
jwt_validate_token, jwt_revoke_token
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
@@ -18,12 +19,13 @@ __all__ = [
|
|
|
18
19
|
"JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
|
|
19
20
|
"JWT_DB_TABLE", "JWT_DB_COL_KID", "JWT_DB_COL_ACCOUNT",
|
|
20
21
|
"JWT_DB_COL_ALGORITHM", "JWT_DB_COL_DECODER", "JWT_DB_COL_TOKEN",
|
|
22
|
+
"JWT_ACCOUNT_LIMIT", "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
|
|
21
23
|
"JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
|
|
22
|
-
"JWT_ENCODING_KEY", "JWT_DECODING_KEY",
|
|
23
24
|
# jwt_pomes
|
|
24
25
|
"jwt_needed", "jwt_verify_request",
|
|
25
26
|
"jwt_assert_account", "jwt_set_account", "jwt_remove_account",
|
|
26
|
-
"
|
|
27
|
+
"jwt_issue_token", "jwt_issue_tokens", "jwt_get_claims",
|
|
28
|
+
"jwt_validate_token", "jwt_revoke_token"
|
|
27
29
|
]
|
|
28
30
|
|
|
29
31
|
from importlib.metadata import version
|
|
@@ -11,10 +11,10 @@ from .jwt_constants import (
|
|
|
11
11
|
JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
|
|
12
12
|
JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
|
|
13
13
|
)
|
|
14
|
-
from .
|
|
14
|
+
from .jwt_registry import JwtRegistry
|
|
15
15
|
|
|
16
16
|
# the JWT data object
|
|
17
|
-
|
|
17
|
+
__jwt_registry: JwtRegistry = JwtRegistry()
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def jwt_needed(func: callable) -> callable:
|
|
@@ -58,10 +58,10 @@ def jwt_verify_request(request: Request,
|
|
|
58
58
|
# yes, extract and validate the JWT access token
|
|
59
59
|
token: str = auth_header.split(" ")[1]
|
|
60
60
|
if logger:
|
|
61
|
-
logger.debug(msg="
|
|
61
|
+
logger.debug(msg="Bearer token was retrieved")
|
|
62
62
|
errors: list[str] = []
|
|
63
63
|
jwt_validate_token(errors=errors,
|
|
64
|
-
|
|
64
|
+
natures=["A"],
|
|
65
65
|
token=token)
|
|
66
66
|
if errors:
|
|
67
67
|
err_msg = "; ".join(errors)
|
|
@@ -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
|
|
88
|
+
return __jwt_registry.access_data.get(account_id) is not None
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def jwt_set_account(account_id: str,
|
|
@@ -126,17 +126,17 @@ def jwt_set_account(account_id: str,
|
|
|
126
126
|
reference_url = reference_url[:pos]
|
|
127
127
|
|
|
128
128
|
# register the JWT service
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
__jwt_registry.add_account(account_id=account_id,
|
|
130
|
+
reference_url=reference_url,
|
|
131
|
+
claims=claims,
|
|
132
|
+
access_max_age=access_max_age,
|
|
133
|
+
refresh_max_age=refresh_max_age,
|
|
134
|
+
grace_interval=grace_interval,
|
|
135
|
+
token_audience=token_audience,
|
|
136
|
+
token_nonce=token_nonce,
|
|
137
|
+
request_timeout=request_timeout,
|
|
138
|
+
remote_provider=remote_provider,
|
|
139
|
+
logger=logger)
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
def jwt_remove_account(account_id: str,
|
|
@@ -151,26 +151,26 @@ def jwt_remove_account(account_id: str,
|
|
|
151
151
|
if logger:
|
|
152
152
|
logger.debug(msg=f"Remove access data for '{account_id}'")
|
|
153
153
|
|
|
154
|
-
return
|
|
155
|
-
|
|
154
|
+
return __jwt_registry.remove_account(account_id=account_id,
|
|
155
|
+
logger=logger)
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
def jwt_validate_token(errors: list[str] | None,
|
|
159
159
|
token: str,
|
|
160
|
-
|
|
160
|
+
natures: 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,
|
|
168
|
-
A token issued locally has the header claim *kid* starting with
|
|
169
|
-
followed by its id in the token database
|
|
167
|
+
if *nature* is provided, verify 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, or as a single letter in the range *[B-Z]*, less *R*.
|
|
170
170
|
|
|
171
171
|
:param errors: incidental error messages
|
|
172
172
|
:param token: the token to be validated
|
|
173
|
-
:param
|
|
173
|
+
:param natures: one or more prefixes identifying the nature of locally issued tokens
|
|
174
174
|
:param account_id: optionally, validate the token's account owner
|
|
175
175
|
:param logger: optional logger
|
|
176
176
|
:return: The token's claims (header and payload) if if is valid, *None* otherwise
|
|
@@ -188,9 +188,10 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
188
188
|
op_errors: list[str] = []
|
|
189
189
|
|
|
190
190
|
# retrieve token data from database
|
|
191
|
-
if
|
|
191
|
+
if natures and not (token_kid and token_kid[0:1] in natures):
|
|
192
192
|
op_errors.append("Invalid token")
|
|
193
|
-
elif token_kid:
|
|
193
|
+
elif token_kid and len(token_kid) > 1 and token_kid[0:1].isupper() and token[1:].isdigit():
|
|
194
|
+
# token was likely issued locally
|
|
194
195
|
where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
|
|
195
196
|
if account_id:
|
|
196
197
|
where_data[JWT_DB_COL_ACCOUNT] = account_id
|
|
@@ -275,7 +276,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
275
276
|
op_errors: list[str] = []
|
|
276
277
|
token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
|
|
277
278
|
token=refresh_token,
|
|
278
|
-
|
|
279
|
+
natures=["A", "R"],
|
|
279
280
|
account_id=account_id,
|
|
280
281
|
logger=logger)
|
|
281
282
|
if not op_errors:
|
|
@@ -298,16 +299,71 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
298
299
|
return result
|
|
299
300
|
|
|
300
301
|
|
|
301
|
-
def
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
def jwt_issue_token(errors: list[str] | None,
|
|
303
|
+
account_id: str,
|
|
304
|
+
nature: str,
|
|
305
|
+
duration: int,
|
|
306
|
+
grace_interval: int = None,
|
|
307
|
+
claims: dict[str, Any] = None,
|
|
308
|
+
logger: Logger = None) -> str:
|
|
309
|
+
"""
|
|
310
|
+
Issue or refresh, and return, a JWT token associated with *account_id*, of the specified *nature*.
|
|
311
|
+
|
|
312
|
+
The parameter *nature* must be a single letter in the range *[B-Z]*, less *R*
|
|
313
|
+
(*A* is reserved for *access* tokens, and *R* for *refresh* tokens).
|
|
314
|
+
The parameter *duration* specifies the token's validity interval (at least 60 seconds).
|
|
315
|
+
These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
|
|
316
|
+
|
|
317
|
+
:param errors: incidental error messages
|
|
318
|
+
:param account_id: the account identification
|
|
319
|
+
:param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
|
|
320
|
+
:param duration: the number of seconds for the token to remain valid (at least 60 seconds)
|
|
321
|
+
:param claims: optional token's claims
|
|
322
|
+
:param grace_interval: optional interval for the token to become active (in seconds)
|
|
323
|
+
:param logger: optional logger
|
|
324
|
+
:return: the JWT token data, or *None* if error
|
|
325
|
+
"""
|
|
326
|
+
# inicialize the return variable
|
|
327
|
+
result: str | None = None
|
|
328
|
+
|
|
329
|
+
if logger:
|
|
330
|
+
logger.debug(msg=f"Issue a JWT token for '{account_id}'")
|
|
331
|
+
op_errors: list[str] = []
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
result = __jwt_registry.issue_token(account_id=account_id,
|
|
335
|
+
nature=nature,
|
|
336
|
+
duration=duration,
|
|
337
|
+
claims=claims,
|
|
338
|
+
grace_interval=grace_interval,
|
|
339
|
+
logger=logger)
|
|
340
|
+
if logger:
|
|
341
|
+
logger.debug(msg=f"Token is '{result}'")
|
|
342
|
+
except Exception as e:
|
|
343
|
+
# token issuing failed
|
|
344
|
+
op_errors.append(str(e))
|
|
345
|
+
|
|
346
|
+
if op_errors:
|
|
347
|
+
if logger:
|
|
348
|
+
logger.error("; ".join(op_errors))
|
|
349
|
+
if isinstance(errors, list):
|
|
350
|
+
errors.extend(op_errors)
|
|
351
|
+
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def jwt_issue_tokens(errors: list[str] | None,
|
|
356
|
+
account_id: str,
|
|
357
|
+
account_claims: dict[str, Any] = None,
|
|
358
|
+
refresh_token: str = None,
|
|
359
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
306
360
|
"""
|
|
307
|
-
Issue
|
|
361
|
+
Issue the JWT tokens associated with *account_id*, for access and refresh operations.
|
|
308
362
|
|
|
309
|
-
If *refresh_token* is provided, its claims are used on issuing the new tokens,
|
|
310
|
-
|
|
363
|
+
If *refresh_token* is provided, its claims are used on issuing the new tokens, and
|
|
364
|
+
claims in *account_claims*, if any, are ignored. Furthermore, these claims are ignored,
|
|
365
|
+
if provided in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
|
|
366
|
+
Other claims specified therein may supercede registered account-related claims.
|
|
311
367
|
|
|
312
368
|
Structure of the return data:
|
|
313
369
|
{
|
|
@@ -328,26 +384,29 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
328
384
|
result: dict[str, Any] | None = None
|
|
329
385
|
|
|
330
386
|
if logger:
|
|
331
|
-
logger.debug(msg=f"
|
|
387
|
+
logger.debug(msg=f"Return JWT token data for '{account_id}'")
|
|
332
388
|
op_errors: list[str] = []
|
|
389
|
+
|
|
390
|
+
# verify whether this refresh token is legitimate
|
|
333
391
|
if refresh_token:
|
|
334
|
-
# verify whether this refresh token is legitimate
|
|
335
392
|
account_claims = (jwt_validate_token(errors=op_errors,
|
|
336
393
|
token=refresh_token,
|
|
337
|
-
|
|
394
|
+
natures=["R"],
|
|
338
395
|
account_id=account_id,
|
|
339
396
|
logger=logger) or {}).get("payload")
|
|
340
397
|
if account_claims:
|
|
398
|
+
account_claims.pop("exp", None)
|
|
341
399
|
account_claims.pop("iat", None)
|
|
342
|
-
account_claims.pop("jti", None)
|
|
343
400
|
account_claims.pop("iss", None)
|
|
344
|
-
account_claims.pop("
|
|
401
|
+
account_claims.pop("jti", None)
|
|
345
402
|
account_claims.pop("nbt", None)
|
|
403
|
+
account_claims.pop("sub", None)
|
|
404
|
+
|
|
346
405
|
if not op_errors:
|
|
347
406
|
try:
|
|
348
|
-
result =
|
|
349
|
-
|
|
350
|
-
|
|
407
|
+
result = __jwt_registry.issue_tokens(account_id=account_id,
|
|
408
|
+
account_claims=account_claims,
|
|
409
|
+
logger=logger)
|
|
351
410
|
if logger:
|
|
352
411
|
logger.debug(msg=f"Token data is '{result}'")
|
|
353
412
|
except Exception as e:
|
pypomes_jwt-0.9.0/src/pypomes_jwt/jwt_data.py → pypomes_jwt-0.9.2/src/pypomes_jwt/jwt_registry.py
RENAMED
|
@@ -18,9 +18,9 @@ from .jwt_constants import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class
|
|
21
|
+
class JwtRegistry:
|
|
22
22
|
"""
|
|
23
|
-
Shared JWT
|
|
23
|
+
Shared JWT registry for security token access.
|
|
24
24
|
|
|
25
25
|
Instance variables:
|
|
26
26
|
- access_lock: lock for safe multi-threading access
|
|
@@ -59,12 +59,13 @@ class JwtData:
|
|
|
59
59
|
as token-related and account-related. All times are UTC.
|
|
60
60
|
|
|
61
61
|
Token-related claims are mostly required claims, and convey information about the token itself:
|
|
62
|
+
# required
|
|
62
63
|
"exp": <timestamp> # expiration time
|
|
63
64
|
"iat": <timestamp> # issued at
|
|
64
65
|
"iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
|
|
65
66
|
"jti": <string> # JWT id
|
|
66
67
|
"sub": <string> # subject (the account identification)
|
|
67
|
-
|
|
68
|
+
# optional
|
|
68
69
|
"aud": <string> # token audience
|
|
69
70
|
"nbt": <timestamp> # not before time
|
|
70
71
|
|
|
@@ -72,20 +73,20 @@ class JwtData:
|
|
|
72
73
|
Alhough they can be freely specified, these are some of the most commonly used claims:
|
|
73
74
|
"valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
|
|
74
75
|
"valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00.00>)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
"birthdate": <string> # subject's birth date
|
|
77
|
+
"email": <string> # subject's email
|
|
78
|
+
"gender": <string> # subject's gender
|
|
79
|
+
"name": <string> # subject's name
|
|
80
|
+
"roles": <List[str]> # subject roles
|
|
81
|
+
"nonce": <string> # value used to associate a client session with a token
|
|
81
82
|
|
|
82
83
|
The token header has these items:
|
|
83
|
-
"alg": <string> # the algorithm used to sign the token (one of
|
|
84
|
-
"typ": <string> # the token type (fixed to
|
|
84
|
+
"alg": <string> # the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
|
|
85
|
+
"typ": <string> # the token type (fixed to *JWT*
|
|
85
86
|
"kid": <string> # a reference to the token type and the key to its location in the token database
|
|
86
87
|
|
|
87
|
-
If issued by the local server, "kid" holds the key to the corresponding record in the token database
|
|
88
|
-
|
|
88
|
+
If issued by the local server, "kid" holds the key to the corresponding record in the token database,
|
|
89
|
+
if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
|
|
89
90
|
"""
|
|
90
91
|
def __init__(self) -> None:
|
|
91
92
|
"""
|
|
@@ -110,7 +111,7 @@ class JwtData:
|
|
|
110
111
|
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
111
112
|
|
|
112
113
|
The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
|
|
113
|
-
at a minimum,
|
|
114
|
+
at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
|
|
114
115
|
If the token provider is local, then the token-related claims are created at token issuing time.
|
|
115
116
|
If the token provider is remote, all claims are sent to it at token request time.
|
|
116
117
|
|
|
@@ -173,6 +174,73 @@ class JwtData:
|
|
|
173
174
|
|
|
174
175
|
return account_data is not None
|
|
175
176
|
|
|
177
|
+
def issue_token(self,
|
|
178
|
+
account_id: str,
|
|
179
|
+
nature: str,
|
|
180
|
+
duration: int,
|
|
181
|
+
grace_interval: int = None,
|
|
182
|
+
claims: dict[str, Any] = None,
|
|
183
|
+
logger: Logger = None) -> str:
|
|
184
|
+
"""
|
|
185
|
+
Issue an return a JWT token associated with *account_id*.
|
|
186
|
+
|
|
187
|
+
The parameter *nature* must be a single letter in the range *[B-Z]*, less *R*
|
|
188
|
+
(*A* is reserved for *access* tokens, and *R* for *refresh* tokens).
|
|
189
|
+
The parameter *duration* specifies the token's validity interval (at least 60 seconds).
|
|
190
|
+
These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
|
|
191
|
+
|
|
192
|
+
:param account_id: the account identification
|
|
193
|
+
:param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
|
|
194
|
+
:param duration: the number of seconds for the token to remain valid (at least 60 seconds)
|
|
195
|
+
:param claims: optional token's claims
|
|
196
|
+
:param grace_interval: optional interval for the token to become active (in seconds)
|
|
197
|
+
:param logger: optional logger
|
|
198
|
+
:return: the JWT token
|
|
199
|
+
:raises RuntimeError: invalid parameter
|
|
200
|
+
"""
|
|
201
|
+
# validate some parameters
|
|
202
|
+
err_msg: str | None = None
|
|
203
|
+
if not isinstance(nature, str) or \
|
|
204
|
+
len(nature) != 1 or nature < "A" or nature > "Z":
|
|
205
|
+
err_msg: str = f"Invalid nature '{nature}'"
|
|
206
|
+
elif not isinstance(claims, dict) or "iss" not in claims:
|
|
207
|
+
err_msg = f"invalid claims '{claims}'"
|
|
208
|
+
elif not isinstance(duration, int) or duration < 60:
|
|
209
|
+
err_msg = f"Invalid duration '{duration}'"
|
|
210
|
+
if err_msg:
|
|
211
|
+
if logger:
|
|
212
|
+
logger.error(err_msg)
|
|
213
|
+
raise RuntimeError(err_msg)
|
|
214
|
+
|
|
215
|
+
# obtain the account data in storage (may raise an exception)
|
|
216
|
+
account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
|
|
217
|
+
logger=logger)
|
|
218
|
+
# issue the token
|
|
219
|
+
current_claims: dict[str, Any] = {}
|
|
220
|
+
if claims:
|
|
221
|
+
current_claims.update(claims)
|
|
222
|
+
current_claims["jti"] = str_random(size=32,
|
|
223
|
+
chars=string.ascii_letters + string.digits)
|
|
224
|
+
current_claims["iss"] = account_data.get("reference-url")
|
|
225
|
+
current_claims["sub"] = account_id
|
|
226
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
227
|
+
current_claims["iat"] = just_now
|
|
228
|
+
if grace_interval:
|
|
229
|
+
current_claims["nbf"] = just_now + grace_interval
|
|
230
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
231
|
+
tz=timezone.utc).isoformat()
|
|
232
|
+
else:
|
|
233
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
234
|
+
tz=timezone.utc).isoformat()
|
|
235
|
+
current_claims["exp"] = just_now + duration
|
|
236
|
+
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
237
|
+
tz=timezone.utc).isoformat()
|
|
238
|
+
# may raise an exception
|
|
239
|
+
return jwt.encode(payload=current_claims,
|
|
240
|
+
key=JWT_ENCODING_KEY,
|
|
241
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
242
|
+
headers={"kid": nature})
|
|
243
|
+
|
|
176
244
|
def issue_tokens(self,
|
|
177
245
|
account_id: str,
|
|
178
246
|
account_claims: dict[str, Any] = None,
|
|
@@ -180,6 +248,9 @@ class JwtData:
|
|
|
180
248
|
"""
|
|
181
249
|
Issue and return the JWT access and refresh tokens for *account_id*.
|
|
182
250
|
|
|
251
|
+
These claims are ignored, if specified in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
|
|
252
|
+
Other claims specified therein may supercede registered account-related claims.
|
|
253
|
+
|
|
183
254
|
Structure of the return data:
|
|
184
255
|
{
|
|
185
256
|
"access_token": <jwt-token>,
|
|
@@ -191,125 +262,118 @@ class JwtData:
|
|
|
191
262
|
:param account_id: the account identification
|
|
192
263
|
:param account_claims: if provided, may supercede registered account-related claims
|
|
193
264
|
:param logger: optional logger
|
|
194
|
-
:return: the JWT token data
|
|
195
|
-
:raises
|
|
196
|
-
:raises InvalidKeyError: authentication key is not in the proper format
|
|
197
|
-
:raises ExpiredSignatureError: token and refresh period have expired
|
|
198
|
-
:raises InvalidSignatureError: signature does not match the one provided as part of the token
|
|
199
|
-
:raises ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
|
|
200
|
-
:raises InvalidAudienceError: 'aud' claim does not match one of the expected audience
|
|
201
|
-
:raises InvalidAlgorithmError: the specified algorithm is not recognized
|
|
202
|
-
:raises InvalidIssuerError: 'iss' claim does not match the expected issuer
|
|
203
|
-
:raises InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
204
|
-
:raises MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
205
|
-
:raises RuntimeError: error accessing the token database
|
|
265
|
+
:return: the JWT token data
|
|
266
|
+
:raises RuntimeError: invalid account id, or error accessing the token database
|
|
206
267
|
"""
|
|
207
268
|
# initialize the return variable
|
|
208
269
|
result: dict[str, Any] | None = None
|
|
209
270
|
|
|
210
|
-
# process the data in storage
|
|
271
|
+
# process the account data in storage
|
|
211
272
|
with (self.access_lock):
|
|
212
|
-
account_data: dict[str, Any] = self.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if errors:
|
|
243
|
-
raise RuntimeError("; ".join(errors))
|
|
273
|
+
account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
|
|
274
|
+
logger=logger)
|
|
275
|
+
current_claims: dict[str, Any] = account_data.get("claims").copy()
|
|
276
|
+
if account_claims:
|
|
277
|
+
current_claims.update(account_claims)
|
|
278
|
+
current_claims["jti"] = str_random(size=32,
|
|
279
|
+
chars=string.ascii_letters + string.digits)
|
|
280
|
+
current_claims["sub"] = account_id
|
|
281
|
+
current_claims["iss"] = account_data.get("reference-url")
|
|
282
|
+
errors: list[str] = []
|
|
283
|
+
|
|
284
|
+
# where is the JWT service provider ?
|
|
285
|
+
if account_data.get("remote-provider"):
|
|
286
|
+
# JWT service is being provided by a remote server
|
|
287
|
+
result = _jwt_request_token(errors=errors,
|
|
288
|
+
reference_url=current_claims.get("iss"),
|
|
289
|
+
claims=current_claims,
|
|
290
|
+
timeout=account_data.get("request-timeout"),
|
|
291
|
+
logger=logger)
|
|
292
|
+
if errors:
|
|
293
|
+
raise RuntimeError("; ".join(errors))
|
|
294
|
+
else:
|
|
295
|
+
# JWT service is being provided locally
|
|
296
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
297
|
+
current_claims["iat"] = just_now
|
|
298
|
+
grace_interval = account_data.get("grace-interval")
|
|
299
|
+
if grace_interval:
|
|
300
|
+
current_claims["nbf"] = just_now + grace_interval
|
|
301
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
302
|
+
tz=timezone.utc).isoformat()
|
|
244
303
|
else:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
304
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
305
|
+
tz=timezone.utc).isoformat()
|
|
306
|
+
# issue a candidate refresh token first, and persist it
|
|
307
|
+
current_claims["exp"] = just_now + account_data.get("refresh-max-age")
|
|
308
|
+
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
309
|
+
tz=timezone.utc).isoformat()
|
|
310
|
+
# may raise an exception
|
|
311
|
+
refresh_token: str = jwt.encode(payload=current_claims,
|
|
312
|
+
key=JWT_ENCODING_KEY,
|
|
313
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
314
|
+
headers={"kid": "R0"})
|
|
315
|
+
# obtain a DB connection (may raise an exception)
|
|
316
|
+
db_conn: Any = db_connect(errors=errors,
|
|
317
|
+
logger=logger)
|
|
318
|
+
# persist the candidate token (may raise an exception)
|
|
319
|
+
token_id: int = _jwt_persist_token(errors=errors,
|
|
320
|
+
account_id=account_id,
|
|
321
|
+
jwt_token=refresh_token,
|
|
322
|
+
db_conn=db_conn,
|
|
323
|
+
logger=logger)
|
|
324
|
+
# issue the definitive refresh token
|
|
325
|
+
refresh_token = jwt.encode(payload=current_claims,
|
|
326
|
+
key=JWT_ENCODING_KEY,
|
|
327
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
328
|
+
headers={"kid": f"R{token_id}"})
|
|
329
|
+
# persist it
|
|
330
|
+
db_update(errors=errors,
|
|
331
|
+
update_stmt=f"UPDATE {JWT_DB_TABLE}",
|
|
332
|
+
update_data={JWT_DB_COL_TOKEN: refresh_token},
|
|
333
|
+
where_data={JWT_DB_COL_KID: token_id},
|
|
334
|
+
connection=db_conn,
|
|
335
|
+
logger=logger)
|
|
336
|
+
# commit the transaction
|
|
337
|
+
db_commit(errors=errors,
|
|
338
|
+
connection=db_conn,
|
|
339
|
+
logger=logger)
|
|
340
|
+
if errors:
|
|
341
|
+
raise RuntimeError("; ".join(errors))
|
|
342
|
+
|
|
343
|
+
# issue the access token
|
|
344
|
+
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
345
|
+
# may raise an exception
|
|
346
|
+
access_token: str = jwt.encode(payload=current_claims,
|
|
276
347
|
key=JWT_ENCODING_KEY,
|
|
277
348
|
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
278
|
-
headers={"kid": f"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
else:
|
|
308
|
-
# JWT access data not found
|
|
309
|
-
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
310
|
-
if logger:
|
|
311
|
-
logger.error(err_msg)
|
|
312
|
-
raise RuntimeError(err_msg)
|
|
349
|
+
headers={"kid": f"A{token_id}"})
|
|
350
|
+
# return the token data
|
|
351
|
+
result = {
|
|
352
|
+
"access_token": access_token,
|
|
353
|
+
"created_in": current_claims.get("iat"),
|
|
354
|
+
"expires_in": current_claims.get("exp"),
|
|
355
|
+
"refresh_token": refresh_token
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
def __get_account_data(self,
|
|
361
|
+
account_id: str,
|
|
362
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
363
|
+
"""
|
|
364
|
+
Retrieve the JWT access data associated with *account_id*.
|
|
365
|
+
|
|
366
|
+
:return: the JWT access data associated with *account_id*
|
|
367
|
+
:raises RuntimeError: No JWT access data exists for *account_id*
|
|
368
|
+
"""
|
|
369
|
+
# retrieve the access data
|
|
370
|
+
result: dict[str, Any] = self.access_data.get(account_id)
|
|
371
|
+
if not result:
|
|
372
|
+
# JWT access data not found
|
|
373
|
+
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
374
|
+
if logger:
|
|
375
|
+
logger.error(err_msg)
|
|
376
|
+
raise RuntimeError(err_msg)
|
|
313
377
|
|
|
314
378
|
return result
|
|
315
379
|
|
|
@@ -389,7 +453,7 @@ def _jwt_persist_token(errors: list[str],
|
|
|
389
453
|
:param db_conn: the database connection to use
|
|
390
454
|
:param logger: optional logger
|
|
391
455
|
:return: the storage id of the inserted token
|
|
392
|
-
:raises RuntimeError: error accessing the
|
|
456
|
+
:raises RuntimeError: error accessing the token database
|
|
393
457
|
"""
|
|
394
458
|
from pypomes_db import db_select, db_insert, db_delete
|
|
395
459
|
from .jwt_pomes import jwt_get_claims
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|