pypomes-jwt 0.9.1__py3-none-any.whl → 0.9.3__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.
pypomes_jwt/__init__.py CHANGED
@@ -9,8 +9,8 @@ from .jwt_constants import (
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
- jwt_issue_token, jwt_issue_tokens, jwt_get_claims,
13
- jwt_validate_token, jwt_revoke_token
12
+ jwt_issue_token, jwt_issue_tokens, jwt_refresh_tokens,
13
+ jwt_get_claims, jwt_validate_token, jwt_revoke_token
14
14
  )
15
15
 
16
16
  __all__ = [
@@ -24,8 +24,8 @@ __all__ = [
24
24
  # jwt_pomes
25
25
  "jwt_needed", "jwt_verify_request",
26
26
  "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
27
- "jwt_issue_token", "jwt_issue_tokens", "jwt_get_claims",
28
- "jwt_validate_token", "jwt_revoke_token"
27
+ "jwt_issue_token", "jwt_issue_tokens", "jwt_refresh_tokens",
28
+ "jwt_get_claims", "jwt_validate_token", "jwt_revoke_token"
29
29
  ]
30
30
 
31
31
  from importlib.metadata import version
pypomes_jwt/jwt_pomes.py CHANGED
@@ -61,7 +61,7 @@ def jwt_verify_request(request: Request,
61
61
  logger.debug(msg="Bearer token was retrieved")
62
62
  errors: list[str] = []
63
63
  jwt_validate_token(errors=errors,
64
- nature=["A"],
64
+ natures=["A"],
65
65
  token=token)
66
66
  if errors:
67
67
  err_msg = "; ".join(errors)
@@ -157,20 +157,20 @@ def jwt_remove_account(account_id: str,
157
157
 
158
158
  def jwt_validate_token(errors: list[str] | None,
159
159
  token: str,
160
- nature: list[str] = None,
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, it is checked 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.
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 nature: one of more prefixes identifying the nature of locally issued tokens
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,7 +188,7 @@ 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 nature and not (token_kid and token_kid[0:1] in nature):
191
+ if natures and not (token_kid and token_kid[0:1] in natures):
192
192
  op_errors.append("Invalid token")
193
193
  elif token_kid and len(token_kid) > 1 and token_kid[0:1].isupper() and token[1:].isdigit():
194
194
  # token was likely issued locally
@@ -263,7 +263,7 @@ def jwt_revoke_token(errors: list[str] | None,
263
263
 
264
264
  :param errors: incidental error messages
265
265
  :param account_id: the account identification
266
- :param refresh_token: the token to be revolked
266
+ :param refresh_token: the token to be revoked
267
267
  :param logger: optional logger
268
268
  :return: *True* if operation could be performed, *False* otherwise
269
269
  """
@@ -276,7 +276,7 @@ def jwt_revoke_token(errors: list[str] | None,
276
276
  op_errors: list[str] = []
277
277
  token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
278
278
  token=refresh_token,
279
- nature=["A", "R"],
279
+ natures=["A", "R"],
280
280
  account_id=account_id,
281
281
  logger=logger)
282
282
  if not op_errors:
@@ -303,22 +303,22 @@ def jwt_issue_token(errors: list[str] | None,
303
303
  account_id: str,
304
304
  nature: str,
305
305
  duration: int,
306
- claims: dict[str, Any],
307
306
  grace_interval: int = None,
307
+ claims: dict[str, Any] = None,
308
308
  logger: Logger = None) -> str:
309
309
  """
310
310
  Issue or refresh, and return, a JWT token associated with *account_id*, of the specified *nature*.
311
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).
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
314
  The parameter *duration* specifies the token's validity interval (at least 60 seconds).
315
- The token's *claims* should contain the claim *iss*.
315
+ These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
316
316
 
317
317
  :param errors: incidental error messages
318
318
  :param account_id: the account identification
319
- :param nature: the token's nature (a single uppercase, unaccented letter different from "A" and "R")
319
+ :param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
320
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")
321
+ :param claims: optional token's claims
322
322
  :param grace_interval: optional interval for the token to become active (in seconds)
323
323
  :param logger: optional logger
324
324
  :return: the JWT token data, or *None* if error
@@ -327,7 +327,7 @@ def jwt_issue_token(errors: list[str] | None,
327
327
  result: str | None = None
328
328
 
329
329
  if logger:
330
- logger.debug(msg=f"Issue a JWT token for '{account_id}'")
330
+ logger.debug(msg=f"Issuing a JWT token for '{account_id}'")
331
331
  op_errors: list[str] = []
332
332
 
333
333
  try:
@@ -355,13 +355,14 @@ def jwt_issue_token(errors: list[str] | None,
355
355
  def jwt_issue_tokens(errors: list[str] | None,
356
356
  account_id: str,
357
357
  account_claims: dict[str, Any] = None,
358
- refresh_token: str = None,
359
358
  logger: Logger = None) -> dict[str, Any]:
360
359
  """
361
360
  Issue the JWT tokens associated with *account_id*, for access and refresh operations.
362
361
 
363
- If *refresh_token* is provided, its claims are used on issuing the new tokens,
364
- and claims in *account_claims*, if any, are ignored.
362
+ If *refresh_token* is provided, its claims are used on issuing the new tokens, and
363
+ claims in *account_claims*, if any, are ignored. Furthermore, these claims are ignored,
364
+ if provided in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
365
+ Other claims specified therein may supercede registered account-related claims.
365
366
 
366
367
  Structure of the return data:
367
368
  {
@@ -374,7 +375,6 @@ def jwt_issue_tokens(errors: list[str] | None,
374
375
  :param errors: incidental error messages
375
376
  :param account_id: the account identification
376
377
  :param account_claims: if provided, may supercede registered claims
377
- :param refresh_token: if provided, defines a token refresh operation
378
378
  :param logger: optional logger
379
379
  :return: the JWT token data, or *None* if error
380
380
  """
@@ -382,33 +382,82 @@ def jwt_issue_tokens(errors: list[str] | None,
382
382
  result: dict[str, Any] | None = None
383
383
 
384
384
  if logger:
385
- logger.debug(msg=f"Return JWT token data for '{account_id}'")
385
+ logger.debug(msg=f"Issuing a pair of JWT tokens for '{account_id}'")
386
+ op_errors: list[str] = []
387
+
388
+ try:
389
+ result = __jwt_registry.issue_tokens(account_id=account_id,
390
+ account_claims=account_claims,
391
+ logger=logger)
392
+ if logger:
393
+ logger.debug(msg=f"Token data is '{result}'")
394
+ except Exception as e:
395
+ # token issuing failed
396
+ op_errors.append(str(e))
397
+
398
+ if op_errors:
399
+ if logger:
400
+ logger.error("; ".join(op_errors))
401
+ if isinstance(errors, list):
402
+ errors.extend(op_errors)
403
+
404
+ return result
405
+
406
+
407
+ def jwt_refresh_tokens(errors: list[str] | None,
408
+ account_id: str,
409
+ refresh_token: str = None,
410
+ logger: Logger = None) -> dict[str, Any]:
411
+ """
412
+ Issue the JWT tokens associated with *account_id*, for access and refresh operations.
413
+
414
+ The claims in *refresh-token* are used on issuing the new tokens.
415
+
416
+ Structure of the return data:
417
+ {
418
+ "access_token": <jwt-token>,
419
+ "created_in": <timestamp>,
420
+ "expires_in": <seconds-to-expiration>,
421
+ "refresh_token": <jwt-token>
422
+ }
423
+
424
+ :param errors: incidental error messages
425
+ :param account_id: the account identification
426
+ :param refresh_token: the base refresh token
427
+ :param logger: optional logger
428
+ :return: the JWT token data, or *None* if error
429
+ """
430
+ # inicialize the return variable
431
+ result: dict[str, Any] | None = None
432
+
433
+ if logger:
434
+ logger.debug(msg=f"Refreshing a pair of JWT tokens for '{account_id}'")
386
435
  op_errors: list[str] = []
387
436
 
388
437
  # verify whether this refresh token is legitimate
389
438
  if refresh_token:
390
- account_claims = (jwt_validate_token(errors=op_errors,
391
- token=refresh_token,
392
- nature=["R"],
393
- account_id=account_id,
394
- logger=logger) or {}).get("payload")
395
- if account_claims:
439
+ account_claims: dict[str, Any] = (jwt_validate_token(errors=op_errors,
440
+ token=refresh_token,
441
+ natures=["R"],
442
+ account_id=account_id,
443
+ logger=logger) or {}).get("payload")
444
+ # revoke current refresh token
445
+ if account_claims and jwt_revoke_token(errors=errors,
446
+ account_id=account_id,
447
+ refresh_token=refresh_token,
448
+ logger=logger):
449
+ account_claims.pop("exp", None)
396
450
  account_claims.pop("iat", None)
397
- account_claims.pop("jti", None)
398
451
  account_claims.pop("iss", None)
399
- account_claims.pop("exp", None)
452
+ account_claims.pop("jti", None)
400
453
  account_claims.pop("nbt", None)
401
-
402
- if not op_errors:
403
- try:
404
- result = __jwt_registry.issue_tokens(account_id=account_id,
405
- account_claims=account_claims,
406
- logger=logger)
407
- if logger:
408
- logger.debug(msg=f"Token data is '{result}'")
409
- except Exception as e:
410
- # token issuing failed
411
- op_errors.append(str(e))
454
+ account_claims.pop("sub", None)
455
+ # issue tokens
456
+ result = jwt_issue_tokens(errors=errors,
457
+ account_id=account_id,
458
+ account_claims=account_claims)
459
+ else:
460
+ op_errors.append("Refresh token was not provided")
412
461
 
413
462
  if op_errors:
414
463
  if logger:
@@ -81,12 +81,12 @@ class JwtRegistry:
81
81
  "nonce": <string> # value used to associate a client session with a token
82
82
 
83
83
  The token header has these items:
84
- "alg": <string> # the algorithm used to sign the token (one of 'HS256', 'HS512', 'RSA256', 'RSA512')
85
- "typ": <string> # the token type (fixed to 'JWT'
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*
86
86
  "kid": <string> # a reference to the token type and the key to its location in the token database
87
87
 
88
- If issued by the local server, "kid" holds the key to the corresponding record in the token database.
89
- It starts with *A* for (*Access*) or *R* (for *Refresh*) followed its integer key.
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.
90
90
  """
91
91
  def __init__(self) -> None:
92
92
  """
@@ -111,7 +111,7 @@ class JwtRegistry:
111
111
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
112
112
 
113
113
  The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
114
- at a minimum, "birthdate", "email", "gender", "name", and "roles".
114
+ at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
115
115
  If the token provider is local, then the token-related claims are created at token issuing time.
116
116
  If the token provider is remote, all claims are sent to it at token request time.
117
117
 
@@ -178,21 +178,21 @@ class JwtRegistry:
178
178
  account_id: str,
179
179
  nature: str,
180
180
  duration: int,
181
- claims: dict[str, Any],
182
181
  grace_interval: int = None,
182
+ claims: dict[str, Any] = None,
183
183
  logger: Logger = None) -> str:
184
184
  """
185
185
  Issue an return a JWT token associated with *account_id*.
186
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).
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
189
  The parameter *duration* specifies the token's validity interval (at least 60 seconds).
190
- The token's *claims* should contain the claim *iss*.
190
+ These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
191
191
 
192
192
  :param account_id: the account identification
193
- :param nature: the token's nature (a single uppercase, unaccented letter different from "A" and "R")
193
+ :param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
194
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")
195
+ :param claims: optional token's claims
196
196
  :param grace_interval: optional interval for the token to become active (in seconds)
197
197
  :param logger: optional logger
198
198
  :return: the JWT token
@@ -212,13 +212,16 @@ class JwtRegistry:
212
212
  logger.error(err_msg)
213
213
  raise RuntimeError(err_msg)
214
214
 
215
- # make sure account data exists
216
- self.__get_account_data(account_id=account_id,
217
- logger=logger)
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
218
  # issue the token
219
- current_claims: dict[str, Any] = claims.copy()
219
+ current_claims: dict[str, Any] = {}
220
+ if claims:
221
+ current_claims.update(claims)
220
222
  current_claims["jti"] = str_random(size=32,
221
223
  chars=string.ascii_letters + string.digits)
224
+ current_claims["iss"] = account_data.get("reference-url")
222
225
  current_claims["sub"] = account_id
223
226
  just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
224
227
  current_claims["iat"] = just_now
@@ -245,6 +248,9 @@ class JwtRegistry:
245
248
  """
246
249
  Issue and return the JWT access and refresh tokens for *account_id*.
247
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
+
248
254
  Structure of the return data:
249
255
  {
250
256
  "access_token": <jwt-token>,
@@ -262,7 +268,7 @@ class JwtRegistry:
262
268
  # initialize the return variable
263
269
  result: dict[str, Any] | None = None
264
270
 
265
- # process the data in storage
271
+ # process the account data in storage
266
272
  with (self.access_lock):
267
273
  account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
268
274
  logger=logger)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.9.1
3
+ Version: 0.9.3
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=t6TzpvttDuLMaKSGuBicOf9cZU4Y0N9mtby3ThS4lt8,1398
2
+ pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
3
+ pypomes_jwt/jwt_pomes.py,sha256=UnEkOUN0vovlenyb8ROvpM96Qf0Mx-JRle-EooyTy7k,21734
4
+ pypomes_jwt/jwt_registry.py,sha256=TANRyMGxoO7sR2EwO_bgVzIMjM3OHAr7olvnSmMtwCQ,26020
5
+ pypomes_jwt-0.9.3.dist-info/METADATA,sha256=DrB3ku89ZNBM3FBpnnnmDv-Rvi4F70sIXj0M5zJnQOM,632
6
+ pypomes_jwt-0.9.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.9.3.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.9.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
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,,