pypomes-jwt 0.9.4__tar.gz → 0.9.6__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.9.4
3
+ Version: 0.9.6
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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_jwt"
9
- version = "0.9.4"
9
+ version = "0.9.6"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -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
- natures=["A"],
64
+ nature="A",
65
65
  token=token)
66
66
  if errors:
67
67
  err_msg = "; ".join(errors)
@@ -89,47 +89,48 @@ def jwt_assert_account(account_id: str) -> bool:
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
96
  request_timeout: int = None,
98
- remote_provider: bool = None,
97
+ remote_url: str = None,
99
98
  logger: Logger = None) -> None:
100
99
  """
101
- Set the data needed to obtain JWT tokens for *account_id*.
100
+ Establish the data needed to obtain JWT tokens for *account_id*.
102
101
 
103
102
  :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
103
  :param claims: the JWT claimset, as key-value pairs
106
104
  :param access_max_age: access token duration, in seconds
107
105
  :param refresh_max_age: refresh token duration, in seconds
108
106
  :param grace_interval: optional time to wait for token to be valid, in seconds
109
107
  :param request_timeout: timeout for the requests to the reference URL
110
- :param remote_provider: whether the JWT provider is a remote server
108
+ :param remote_url: remote server's URL to obtain the JWT tokens from
111
109
  :param logger: optional logger
112
110
  """
113
111
  if logger:
114
112
  logger.debug(msg=f"Register account data for '{account_id}'")
115
113
 
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]
114
+ # extract the claims provided in the remote_url URL's query string
115
+ if remote_url:
116
+ pos: int = remote_url.find("?")
117
+ if pos > 0:
118
+ claims = claims or {}
119
+ params: list[str] = remote_url[pos+1:].split(sep="&")
120
+ for param in params:
121
+ key: str = param.split("=")[0]
122
+ value: str = param.split("=")[1]
123
+ claims[key] = value
124
+ remote_url = remote_url[:pos]
123
125
 
124
126
  # register the JWT service
125
127
  __jwt_registry.add_account(account_id=account_id,
126
- reference_url=reference_url,
127
128
  claims=claims,
128
129
  access_max_age=access_max_age,
129
130
  refresh_max_age=refresh_max_age,
130
131
  grace_interval=grace_interval,
131
132
  request_timeout=request_timeout,
132
- remote_provider=remote_provider,
133
+ remote_url=remote_url,
133
134
  logger=logger)
134
135
 
135
136
 
@@ -151,7 +152,7 @@ def jwt_remove_account(account_id: str,
151
152
 
152
153
  def jwt_validate_token(errors: list[str] | None,
153
154
  token: str,
154
- natures: list[str] = None,
155
+ nature: str = None,
155
156
  account_id: str = None,
156
157
  logger: Logger = None) -> dict[str, Any] | None:
157
158
  """
@@ -164,7 +165,7 @@ def jwt_validate_token(errors: list[str] | None,
164
165
 
165
166
  :param errors: incidental error messages
166
167
  :param token: the token to be validated
167
- :param natures: one or more prefixes identifying the nature of locally issued tokens
168
+ :param nature: prefix identifying the nature of locally issued tokens
168
169
  :param account_id: optionally, validate the token's account owner
169
170
  :param logger: optional logger
170
171
  :return: The token's claims (header and payload) if if is valid, *None* otherwise
@@ -182,9 +183,10 @@ def jwt_validate_token(errors: list[str] | None,
182
183
  op_errors: list[str] = []
183
184
 
184
185
  # retrieve token data from database
185
- if natures and not (token_kid and token_kid[0:1] in natures):
186
+ if nature and not (token_kid and token_kid[0:1] == nature):
186
187
  op_errors.append("Invalid token")
187
- elif token_kid and len(token_kid) > 1 and token_kid[0:1].isupper() and token[1:].isdigit():
188
+ elif token_kid and len(token_kid) > 1 and \
189
+ token_kid[0:1] in ["A", "R"] and token[1:].isdigit():
188
190
  # token was likely issued locally
189
191
  where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
190
192
  if account_id:
@@ -270,18 +272,20 @@ def jwt_revoke_token(errors: list[str] | None,
270
272
  op_errors: list[str] = []
271
273
  token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
272
274
  token=refresh_token,
273
- natures=["A", "R"],
274
275
  account_id=account_id,
275
276
  logger=logger)
276
277
  if not op_errors:
277
278
  token_kid: str = token_claims["header"].get("kid")
278
- db_delete(errors=op_errors,
279
- delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
280
- where_data={
281
- JWT_DB_COL_KID: int(token_kid[1:]),
282
- JWT_DB_COL_ACCOUNT: account_id
283
- },
284
- logger=logger)
279
+ if token_kid[0:1] not in ["A", "R"]:
280
+ op_errors.append("Invalid token")
281
+ else:
282
+ db_delete(errors=op_errors,
283
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
284
+ where_data={
285
+ JWT_DB_COL_KID: int(token_kid[1:]),
286
+ JWT_DB_COL_ACCOUNT: account_id
287
+ },
288
+ logger=logger)
285
289
  if op_errors:
286
290
  if logger:
287
291
  logger.error(msg="; ".join(op_errors))
@@ -351,12 +355,10 @@ def jwt_issue_tokens(errors: list[str] | None,
351
355
  account_claims: dict[str, Any] = None,
352
356
  logger: Logger = None) -> dict[str, Any]:
353
357
  """
354
- Issue the JWT tokens associated with *account_id*, for access and refresh operations.
358
+ Issue the JWT token pair associated with *account_id*, for access and refresh operations.
355
359
 
356
- If *refresh_token* is provided, its claims are used on issuing the new tokens, and
357
- claims in *account_claims*, if any, are ignored. Furthermore, these claims are ignored,
358
- if provided in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
359
- Other claims specified therein may supercede registered account-related claims.
360
+ These claims are ignored, if provided in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
361
+ Other claims specified therein may supercede currently registered account-related claims.
360
362
 
361
363
  Structure of the return data:
362
364
  {
@@ -368,7 +370,7 @@ def jwt_issue_tokens(errors: list[str] | None,
368
370
 
369
371
  :param errors: incidental error messages
370
372
  :param account_id: the account identification
371
- :param account_claims: if provided, may supercede registered claims
373
+ :param account_claims: if provided, may supercede currently registered account-related claims
372
374
  :param logger: optional logger
373
375
  :return: the JWT token data, or *None* if error
374
376
  """
@@ -376,7 +378,7 @@ def jwt_issue_tokens(errors: list[str] | None,
376
378
  result: dict[str, Any] | None = None
377
379
 
378
380
  if logger:
379
- logger.debug(msg=f"Issuing a pair of JWT tokens for '{account_id}'")
381
+ logger.debug(msg=f"Issuing a JWT token pair for '{account_id}'")
380
382
  op_errors: list[str] = []
381
383
 
382
384
  try:
@@ -400,10 +402,10 @@ def jwt_issue_tokens(errors: list[str] | None,
400
402
 
401
403
  def jwt_refresh_tokens(errors: list[str] | None,
402
404
  account_id: str,
403
- refresh_token: str = None,
405
+ refresh_token: str,
404
406
  logger: Logger = None) -> dict[str, Any]:
405
407
  """
406
- Issue the JWT tokens associated with *account_id*, for access and refresh operations.
408
+ Refresh the JWT token pair associated with *account_id*, for access and refresh operations.
407
409
 
408
410
  The claims in *refresh-token* are used on issuing the new tokens.
409
411
 
@@ -425,14 +427,14 @@ def jwt_refresh_tokens(errors: list[str] | None,
425
427
  result: dict[str, Any] | None = None
426
428
 
427
429
  if logger:
428
- logger.debug(msg=f"Refreshing a pair of JWT tokens for '{account_id}'")
430
+ logger.debug(msg=f"Refreshing a JWT token pair for '{account_id}'")
429
431
  op_errors: list[str] = []
430
432
 
431
433
  # verify whether this refresh token is legitimate
432
434
  if refresh_token:
433
435
  account_claims: dict[str, Any] = (jwt_validate_token(errors=op_errors,
434
436
  token=refresh_token,
435
- natures=["R"],
437
+ nature="R",
436
438
  account_id=account_id,
437
439
  logger=logger) or {}).get("payload")
438
440
  # revoke current refresh token
@@ -440,12 +442,6 @@ def jwt_refresh_tokens(errors: list[str] | None,
440
442
  account_id=account_id,
441
443
  refresh_token=refresh_token,
442
444
  logger=logger):
443
- account_claims.pop("exp", None)
444
- account_claims.pop("iat", None)
445
- account_claims.pop("iss", None)
446
- account_claims.pop("jti", None)
447
- account_claims.pop("nbt", None)
448
- account_claims.pop("sub", None)
449
445
  # issue tokens
450
446
  result = jwt_issue_tokens(errors=errors,
451
447
  account_id=account_id,
@@ -27,23 +27,22 @@ class JwtRegistry:
27
27
  - access_data: dictionary holding the JWT token data, organized by account id:
28
28
  {
29
29
  <account-id>: {
30
- "reference-url": # the reference URL
31
- "remote-provider": <bool>, # whether the JWT provider is a remote server
32
- "request-timeout": <int>, # in seconds - defaults to no timeout
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)
30
+ "access-max-age": <int>, # defaults to JWT_ACCESS_MAX_AGE (in seconds)
31
+ "refresh-max-age": <int>, # defaults to JWT_REFRESH_MAX_AGE (in seconds)
32
+ "grace-interval": <int>, # time to wait for token to be valid, in seconds
33
+ "request-timeout": <int>, # timeout for the requests to the reference URL (in seconds)
34
+ "remote_url": <string>, # remote server's URL to obtain the JWT tokens from
37
35
  "claims": {
38
- "valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
39
- "valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
40
- # optional
36
+ "iss": <string>, # token'ss issuer
41
37
  "birthdate": <string>, # subject's birth date
42
38
  "email": <string>, # subject's email
43
39
  "gender": <string>, # subject's gender
44
40
  "name": <string>, # subject's name
45
41
  "roles": <List[str]>, # subject roles
46
42
  "nonce": <string>, # used to associate a Client session with a token
43
+ # output, only
44
+ "valid-from": <string>, # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
45
+ "valid-until": <string>, # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
47
46
  ...
48
47
  }
49
48
  },
@@ -58,30 +57,30 @@ class JwtRegistry:
58
57
 
59
58
  Token-related claims are mostly required claims, and convey information about the token itself:
60
59
  # required
61
- "exp": <timestamp> # expiration time
62
- "iat": <timestamp> # issued at
63
- "iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
64
- "jti": <string> # JWT id
65
- "sub": <string> # subject (the account identification)
60
+ "exp": <timestamp> expiration time
61
+ "iat": <timestamp> issued at
62
+ "iss": <string> token's issuer
63
+ "jti": <string> JWT id
64
+ "sub": <string> subject (the account identification)
66
65
  # optional
67
- "aud": <string> # token audience
68
- "nbt": <timestamp> # not before time
66
+ "aud": <string> token audience
67
+ "nbt": <timestamp> not before time
69
68
 
70
69
  Account-related claims are optional claims, and convey information about the registered account they belong to.
71
70
  Alhough they can be freely specified, these are some of the most commonly used claims:
72
- "valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
73
- "valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00.00>)
74
- "birthdate": <string> # subject's birth date
75
- "email": <string> # subject's email
76
- "gender": <string> # subject's gender
77
- "name": <string> # subject's name
78
- "roles": <List[str]> # subject roles
79
- "nonce": <string> # used to associate a client session with a token
71
+ "valid-from": <string> token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
72
+ "valid-until": <string> token's finish (<YYYY-MM-DDThh:mm:ss+00.00>)
73
+ "birthdate": <string> subject's birth date
74
+ "email": <string> subject's email
75
+ "gender": <string> subject's gender
76
+ "name": <string> subject's name
77
+ "roles": <List[str]> subject roles
78
+ "nonce": <string> used to associate a client session with a token
80
79
 
81
80
  The token header has these items:
82
- "alg": <string> # the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
83
- "typ": <string> # the token type (fixed to *JWT*
84
- "kid": <string> # a reference to the token type and the key to its location in the token database
81
+ "alg": <string> the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
82
+ "typ": <string> the token type (fixed to *JWT*
83
+ "kid": <string> a reference to the token type and the key to its location in the token database
85
84
 
86
85
  If issued by the local server, "kid" holds the key to the corresponding record in the token database,
87
86
  if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
@@ -95,13 +94,12 @@ class JwtRegistry:
95
94
 
96
95
  def add_account(self,
97
96
  account_id: str,
98
- reference_url: str,
99
97
  claims: dict[str, Any],
100
98
  access_max_age: int,
101
99
  refresh_max_age: int,
102
100
  grace_interval: int | None,
103
101
  request_timeout: int | None,
104
- remote_provider: bool | None,
102
+ remote_url: str | None,
105
103
  logger: Logger = None) -> None:
106
104
  """
107
105
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
@@ -112,25 +110,23 @@ class JwtRegistry:
112
110
  If the token provider is remote, all claims are sent to it at token request time.
113
111
 
114
112
  :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
113
  :param claims: the JWT claimset, as key-value pairs
117
114
  :param access_max_age: access token duration, in seconds
118
115
  :param refresh_max_age: refresh token duration, in seconds
119
116
  :param grace_interval: time to wait for token to be valid, in seconds
120
117
  :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
118
+ :param remote_url: remote server's URL to obtain the JWT tokens from
122
119
  :param logger: optional logger
123
120
  """
124
121
  # build and store the access data for the account
125
122
  with self.access_lock:
126
123
  if account_id not in self.access_data:
127
124
  self.access_data[account_id] = {
128
- "reference-url": reference_url,
129
125
  "access-max-age": access_max_age,
130
126
  "refresh-max-age": refresh_max_age,
131
127
  "grace-interval": grace_interval,
132
128
  "request-timeout": request_timeout,
133
- "remote-provider": remote_provider,
129
+ "remote-url": remote_url,
134
130
  "claims": claims or {}
135
131
  }
136
132
  if logger:
@@ -195,8 +191,6 @@ class JwtRegistry:
195
191
  if not isinstance(nature, str) or \
196
192
  len(nature) != 1 or nature < "A" or nature > "Z":
197
193
  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
194
  elif not isinstance(duration, int) or duration < 60:
201
195
  err_msg = f"Invalid duration '{duration}'"
202
196
  if err_msg:
@@ -209,11 +203,14 @@ class JwtRegistry:
209
203
  logger=logger)
210
204
  # issue the token
211
205
  current_claims: dict[str, Any] = {}
206
+ iss: str = account_data["claims"].get("iss")
207
+ if iss:
208
+ current_claims["iss"] = iss
212
209
  if claims:
213
210
  current_claims.update(claims)
211
+
214
212
  current_claims["jti"] = str_random(size=32,
215
213
  chars=string.ascii_letters + string.digits)
216
- current_claims["iss"] = account_data.get("reference-url")
217
214
  current_claims["sub"] = account_id
218
215
  just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
219
216
  current_claims["iat"] = just_now
@@ -238,9 +235,9 @@ class JwtRegistry:
238
235
  account_claims: dict[str, Any] = None,
239
236
  logger: Logger = None) -> dict[str, Any]:
240
237
  """
241
- Issue and return the JWT access and refresh tokens for *account_id*.
238
+ Issue and return a JWT token pair associated with *account_id*.
242
239
 
243
- These claims are ignored, if specified in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
240
+ These claims are ignored, if specified in *account_claims*: *iat*, *exp*, *jti*, *nbf*, and *sub*.
244
241
  Other claims specified therein may supercede registered account-related claims.
245
242
 
246
243
  Structure of the return data:
@@ -264,20 +261,19 @@ class JwtRegistry:
264
261
  with (self.access_lock):
265
262
  account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
266
263
  logger=logger)
267
- current_claims: dict[str, Any] = account_data.get("claims").copy()
264
+ current_claims: dict[str, Any] = account_data["claims"].copy()
268
265
  if account_claims:
269
266
  current_claims.update(account_claims)
270
267
  current_claims["jti"] = str_random(size=32,
271
268
  chars=string.ascii_letters + string.digits)
272
269
  current_claims["sub"] = account_id
273
- current_claims["iss"] = account_data.get("reference-url")
274
270
  errors: list[str] = []
275
271
 
276
272
  # where is the JWT service provider ?
277
- if account_data.get("remote-provider"):
273
+ if account_data.get("remote-url"):
278
274
  # JWT service is being provided by a remote server
279
275
  result = _jwt_request_token(errors=errors,
280
- reference_url=current_claims.get("iss"),
276
+ remote_url=account_data.get("remote-url"),
281
277
  claims=current_claims,
282
278
  timeout=account_data.get("request-timeout"),
283
279
  logger=logger)
@@ -371,7 +367,7 @@ class JwtRegistry:
371
367
 
372
368
 
373
369
  def _jwt_request_token(errors: list[str],
374
- reference_url: str,
370
+ remote_url: str,
375
371
  claims: dict[str, Any],
376
372
  timeout: int = None,
377
373
  logger: Logger = None) -> dict[str, Any]:
@@ -389,7 +385,7 @@ def _jwt_request_token(errors: list[str],
389
385
  of the provider issuing the JWT token.
390
386
 
391
387
  :param errors: incidental errors
392
- :param reference_url: the reference URL for obtaining JWT tokens
388
+ :param remote_url: the reference URL for obtaining JWT tokens
393
389
  :param claims: the JWT claimset, as expected by the issuing server
394
390
  :param timeout: request timeout, in seconds (defaults to *None*)
395
391
  :param logger: optional logger
@@ -399,9 +395,9 @@ def _jwt_request_token(errors: list[str],
399
395
 
400
396
  # request the JWT token
401
397
  if logger:
402
- logger.debug(f"POST request JWT token to '{reference_url}'")
398
+ logger.debug(f"POST request JWT token to '{remote_url}'")
403
399
  response: Response = requests.post(
404
- url=reference_url,
400
+ url=remote_url,
405
401
  json=claims,
406
402
  timeout=timeout
407
403
  )
@@ -414,7 +410,7 @@ def _jwt_request_token(errors: list[str],
414
410
  logger.debug(f"JWT token obtained: {result}")
415
411
  else:
416
412
  # no, report the problem
417
- err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
413
+ err_msg: str = f"POST request to '{remote_url}' failed: {response.reason}"
418
414
  if response.text:
419
415
  err_msg += f" - {response.text}"
420
416
  if logger:
File without changes
File without changes
File without changes