pypomes-jwt 0.6.1__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of pypomes-jwt might be problematic. Click here for more details.

pypomes_jwt/jwt_data.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import jwt
2
2
  import requests
3
- from datetime import datetime, timedelta, timezone
3
+ from datetime import datetime, timezone
4
4
  from jwt.exceptions import InvalidTokenError
5
5
  from logging import Logger
6
+ from pypomes_core import str_random
6
7
  from requests import Response
7
8
  from threading import Lock
8
9
  from typing import Any, Literal
@@ -17,6 +18,17 @@ class JwtData:
17
18
  - access_data: list with dictionaries holding the JWT token data:
18
19
  [
19
20
  {
21
+ "control-data": { # control data
22
+ "remote-provider": <bool>, # whether the JWT provider is a remote server
23
+ "access-token": <jwt-token>, # access token
24
+ "algorithm": <string>, # HS256, HS512, RSA256, RSA512
25
+ "request-timeout": <int>, # in seconds - defaults to no timeout
26
+ "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
27
+ "refresh-exp": <timestamp>, # expiration time for the refresh operation
28
+ "hs-secret-key": <bytes>, # HS secret key
29
+ "rsa-private-key": <bytes>, # RSA private key
30
+ "rsa-public-key": <bytes>, # RSA public key
31
+ },
20
32
  "reserved-claims": { # reserved claims
21
33
  "exp": <timestamp>, # expiration time
22
34
  "iat": <timestamp> # issued at
@@ -38,17 +50,6 @@ class JwtData:
38
50
  "<custom-claim-key-1>": "<custom-claim-value-1>",
39
51
  ...
40
52
  "<custom-claim-key-n>": "<custom-claim-value-n>"
41
- },
42
- "control-data": { # control data
43
- "remote-provider": <bool>, # whether the JWT provider is a remote server
44
- "access-token": <jwt-token>, # access token
45
- "algorithm": <string>, # HS256, HS512, RSA256, RSA512
46
- "request-timeout": <int>, # in seconds - defaults to no timeout
47
- "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
48
- "refresh-exp": <timestamp>, # expiration time for the refresh operation
49
- "secret-key": <bytes>, # HS secret key
50
- "private-key": <bytes>, # RSA private key
51
- "public-key": <bytes>, # RSA public key
52
53
  }
53
54
  },
54
55
  ...
@@ -68,9 +69,9 @@ class JwtData:
68
69
  algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
69
70
  access_max_age: int,
70
71
  refresh_max_age: int,
71
- secret_key: bytes,
72
- private_key: bytes,
73
- public_key: bytes,
72
+ hs_secret_key: bytes,
73
+ rsa_private_key: bytes,
74
+ rsa_public_key: bytes,
74
75
  request_timeout: int,
75
76
  remote_provider: bool,
76
77
  logger: Logger = None) -> None:
@@ -78,11 +79,11 @@ class JwtData:
78
79
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
79
80
 
80
81
  The parameter *claims* may contain public and custom claims. Currently, the public claims supported
81
- are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else are considered to be custom
82
- claims, and are sent to the remote JWT provided, if applicable.
82
+ are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else is considered to be custom
83
+ claims, and sent to the remote JWT provided, if applicable.
83
84
 
84
85
  Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
85
- (typically, an acess-key/secret-key pair), have been previously validated elsewhere.
86
+ (typically, an acess-key/hs-secret-key pair), have been previously validated elsewhere.
86
87
  This situation might change in the future.
87
88
 
88
89
  :param account_id: the account identification
@@ -91,28 +92,28 @@ class JwtData:
91
92
  :param algorithm: the algorithm used to sign the token with
92
93
  :param access_max_age: token duration (in seconds)
93
94
  :param refresh_max_age: duration for the refresh operation (in seconds)
94
- :param secret_key: secret key for HS authentication
95
- :param private_key: private key for RSA authentication
96
- :param public_key: public key for RSA authentication
95
+ :param hs_secret_key: secret key for HS authentication
96
+ :param rsa_private_key: private key for RSA authentication
97
+ :param rsa_public_key: public key for RSA authentication
97
98
  :param request_timeout: timeout for the requests to the reference URL
98
99
  :param remote_provider: whether the JWT provider is a remote server
99
100
  :param logger: optional logger
100
101
  """
101
102
  # Do the access data already exist ?
102
- if not self.retrieve_access_data(account_id=account_id):
103
+ if not self.get_access_data(account_id=account_id):
103
104
  # no, build control data
104
105
  control_data: dict[str, Any] = {
105
106
  "algorithm": algorithm,
106
107
  "access-max-age": access_max_age,
107
108
  "request-timeout": request_timeout,
108
109
  "remote-provider": remote_provider,
109
- "refresh-exp": datetime.now(tz=timezone.utc) + timedelta(seconds=refresh_max_age)
110
+ "refresh-exp": datetime.now(tz=timezone.utc).timestamp() + refresh_max_age
110
111
  }
111
112
  if algorithm in ["HS256", "HS512"]:
112
- control_data["secret-key"] = secret_key
113
+ control_data["hs-secret-key"] = hs_secret_key
113
114
  else:
114
- control_data["private-key"] = private_key
115
- control_data["public-key"] = public_key
115
+ control_data["rsa-private-key"] = rsa_private_key
116
+ control_data["rsa-public-key"] = rsa_public_key
116
117
 
117
118
  # build claims
118
119
  reserved_claims: dict[str, Any] = {
@@ -120,7 +121,7 @@ class JwtData:
120
121
  "iss": reference_url,
121
122
  "exp": "<numeric-UTC-datetime>",
122
123
  "iat": "<numeric-UTC-datetime>",
123
- "jti": "<jwt-id",
124
+ "jti": "<jwt-id>",
124
125
  }
125
126
  custom_claims: dict[str, Any] = {}
126
127
  public_claims: dict[str, Any] = {}
@@ -153,8 +154,8 @@ class JwtData:
153
154
  :param logger: optional logger
154
155
  """
155
156
  # obtain the access data item in storage
156
- item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
157
- logger=logger)
157
+ item_data: dict[str, dict[str, Any]] = self.get_access_data(account_id=account_id,
158
+ logger=logger)
158
159
  if item_data:
159
160
  with self.access_lock:
160
161
  self.access_data.remove(item_data)
@@ -172,6 +173,7 @@ class JwtData:
172
173
  Structure of the return data:
173
174
  {
174
175
  "access_token": <jwt-token>,
176
+ "created_in": <timestamp>,
175
177
  "expires_in": <seconds-to-expiration>
176
178
  }
177
179
 
@@ -195,8 +197,8 @@ class JwtData:
195
197
  result: dict[str, Any]
196
198
 
197
199
  # obtain the item in storage
198
- item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
199
- logger=logger)
200
+ item_data: dict[str, Any] = self.get_access_data(account_id=account_id,
201
+ logger=logger)
200
202
  # was the JWT data obtained ?
201
203
  if item_data:
202
204
  # yes, proceed
@@ -207,40 +209,51 @@ class JwtData:
207
209
 
208
210
  # obtain a new token, if the current token has expired
209
211
  if just_now > reserved_claims.get("exp"):
210
- # where is the locus of the JWT service provider ?
212
+ # where is the JWT service provider ?
211
213
  if control_data.get("remote-provider"):
212
214
  # JWT service is being provided by a remote server
213
215
  errors: list[str] = []
214
- result = jwt_request_token(errors=errors,
215
- reference_url=reserved_claims.get("iss"),
216
- claims=custom_claims,
217
- timeout=control_data.get("request-timeout"),
218
- logger=logger)
219
- if result:
216
+ # Structure of the return data:
217
+ # {
218
+ # "access_token": <jwt-token>,
219
+ # "created_in": <timestamp>,
220
+ # "expires_in": <seconds-to-expiration>,
221
+ # ...
222
+ # }
223
+ reply: dict[str, Any] = jwt_request_token(errors=errors,
224
+ reference_url=reserved_claims.get("iss"),
225
+ claims=custom_claims,
226
+ timeout=control_data.get("request-timeout"),
227
+ logger=logger)
228
+ if reply:
220
229
  with self.access_lock:
221
- control_data["access-token"] = result.get("access_token")
222
- duration: int = result.get("expires_in")
223
- reserved_claims["exp"] = just_now + duration
230
+ control_data["access-token"] = reply.get("access_token")
231
+ reserved_claims["jti"] = str_random(size=16)
232
+ reserved_claims["iat"] = reply.get("created_in")
233
+ reserved_claims["exp"] = reply.get("created_in") + reply.get("expires_in")
224
234
  else:
225
235
  raise RuntimeError(" - ".join(errors))
226
236
  else:
227
237
  # JWT service is being provided locally
228
- reserved_claims["iat"] = just_now
229
- reserved_claims["exp"] = just_now + control_data.get("access-max-age")
230
238
  claims: dict[str, Any] = item_data.get("public-claims").copy()
231
- claims.update(reserved_claims)
232
- claims.update(custom_claims)
239
+ claims.update(m=reserved_claims)
240
+ claims.update(m=custom_claims)
233
241
  # may raise an exception
234
242
  token: str = jwt.encode(payload=claims,
235
- key=control_data.get("secret-key") or control_data.get("private-key"),
243
+ key=(control_data.get("hs-secret-key") or
244
+ control_data.get("rsa-private-key")),
236
245
  algorithm=control_data.get("algorithm"))
237
246
  with self.access_lock:
247
+ reserved_claims["jti"] = str_random(size=16)
248
+ reserved_claims["iat"] = just_now
249
+ reserved_claims["exp"] = just_now + control_data.get("access-max-age")
238
250
  control_data["access-token"] = token
239
251
 
240
252
  # return the token
241
253
  result = {
242
254
  "access_token": control_data.get("access-token"),
243
- "expires_in": reserved_claims.get("exp") - just_now
255
+ "created_in": reserved_claims.get("iat"),
256
+ "expires_in": reserved_claims.get("exp") - reserved_claims.get("iat")
244
257
  }
245
258
  else:
246
259
  # JWT access data not found
@@ -263,37 +276,46 @@ class JwtData:
263
276
  :raises InvalidTokenError: token is not valid
264
277
  :raises ExpiredSignatureError: token has expired
265
278
  """
266
- algorithm: str | None = None
267
- key: str | None = None
268
- with self.access_lock:
269
- for item_data in self.access_data:
270
- control_data: dict[str, Any] = item_data.get("control-data")
271
- if token == control_data.get("access-token"):
272
- algorithm = control_data.get("algorithm")
273
- key = control_data.get("public-key") or control_data.get("secret-key")
274
- break
275
-
276
- if not algorithm or not key:
277
- raise InvalidTokenError("JWT token is not valid")
279
+ # declare the return variable
280
+ result: dict[str, Any]
278
281
 
279
282
  if logger:
280
283
  logger.debug(msg=f"Retrieve claims for JWT token '{token}'")
281
- result: dict[str, Any] = jwt.decode(jwt=token,
282
- key=key,
283
- algorithms=[algorithm])
284
+
285
+ control_data: dict[str, Any] = self.get_access_data(access_token=token,
286
+ logger=logger)
287
+ if control_data:
288
+ if control_data.get("remote-provider"):
289
+ # provider is remote
290
+ result = control_data.get("custom-claims")
291
+ else:
292
+ # may raise InvalidTokenError or ExpiredSignatureError
293
+ result = jwt.decode(jwt=token,
294
+ key=(control_data.get("hs-secret-key") or
295
+ control_data.get("rsa-public-key")),
296
+ algorithms=[control_data.get("algorithm")])
297
+ else:
298
+ raise InvalidTokenError("JWT token is not valid")
299
+
284
300
  if logger:
285
301
  logger.debug(f"Retrieved claims for JWT token '{token}': {result}")
286
302
 
287
303
  return result
288
304
 
289
- def retrieve_access_data(self,
290
- account_id: str,
291
- logger: Logger = None) -> dict[str, dict[str, Any]]:
305
+ def get_access_data(self,
306
+ account_id: str = None,
307
+ access_token: str = None,
308
+ logger: Logger = None) -> dict[str, dict[str, Any]]:
292
309
  # noinspection HttpUrlsUsage
293
310
  """
294
- Retrieve and return the access data in storage for *account_id*.
311
+ Retrieve and return the access data in storage for *account_id*, or optionally, for *access_token*.
312
+
313
+ Either *account_id* or *access_token* must be provided, the former having precedence over the later.
314
+ Note that, whereas *account_id* uniquely identifies an access dataset, *access_token* might not,
315
+ and thus, the first dataset associated with it would be returned.
295
316
 
296
317
  :param account_id: the account identification
318
+ :param access_token: the access token
297
319
  :param logger: optional logger
298
320
  :return: the corresponding item in storage, or *None* if not found
299
321
  """
@@ -301,11 +323,13 @@ class JwtData:
301
323
  result: dict[str, dict[str, Any]] | None = None
302
324
 
303
325
  if logger:
304
- logger.debug(f"Retrieve access data for account id '{account_id}'")
326
+ target: str = f"account id '{account_id}'" if account_id else f"token '{access_token}'"
327
+ logger.debug(f"Retrieve access data for {target}")
305
328
  # retrieve the data
306
329
  with self.access_lock:
307
330
  for item_data in self.access_data:
308
- if account_id == item_data.get("reserved-claims").get("sub"):
331
+ if (account_id and account_id == item_data.get("reserved-claims").get("sub")) or \
332
+ (access_token and access_token == item_data.get("control-data").get("access-token")):
309
333
  result = item_data
310
334
  break
311
335
  if logger:
@@ -320,7 +344,7 @@ def jwt_request_token(errors: list[str],
320
344
  timeout: int = None,
321
345
  logger: Logger = None) -> dict[str, Any]:
322
346
  """
323
- Obtain and return the JWT token associated with *reference_url*, along with its duration.
347
+ Obtain and return the JWT token from *reference_url*, along with its duration.
324
348
 
325
349
  Expected structure of the return data:
326
350
  {
pypomes_jwt/jwt_pomes.py CHANGED
@@ -2,6 +2,7 @@ import contextlib
2
2
  from cryptography.hazmat.primitives import serialization
3
3
  from cryptography.hazmat.primitives.asymmetric import rsa
4
4
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
5
+ from datetime import datetime
5
6
  from flask import Request, Response, request, jsonify
6
7
  from logging import Logger
7
8
  from pypomes_core import APP_PREFIX, env_get_str, env_get_bytes, env_get_int
@@ -64,7 +65,7 @@ def jwt_assert_access(account_id: str) -> bool:
64
65
  :param account_id: the account identification
65
66
  :return: *True* if access data exists for *account_id*, *False* otherwise
66
67
  """
67
- return __jwt_data.retrieve_access_data(account_id=account_id) is not None
68
+ return __jwt_data.get_access_data(account_id=account_id) is not None
68
69
 
69
70
 
70
71
  def jwt_set_access(account_id: str,
@@ -113,9 +114,9 @@ def jwt_set_access(account_id: str,
113
114
  algorithm=algorithm,
114
115
  access_max_age=access_max_age,
115
116
  refresh_max_age=refresh_max_age,
116
- secret_key=secret_key,
117
- private_key=private_key,
118
- public_key=public_key,
117
+ hs_secret_key=secret_key,
118
+ rsa_private_key=private_key,
119
+ rsa_public_key=public_key,
119
120
  request_timeout=request_timeout,
120
121
  remote_provider=remote_provider,
121
122
  logger=logger)
@@ -176,6 +177,7 @@ def jwt_get_token_data(errors: list[str],
176
177
  Structure of the return data:
177
178
  {
178
179
  "access_token": <jwt-token>,
180
+ "created_in": <timestamp>,
179
181
  "expires_in": <seconds-to-expiration>
180
182
  }
181
183
 
@@ -236,13 +238,14 @@ def jwt_verify_request(request: Request,
236
238
 
237
239
  :param request: the request to be verified
238
240
  :param logger: optional logger
239
- :return: 'None' if the request is valid, otherwise a 'Response' object reporting the error
241
+ :return: *None* if the request is valid, otherwise a *Response* object reporting the error
240
242
  """
241
243
  # initialize the return variable
242
244
  result: Response | None = None
243
-
245
+
244
246
  if logger:
245
247
  logger.debug(msg="Validate a JWT token")
248
+ err_msg: str | None = None
246
249
 
247
250
  # retrieve the authorization from the request header
248
251
  auth_header: str = request.headers.get("Authorization")
@@ -253,20 +256,34 @@ def jwt_verify_request(request: Request,
253
256
  token: str = auth_header.split(" ")[1]
254
257
  if logger:
255
258
  logger.debug(msg=f"Token is '{token}'")
256
- try:
257
- jwt_validate_token(token=token,
258
- key=JWT_HS_SECRET_KEY or JWT_RSA_PUBLIC_KEY,
259
- algorithm=JWT_DEFAULT_ALGORITHM)
260
- except Exception as e:
261
- # validation failed
262
- if logger:
263
- logger.error(msg=str(e))
264
- result = Response(response=str(e),
265
- status=401)
259
+ # retrieve the reference access data
260
+ access_data: dict[str, Any] = __jwt_data.get_access_data(access_token=token)
261
+ if access_data:
262
+ control_data: dict[str, Any] = access_data.get("control-data")
263
+ if control_data.get("remote-provider"):
264
+ # JWT provider is remote
265
+ if datetime.now().timestamp() > access_data.get("reserved-claims").get("exp"):
266
+ err_msg = "Token has expired"
267
+ else:
268
+ # JWT was locally provided
269
+ try:
270
+ jwt_validate_token(token=token,
271
+ key=(control_data.get("hs-secret-key") or
272
+ control_data.get("rsa-public-key")),
273
+ algorithm=control_data.get("algorithm"))
274
+ except Exception as e:
275
+ # validation failed
276
+ err_msg = str(e)
277
+ else:
278
+ err_msg = "No access data found for token"
266
279
  else:
267
- # no, report the error
280
+ # no 'Bearer' found, report the error
281
+ err_msg = "Request header has no 'Bearer' data"
282
+
283
+ # log the error and deny the authorization
284
+ if err_msg:
268
285
  if logger:
269
- logger.error(msg="Request header has no 'Bearer' data")
286
+ logger.error(msg=err_msg)
270
287
  result = Response(response="Authorization failed",
271
288
  status=401)
272
289
 
@@ -293,13 +310,14 @@ def jwt_service(account_id: str = None,
293
310
  Structure of the return data:
294
311
  {
295
312
  "access_token": <jwt-token>,
313
+ "created_in": <timestamp>,
296
314
  "expires_in": <seconds-to-expiration>
297
315
  }
298
316
 
299
317
  :param account_id: the account identification, alternatively passed in JSON
300
318
  :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
301
319
  :param logger: optional logger
302
- :return: the requested JWT token, along with its duration.
320
+ :return: a *Response* containing the requested JWT token and its duration, or reporting an error
303
321
  """
304
322
  # declare the return variable
305
323
  result: Response
@@ -324,8 +342,8 @@ def jwt_service(account_id: str = None,
324
342
  # yes, proceed
325
343
  if logger:
326
344
  logger.debug(msg=f"Account identification is '{account_id}'")
327
- item_data: dict[str, dict[str, Any]] = __jwt_data.retrieve_access_data(account_id=account_id,
328
- logger=logger) or {}
345
+ item_data: dict[str, dict[str, Any]] = __jwt_data.get_access_data(account_id=account_id,
346
+ logger=logger) or {}
329
347
  custom_claims: dict[str, Any] = item_data.get("custom-claims").copy()
330
348
  for key, value in params.items():
331
349
  custom_claims[key] = value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.6.1
3
+ Version: 0.6.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
@@ -12,4 +12,4 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: cryptography>=44.0.1
14
14
  Requires-Dist: pyjwt>=2.10.1
15
- Requires-Dist: pypomes-core>=1.7.7
15
+ Requires-Dist: pypomes-core>=1.7.8
@@ -0,0 +1,7 @@
1
+ pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
2
+ pypomes_jwt/jwt_data.py,sha256=5GB5NgmVeTJinlfIAO7BaWO0aPCETqG3dxm-aP99pCk,19222
3
+ pypomes_jwt/jwt_pomes.py,sha256=U8Vc0IOlW5-XmRR_Px2xLlVit5oAHVnfBcNwDzwh_8I,14786
4
+ pypomes_jwt-0.6.3.dist-info/METADATA,sha256=WaVMfTzEO-cnKmP7XDm-p8owJxmIhzua6yk61s0l86E,599
5
+ pypomes_jwt-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ pypomes_jwt-0.6.3.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
+ pypomes_jwt-0.6.3.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
2
- pypomes_jwt/jwt_data.py,sha256=2LnD9_0VMlsUi95jw3biSY5j21boqApSLAyZ_HXMiks,17722
3
- pypomes_jwt/jwt_pomes.py,sha256=93o0QC7Phsb_29KaLn9mlfE6nUw8HXadqpCZn-Q8gvI,13891
4
- pypomes_jwt-0.6.1.dist-info/METADATA,sha256=qY2VCQtNpS2WtKUDdMC-Gq5IXyRvFfk8rDhu9MeDyFM,599
5
- pypomes_jwt-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.6.1.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.6.1.dist-info/RECORD,,