pypomes-jwt 0.6.2__py3-none-any.whl → 0.6.4__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/jwt_data.py CHANGED
@@ -1,6 +1,6 @@
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
6
  from pypomes_core import str_random
@@ -18,6 +18,17 @@ class JwtData:
18
18
  - access_data: list with dictionaries holding the JWT token data:
19
19
  [
20
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
+ },
21
32
  "reserved-claims": { # reserved claims
22
33
  "exp": <timestamp>, # expiration time
23
34
  "iat": <timestamp> # issued at
@@ -39,17 +50,6 @@ class JwtData:
39
50
  "<custom-claim-key-1>": "<custom-claim-value-1>",
40
51
  ...
41
52
  "<custom-claim-key-n>": "<custom-claim-value-n>"
42
- },
43
- "control-data": { # control data
44
- "remote-provider": <bool>, # whether the JWT provider is a remote server
45
- "access-token": <jwt-token>, # access token
46
- "algorithm": <string>, # HS256, HS512, RSA256, RSA512
47
- "request-timeout": <int>, # in seconds - defaults to no timeout
48
- "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
49
- "refresh-exp": <timestamp>, # expiration time for the refresh operation
50
- "secret-key": <bytes>, # HS secret key
51
- "private-key": <bytes>, # RSA private key
52
- "public-key": <bytes>, # RSA public key
53
53
  }
54
54
  },
55
55
  ...
@@ -69,9 +69,9 @@ class JwtData:
69
69
  algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
70
70
  access_max_age: int,
71
71
  refresh_max_age: int,
72
- secret_key: bytes,
73
- private_key: bytes,
74
- public_key: bytes,
72
+ hs_secret_key: bytes,
73
+ rsa_private_key: bytes,
74
+ rsa_public_key: bytes,
75
75
  request_timeout: int,
76
76
  remote_provider: bool,
77
77
  logger: Logger = None) -> None:
@@ -79,11 +79,11 @@ class JwtData:
79
79
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
80
80
 
81
81
  The parameter *claims* may contain public and custom claims. Currently, the public claims supported
82
- are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else are considered to be custom
83
- 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.
84
84
 
85
85
  Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
86
- (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.
87
87
  This situation might change in the future.
88
88
 
89
89
  :param account_id: the account identification
@@ -92,35 +92,35 @@ class JwtData:
92
92
  :param algorithm: the algorithm used to sign the token with
93
93
  :param access_max_age: token duration (in seconds)
94
94
  :param refresh_max_age: duration for the refresh operation (in seconds)
95
- :param secret_key: secret key for HS authentication
96
- :param private_key: private key for RSA authentication
97
- :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
98
98
  :param request_timeout: timeout for the requests to the reference URL
99
99
  :param remote_provider: whether the JWT provider is a remote server
100
100
  :param logger: optional logger
101
101
  """
102
102
  # Do the access data already exist ?
103
- if not self.retrieve_access_data(account_id=account_id):
103
+ if not self.get_access_data(account_id=account_id):
104
104
  # no, build control data
105
105
  control_data: dict[str, Any] = {
106
106
  "algorithm": algorithm,
107
107
  "access-max-age": access_max_age,
108
108
  "request-timeout": request_timeout,
109
109
  "remote-provider": remote_provider,
110
- "refresh-exp": datetime.now(tz=timezone.utc) + timedelta(seconds=refresh_max_age)
110
+ "refresh-exp": int(datetime.now(tz=timezone.utc).timestamp()) + refresh_max_age
111
111
  }
112
112
  if algorithm in ["HS256", "HS512"]:
113
- control_data["secret-key"] = secret_key
113
+ control_data["hs-secret-key"] = hs_secret_key
114
114
  else:
115
- control_data["private-key"] = private_key
116
- control_data["public-key"] = public_key
115
+ control_data["rsa-private-key"] = rsa_private_key
116
+ control_data["rsa-public-key"] = rsa_public_key
117
117
 
118
118
  # build claims
119
119
  reserved_claims: dict[str, Any] = {
120
120
  "sub": account_id,
121
121
  "iss": reference_url,
122
- "exp": "<numeric-UTC-datetime>",
123
- "iat": "<numeric-UTC-datetime>",
122
+ "exp": 0,
123
+ "iat": 0,
124
124
  "jti": "<jwt-id>",
125
125
  }
126
126
  custom_claims: dict[str, Any] = {}
@@ -154,8 +154,8 @@ class JwtData:
154
154
  :param logger: optional logger
155
155
  """
156
156
  # obtain the access data item in storage
157
- item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
158
- logger=logger)
157
+ item_data: dict[str, dict[str, Any]] = self.get_access_data(account_id=account_id,
158
+ logger=logger)
159
159
  if item_data:
160
160
  with self.access_lock:
161
161
  self.access_data.remove(item_data)
@@ -173,6 +173,7 @@ class JwtData:
173
173
  Structure of the return data:
174
174
  {
175
175
  "access_token": <jwt-token>,
176
+ "created_in": <timestamp>,
176
177
  "expires_in": <seconds-to-expiration>
177
178
  }
178
179
 
@@ -196,8 +197,8 @@ class JwtData:
196
197
  result: dict[str, Any]
197
198
 
198
199
  # obtain the item in storage
199
- item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
200
- logger=logger)
200
+ item_data: dict[str, Any] = self.get_access_data(account_id=account_id,
201
+ logger=logger)
201
202
  # was the JWT data obtained ?
202
203
  if item_data:
203
204
  # yes, proceed
@@ -208,41 +209,51 @@ class JwtData:
208
209
 
209
210
  # obtain a new token, if the current token has expired
210
211
  if just_now > reserved_claims.get("exp"):
211
- # where is the locus of the JWT service provider ?
212
+ # where is the JWT service provider ?
212
213
  if control_data.get("remote-provider"):
213
214
  # JWT service is being provided by a remote server
214
215
  errors: list[str] = []
215
- result = jwt_request_token(errors=errors,
216
- reference_url=reserved_claims.get("iss"),
217
- claims=custom_claims,
218
- timeout=control_data.get("request-timeout"),
219
- logger=logger)
220
- 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:
221
229
  with self.access_lock:
222
- control_data["access-token"] = result.get("access_token")
223
- duration: int = result.get("expires_in")
224
- 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")
225
234
  else:
226
235
  raise RuntimeError(" - ".join(errors))
227
236
  else:
228
237
  # JWT service is being provided locally
229
- reserved_claims["jti"] = str_random(size=16)
230
- reserved_claims["iat"] = just_now
231
- reserved_claims["exp"] = just_now + control_data.get("access-max-age")
232
238
  claims: dict[str, Any] = item_data.get("public-claims").copy()
233
239
  claims.update(m=reserved_claims)
234
240
  claims.update(m=custom_claims)
235
241
  # may raise an exception
236
242
  token: str = jwt.encode(payload=claims,
237
- 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")),
238
245
  algorithm=control_data.get("algorithm"))
239
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")
240
250
  control_data["access-token"] = token
241
251
 
242
252
  # return the token
243
253
  result = {
244
254
  "access_token": control_data.get("access-token"),
245
- "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")
246
257
  }
247
258
  else:
248
259
  # JWT access data not found
@@ -265,37 +276,46 @@ class JwtData:
265
276
  :raises InvalidTokenError: token is not valid
266
277
  :raises ExpiredSignatureError: token has expired
267
278
  """
268
- algorithm: str | None = None
269
- key: str | None = None
270
- with self.access_lock:
271
- for item_data in self.access_data:
272
- control_data: dict[str, Any] = item_data.get("control-data")
273
- if token == control_data.get("access-token"):
274
- algorithm = control_data.get("algorithm")
275
- key = control_data.get("public-key") or control_data.get("secret-key")
276
- break
277
-
278
- if not algorithm or not key:
279
- raise InvalidTokenError("JWT token is not valid")
279
+ # declare the return variable
280
+ result: dict[str, Any]
280
281
 
281
282
  if logger:
282
283
  logger.debug(msg=f"Retrieve claims for JWT token '{token}'")
283
- result: dict[str, Any] = jwt.decode(jwt=token,
284
- key=key,
285
- 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
+
286
300
  if logger:
287
301
  logger.debug(f"Retrieved claims for JWT token '{token}': {result}")
288
302
 
289
303
  return result
290
304
 
291
- def retrieve_access_data(self,
292
- account_id: str,
293
- 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]]:
294
309
  # noinspection HttpUrlsUsage
295
310
  """
296
- 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.
297
316
 
298
317
  :param account_id: the account identification
318
+ :param access_token: the access token
299
319
  :param logger: optional logger
300
320
  :return: the corresponding item in storage, or *None* if not found
301
321
  """
@@ -303,11 +323,13 @@ class JwtData:
303
323
  result: dict[str, dict[str, Any]] | None = None
304
324
 
305
325
  if logger:
306
- 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}")
307
328
  # retrieve the data
308
329
  with self.access_lock:
309
330
  for item_data in self.access_data:
310
- 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")):
311
333
  result = item_data
312
334
  break
313
335
  if logger:
@@ -322,7 +344,7 @@ def jwt_request_token(errors: list[str],
322
344
  timeout: int = None,
323
345
  logger: Logger = None) -> dict[str, Any]:
324
346
  """
325
- 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.
326
348
 
327
349
  Expected structure of the return data:
328
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.2
3
+ Version: 0.6.4
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.8
15
+ Requires-Dist: pypomes-core>=1.7.9
@@ -0,0 +1,7 @@
1
+ pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
2
+ pypomes_jwt/jwt_data.py,sha256=h_FbKrbnN9eLQYtzTQwCd1hZQYGCidHJy4rG9iBn8hs,19181
3
+ pypomes_jwt/jwt_pomes.py,sha256=U8Vc0IOlW5-XmRR_Px2xLlVit5oAHVnfBcNwDzwh_8I,14786
4
+ pypomes_jwt-0.6.4.dist-info/METADATA,sha256=vwG7SQieuLoTIRP2wqMCZ-DnK-XUqiajFnezKD5Bt1s,599
5
+ pypomes_jwt-0.6.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ pypomes_jwt-0.6.4.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
+ pypomes_jwt-0.6.4.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
2
- pypomes_jwt/jwt_data.py,sha256=auVG-sQJjeeQwAzwkaSV149_qHwLIYqoi-Aa0op9eI8,17830
3
- pypomes_jwt/jwt_pomes.py,sha256=93o0QC7Phsb_29KaLn9mlfE6nUw8HXadqpCZn-Q8gvI,13891
4
- pypomes_jwt-0.6.2.dist-info/METADATA,sha256=2rj4pjbMCHX_lOmN59UAMGscCesvMvYQgkzvcSCc50s,599
5
- pypomes_jwt-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.6.2.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.6.2.dist-info/RECORD,,