pypomes-jwt 0.9.0__py3-none-any.whl → 0.9.1__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 +7 -5
- pypomes_jwt/jwt_pomes.py +85 -29
- pypomes_jwt/{jwt_data.py → jwt_registry.py} +178 -120
- {pypomes_jwt-0.9.0.dist-info → pypomes_jwt-0.9.1.dist-info}/METADATA +1 -1
- pypomes_jwt-0.9.1.dist-info/RECORD +8 -0
- pypomes_jwt-0.9.0.dist-info/RECORD +0 -8
- {pypomes_jwt-0.9.0.dist-info → pypomes_jwt-0.9.1.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.9.0.dist-info → pypomes_jwt-0.9.1.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/__init__.py
CHANGED
|
@@ -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
|
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -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,7 +58,7 @@ 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
|
nature=["A"],
|
|
@@ -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,8 +151,8 @@ 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,
|
|
@@ -190,7 +190,8 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
190
190
|
# retrieve token data from database
|
|
191
191
|
if nature and not (token_kid and token_kid[0:1] in nature):
|
|
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
|
|
@@ -298,13 +299,66 @@ 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
|
+
claims: dict[str, Any],
|
|
307
|
+
grace_interval: int = 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 uppercase, unaccented letter, different from "A"
|
|
313
|
+
(used to indicate *access* tokens) and *R* (used to indicate *refresh* tokens).
|
|
314
|
+
The parameter *duration* specifies the token's validity interval (at least 60 seconds).
|
|
315
|
+
The token's *claims* should contain the claim *iss*.
|
|
316
|
+
|
|
317
|
+
:param errors: incidental error messages
|
|
318
|
+
:param account_id: the account identification
|
|
319
|
+
:param nature: the token's nature (a single uppercase, unaccented letter different from "A" and "R")
|
|
320
|
+
:param duration: the number of seconds for the token to remain valid (at least 60 seconds)
|
|
321
|
+
:param claims: the token's claims (must contain at least the claim "iss")
|
|
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
|
|
306
325
|
"""
|
|
307
|
-
|
|
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]:
|
|
360
|
+
"""
|
|
361
|
+
Issue the JWT tokens associated with *account_id*, for access and refresh operations.
|
|
308
362
|
|
|
309
363
|
If *refresh_token* is provided, its claims are used on issuing the new tokens,
|
|
310
364
|
and claims in *account_claims*, if any, are ignored.
|
|
@@ -328,10 +382,11 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
328
382
|
result: dict[str, Any] | None = None
|
|
329
383
|
|
|
330
384
|
if logger:
|
|
331
|
-
logger.debug(msg=f"
|
|
385
|
+
logger.debug(msg=f"Return JWT token data for '{account_id}'")
|
|
332
386
|
op_errors: list[str] = []
|
|
387
|
+
|
|
388
|
+
# verify whether this refresh token is legitimate
|
|
333
389
|
if refresh_token:
|
|
334
|
-
# verify whether this refresh token is legitimate
|
|
335
390
|
account_claims = (jwt_validate_token(errors=op_errors,
|
|
336
391
|
token=refresh_token,
|
|
337
392
|
nature=["R"],
|
|
@@ -343,11 +398,12 @@ def jwt_get_tokens(errors: list[str] | None,
|
|
|
343
398
|
account_claims.pop("iss", None)
|
|
344
399
|
account_claims.pop("exp", None)
|
|
345
400
|
account_claims.pop("nbt", None)
|
|
401
|
+
|
|
346
402
|
if not op_errors:
|
|
347
403
|
try:
|
|
348
|
-
result =
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
result = __jwt_registry.issue_tokens(account_id=account_id,
|
|
405
|
+
account_claims=account_claims,
|
|
406
|
+
logger=logger)
|
|
351
407
|
if logger:
|
|
352
408
|
logger.debug(msg=f"Token data is '{result}'")
|
|
353
409
|
except Exception as e:
|
|
@@ -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,12 +73,12 @@ 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
84
|
"alg": <string> # the algorithm used to sign the token (one of 'HS256', 'HS512', 'RSA256', 'RSA512')
|
|
@@ -173,6 +174,70 @@ 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
|
+
claims: dict[str, Any],
|
|
182
|
+
grace_interval: int = 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 uppercase, unaccented letter, different from "A"
|
|
188
|
+
(used to indicate *access* tokens) and *R* (used to indicate *refresh* tokens).
|
|
189
|
+
The parameter *duration* specifies the token's validity interval (at least 60 seconds).
|
|
190
|
+
The token's *claims* should contain the claim *iss*.
|
|
191
|
+
|
|
192
|
+
:param account_id: the account identification
|
|
193
|
+
:param nature: the token's nature (a single uppercase, unaccented letter different from "A" and "R")
|
|
194
|
+
:param duration: the number of seconds for the token to remain valid (at least 60 seconds)
|
|
195
|
+
:param claims: the token's claims (must contain at least the claim "iss")
|
|
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
|
+
# make sure account data exists
|
|
216
|
+
self.__get_account_data(account_id=account_id,
|
|
217
|
+
logger=logger)
|
|
218
|
+
# issue the token
|
|
219
|
+
current_claims: dict[str, Any] = claims.copy()
|
|
220
|
+
current_claims["jti"] = str_random(size=32,
|
|
221
|
+
chars=string.ascii_letters + string.digits)
|
|
222
|
+
current_claims["sub"] = account_id
|
|
223
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
224
|
+
current_claims["iat"] = just_now
|
|
225
|
+
if grace_interval:
|
|
226
|
+
current_claims["nbf"] = just_now + grace_interval
|
|
227
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
228
|
+
tz=timezone.utc).isoformat()
|
|
229
|
+
else:
|
|
230
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
231
|
+
tz=timezone.utc).isoformat()
|
|
232
|
+
current_claims["exp"] = just_now + duration
|
|
233
|
+
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
234
|
+
tz=timezone.utc).isoformat()
|
|
235
|
+
# may raise an exception
|
|
236
|
+
return jwt.encode(payload=current_claims,
|
|
237
|
+
key=JWT_ENCODING_KEY,
|
|
238
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
239
|
+
headers={"kid": nature})
|
|
240
|
+
|
|
176
241
|
def issue_tokens(self,
|
|
177
242
|
account_id: str,
|
|
178
243
|
account_claims: dict[str, Any] = None,
|
|
@@ -191,125 +256,118 @@ class JwtData:
|
|
|
191
256
|
:param account_id: the account identification
|
|
192
257
|
:param account_claims: if provided, may supercede registered account-related claims
|
|
193
258
|
: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
|
|
259
|
+
:return: the JWT token data
|
|
260
|
+
:raises RuntimeError: invalid account id, or error accessing the token database
|
|
206
261
|
"""
|
|
207
262
|
# initialize the return variable
|
|
208
263
|
result: dict[str, Any] | None = None
|
|
209
264
|
|
|
210
265
|
# process the data in storage
|
|
211
266
|
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))
|
|
267
|
+
account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
|
|
268
|
+
logger=logger)
|
|
269
|
+
current_claims: dict[str, Any] = account_data.get("claims").copy()
|
|
270
|
+
if account_claims:
|
|
271
|
+
current_claims.update(account_claims)
|
|
272
|
+
current_claims["jti"] = str_random(size=32,
|
|
273
|
+
chars=string.ascii_letters + string.digits)
|
|
274
|
+
current_claims["sub"] = account_id
|
|
275
|
+
current_claims["iss"] = account_data.get("reference-url")
|
|
276
|
+
errors: list[str] = []
|
|
277
|
+
|
|
278
|
+
# where is the JWT service provider ?
|
|
279
|
+
if account_data.get("remote-provider"):
|
|
280
|
+
# JWT service is being provided by a remote server
|
|
281
|
+
result = _jwt_request_token(errors=errors,
|
|
282
|
+
reference_url=current_claims.get("iss"),
|
|
283
|
+
claims=current_claims,
|
|
284
|
+
timeout=account_data.get("request-timeout"),
|
|
285
|
+
logger=logger)
|
|
286
|
+
if errors:
|
|
287
|
+
raise RuntimeError("; ".join(errors))
|
|
288
|
+
else:
|
|
289
|
+
# JWT service is being provided locally
|
|
290
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
291
|
+
current_claims["iat"] = just_now
|
|
292
|
+
grace_interval = account_data.get("grace-interval")
|
|
293
|
+
if grace_interval:
|
|
294
|
+
current_claims["nbf"] = just_now + grace_interval
|
|
295
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
|
|
296
|
+
tz=timezone.utc).isoformat()
|
|
244
297
|
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
|
-
|
|
298
|
+
current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
|
|
299
|
+
tz=timezone.utc).isoformat()
|
|
300
|
+
# issue a candidate refresh token first, and persist it
|
|
301
|
+
current_claims["exp"] = just_now + account_data.get("refresh-max-age")
|
|
302
|
+
current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
|
|
303
|
+
tz=timezone.utc).isoformat()
|
|
304
|
+
# may raise an exception
|
|
305
|
+
refresh_token: str = jwt.encode(payload=current_claims,
|
|
306
|
+
key=JWT_ENCODING_KEY,
|
|
307
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
308
|
+
headers={"kid": "R0"})
|
|
309
|
+
# obtain a DB connection (may raise an exception)
|
|
310
|
+
db_conn: Any = db_connect(errors=errors,
|
|
311
|
+
logger=logger)
|
|
312
|
+
# persist the candidate token (may raise an exception)
|
|
313
|
+
token_id: int = _jwt_persist_token(errors=errors,
|
|
314
|
+
account_id=account_id,
|
|
315
|
+
jwt_token=refresh_token,
|
|
316
|
+
db_conn=db_conn,
|
|
317
|
+
logger=logger)
|
|
318
|
+
# issue the definitive refresh token
|
|
319
|
+
refresh_token = jwt.encode(payload=current_claims,
|
|
320
|
+
key=JWT_ENCODING_KEY,
|
|
321
|
+
algorithm=JWT_DEFAULT_ALGORITHM,
|
|
322
|
+
headers={"kid": f"R{token_id}"})
|
|
323
|
+
# persist it
|
|
324
|
+
db_update(errors=errors,
|
|
325
|
+
update_stmt=f"UPDATE {JWT_DB_TABLE}",
|
|
326
|
+
update_data={JWT_DB_COL_TOKEN: refresh_token},
|
|
327
|
+
where_data={JWT_DB_COL_KID: token_id},
|
|
328
|
+
connection=db_conn,
|
|
329
|
+
logger=logger)
|
|
330
|
+
# commit the transaction
|
|
331
|
+
db_commit(errors=errors,
|
|
332
|
+
connection=db_conn,
|
|
333
|
+
logger=logger)
|
|
334
|
+
if errors:
|
|
335
|
+
raise RuntimeError("; ".join(errors))
|
|
336
|
+
|
|
337
|
+
# issue the access token
|
|
338
|
+
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
339
|
+
# may raise an exception
|
|
340
|
+
access_token: str = jwt.encode(payload=current_claims,
|
|
276
341
|
key=JWT_ENCODING_KEY,
|
|
277
342
|
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)
|
|
343
|
+
headers={"kid": f"A{token_id}"})
|
|
344
|
+
# return the token data
|
|
345
|
+
result = {
|
|
346
|
+
"access_token": access_token,
|
|
347
|
+
"created_in": current_claims.get("iat"),
|
|
348
|
+
"expires_in": current_claims.get("exp"),
|
|
349
|
+
"refresh_token": refresh_token
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return result
|
|
353
|
+
|
|
354
|
+
def __get_account_data(self,
|
|
355
|
+
account_id: str,
|
|
356
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Retrieve the JWT access data associated with *account_id*.
|
|
359
|
+
|
|
360
|
+
:return: the JWT access data associated with *account_id*
|
|
361
|
+
:raises RuntimeError: No JWT access data exists for *account_id*
|
|
362
|
+
"""
|
|
363
|
+
# retrieve the access data
|
|
364
|
+
result: dict[str, Any] = self.access_data.get(account_id)
|
|
365
|
+
if not result:
|
|
366
|
+
# JWT access data not found
|
|
367
|
+
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
368
|
+
if logger:
|
|
369
|
+
logger.error(err_msg)
|
|
370
|
+
raise RuntimeError(err_msg)
|
|
313
371
|
|
|
314
372
|
return result
|
|
315
373
|
|
|
@@ -389,7 +447,7 @@ def _jwt_persist_token(errors: list[str],
|
|
|
389
447
|
:param db_conn: the database connection to use
|
|
390
448
|
:param logger: optional logger
|
|
391
449
|
:return: the storage id of the inserted token
|
|
392
|
-
:raises RuntimeError: error accessing the
|
|
450
|
+
:raises RuntimeError: error accessing the token database
|
|
393
451
|
"""
|
|
394
452
|
from pypomes_db import db_select, db_insert, db_delete
|
|
395
453
|
from .jwt_pomes import jwt_get_claims
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
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=6AISZOs7JP695PnMSsYyKfoiMXvSzXffkv3nKw7qA1A,1356
|
|
2
|
+
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
+
pypomes_jwt/jwt_pomes.py,sha256=qm8COn0vPts-I0n5pnA_5phm-5wkV82dicAf3bvQ8R8,19734
|
|
4
|
+
pypomes_jwt/jwt_registry.py,sha256=MWJy1bxXdgVySmBYJCdK_721U6tJXk3BHrYVChLjCR8,25613
|
|
5
|
+
pypomes_jwt-0.9.1.dist-info/METADATA,sha256=b5EpwtK-c0JCi3QjRt6W_QoQXg8nYkUjJHbD0pTGgF4,632
|
|
6
|
+
pypomes_jwt-0.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pypomes_jwt-0.9.1.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
+
pypomes_jwt-0.9.1.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
|
|
2
|
-
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
-
pypomes_jwt/jwt_data.py,sha256=keC95-2DgWYZIzAtYEbtLtfkFSH-V_sTBiTbszsZqho,23286
|
|
4
|
-
pypomes_jwt/jwt_pomes.py,sha256=0C4u9UnzqM1ZWTW4rpw18kyvnR1biLEq5nAwZ8MMrz8,17244
|
|
5
|
-
pypomes_jwt-0.9.0.dist-info/METADATA,sha256=0KthAWPxm04v3KMnYfsaHal4_kN0tFj8er2a52A__AI,632
|
|
6
|
-
pypomes_jwt-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
pypomes_jwt-0.9.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
-
pypomes_jwt-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|