pypomes-jwt 0.5.9__py3-none-any.whl → 0.6.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 CHANGED
@@ -5,9 +5,9 @@ from .jwt_pomes import (
5
5
  JWT_ENDPOINT_URL,
6
6
  JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
7
7
  JWT_HS_SECRET_KEY, JWT_RSA_PRIVATE_KEY, JWT_RSA_PUBLIC_KEY,
8
- jwt_needed, jwt_verify_request,
9
- jwt_get_claims, jwt_get_token, jwt_get_token_data,
10
- jwt_service, jwt_set_service_access, jwt_remove_service_access
8
+ jwt_needed, jwt_verify_request, jwt_service,
9
+ jwt_get_token_claims, jwt_get_token, jwt_get_token_data,
10
+ jwt_assert_access, jwt_set_access, jwt_remove_access
11
11
  )
12
12
 
13
13
  __all__ = [
@@ -17,9 +17,9 @@ __all__ = [
17
17
  "JWT_ENDPOINT_URL",
18
18
  "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
19
19
  "JWT_HS_SECRET_KEY", "JWT_RSA_PRIVATE_KEY", "JWT_RSA_PUBLIC_KEY",
20
- "jwt_needed", "jwt_verify_request",
21
- "jwt_get_claims", "jwt_get_token", "jwt_get_token_data",
22
- "jwt_service", "jwt_set_service_access", "jwt_remove_service_access"
20
+ "jwt_needed", "jwt_verify_request", "jwt_service",
21
+ "jwt_get_token_claims", "jwt_get_token", "jwt_get_token_data",
22
+ "jwt_assert_access", "jwt_set_access", "jwt_remove_access"
23
23
  ]
24
24
 
25
25
  from importlib.metadata import version
pypomes_jwt/jwt_data.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import jwt
2
- import math
3
2
  import requests
4
3
  from datetime import datetime, timedelta, timezone
5
4
  from jwt.exceptions import InvalidTokenError
@@ -18,26 +17,35 @@ class JwtData:
18
17
  - access_data: list with dictionaries holding the JWT token data:
19
18
  [
20
19
  {
21
- "standard-claims": { # standard claims
22
- "exp": <timestamp>, # expiration time
23
- "nbt": <timestamp>, # not before time
24
- "iss": <string>, # issuer
25
- "aud": <string>, # audience
26
- "iat": <string> # issued at
20
+ "reserved-claims": { # reserved claims
21
+ "exp": <timestamp>, # expiration time
22
+ "iat": <timestamp> # issued at
23
+ "iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
24
+ "jti": <string>, # JWT id
25
+ "sub": <string> # subject (the account identification)
26
+ # not used:
27
+ # "aud": <string> # audience
28
+ # "nbt": <timestamp> # not before time
27
29
  },
28
- "custom-claims": { # custom claims
30
+ "public-claims": {
31
+ "birthdate": <string>, # subject's birth date
32
+ "email": <string>, # subject's email
33
+ "gender": <string>, # subject's gender
34
+ "name": <string>, # subject's name
35
+ "roles": <List[str]> # subject roles
36
+ },
37
+ "custom-claims": { # custom claims
29
38
  "<custom-claim-key-1>": "<custom-claim-value-1>",
30
39
  ...
31
40
  "<custom-claim-key-n>": "<custom-claim-value-n>"
32
41
  },
33
42
  "control-data": { # control data
43
+ "remote-provider": <bool>, # whether the JWT provider is a remote server
34
44
  "access-token": <jwt-token>, # access token
35
45
  "algorithm": <string>, # HS256, HS512, RSA256, RSA512
36
- "request-timeout": <float>, # in seconds - defaults to no timeout
46
+ "request-timeout": <int>, # in seconds - defaults to no timeout
37
47
  "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
38
48
  "refresh-exp": <timestamp>, # expiration time for the refresh operation
39
- "reference-url": <url>, # URL to obtain and validate the access tokens
40
- "remote-provider": <bool>, # whether the JWT provider is a remote server
41
49
  "secret-key": <bytes>, # HS secret key
42
50
  "private-key": <bytes>, # RSA private key
43
51
  "public-key": <bytes>, # RSA public key
@@ -54,6 +62,7 @@ class JwtData:
54
62
  self.access_data: list[dict[str, dict[str, Any]]] = []
55
63
 
56
64
  def add_access_data(self,
65
+ account_id: str,
57
66
  reference_url: str,
58
67
  claims: dict[str, Any],
59
68
  algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
@@ -62,33 +71,37 @@ class JwtData:
62
71
  secret_key: bytes,
63
72
  private_key: bytes,
64
73
  public_key: bytes,
65
- request_timeout: float,
74
+ request_timeout: int,
66
75
  remote_provider: bool,
67
76
  logger: Logger = None) -> None:
68
77
  """
69
- Add to storage the parameters needed to obtain and validate JWT tokens for *reference_url*.
78
+ Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
79
+
80
+ 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.
70
83
 
71
84
  Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
72
85
  (typically, an acess-key/secret-key pair), have been previously validated elsewhere.
73
86
  This situation might change in the future.
74
87
 
75
- :param reference_url: the reference URL
88
+ :param account_id: the account identification
89
+ :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
76
90
  :param claims: the JWT claimset, as key-value pairs
77
91
  :param algorithm: the algorithm used to sign the token with
78
- :param access_max_age: token duration
79
- :param refresh_max_age: duration for the refresh operation
92
+ :param access_max_age: token duration (in seconds)
93
+ :param refresh_max_age: duration for the refresh operation (in seconds)
80
94
  :param secret_key: secret key for HS authentication
81
95
  :param private_key: private key for RSA authentication
82
96
  :param public_key: public key for RSA authentication
83
- :param request_timeout: timeout for the requests to the service URL
97
+ :param request_timeout: timeout for the requests to the reference URL
84
98
  :param remote_provider: whether the JWT provider is a remote server
85
99
  :param logger: optional logger
86
100
  """
87
101
  # Do the access data already exist ?
88
- if not self.assert_access_data(reference_url=reference_url):
102
+ if not self.retrieve_access_data(account_id=account_id):
89
103
  # no, build control data
90
104
  control_data: dict[str, Any] = {
91
- "reference-url": reference_url,
92
105
  "algorithm": algorithm,
93
106
  "access-max-age": access_max_age,
94
107
  "request-timeout": request_timeout,
@@ -102,55 +115,59 @@ class JwtData:
102
115
  control_data["public-key"] = public_key
103
116
 
104
117
  # build claims
118
+ reserved_claims: dict[str, Any] = {
119
+ "sub": account_id,
120
+ "iss": reference_url,
121
+ "exp": "<numeric-UTC-datetime>",
122
+ "iat": "<numeric-UTC-datetime>",
123
+ "jti": "<jwt-id",
124
+ }
105
125
  custom_claims: dict[str, Any] = {}
106
- standard_claims: dict[str, Any] = {}
126
+ public_claims: dict[str, Any] = {}
107
127
  for key, value in claims.items():
108
- if key in ["nbt", "iss", "aud", "iat"]:
109
- standard_claims[key] = value
128
+ if key in ["birthdate", "email", "gender", "name", "roles"]:
129
+ public_claims[key] = value
110
130
  else:
111
131
  custom_claims[key] = value
112
- standard_claims["exp"] = datetime(year=2000,
113
- month=1,
114
- day=1,
115
- tzinfo=timezone.utc)
116
132
  # store access data
117
133
  item_data = {
118
134
  "control-data": control_data,
119
- "standard-claims": standard_claims,
135
+ "reserved-claims": reserved_claims,
136
+ "public-claims": public_claims,
120
137
  "custom-claims": custom_claims
121
138
  }
122
139
  with self.access_lock:
123
140
  self.access_data.append(item_data)
124
141
  if logger:
125
- logger.debug(f"JWT data added for '{reference_url}': {item_data}")
142
+ logger.debug(f"JWT data added for '{account_id}': {item_data}")
126
143
  elif logger:
127
- logger.warning(f"JWT data already exists for '{reference_url}'")
144
+ logger.warning(f"JWT data already exists for '{account_id}'")
128
145
 
129
146
  def remove_access_data(self,
130
- reference_url: str,
147
+ account_id: str,
131
148
  logger: Logger) -> None:
132
149
  """
133
- Remove from storage the access data for *reference_url*.
150
+ Remove from storage the access data for *account_id*.
134
151
 
135
- :param reference_url: the reference URL
152
+ :param account_id: the account identification
136
153
  :param logger: optional logger
137
154
  """
138
155
  # obtain the access data item in storage
139
- item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(reference_url=reference_url,
156
+ item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
140
157
  logger=logger)
141
158
  if item_data:
142
159
  with self.access_lock:
143
160
  self.access_data.remove(item_data)
144
161
  if logger:
145
- logger.debug(f"Removed JWT data for '{reference_url}'")
162
+ logger.debug(f"Removed JWT data for '{account_id}'")
146
163
  elif logger:
147
- logger.warning(f"No JWT data found for '{reference_url}'")
164
+ logger.warning(f"No JWT data found for '{account_id}'")
148
165
 
149
166
  def get_token_data(self,
150
- reference_url: str,
167
+ account_id: str,
151
168
  logger: Logger = None) -> dict[str, Any]:
152
169
  """
153
- Obtain and return the JWT token for *reference_url*, along with its duration.
170
+ Obtain and return the JWT token for *account_id*, along with its duration.
154
171
 
155
172
  Structure of the return data:
156
173
  {
@@ -158,9 +175,9 @@ class JwtData:
158
175
  "expires_in": <seconds-to-expiration>
159
176
  }
160
177
 
161
- :param reference_url: the reference URL for obtaining JWT tokens
178
+ :param account_id: the account identification
162
179
  :param logger: optional logger
163
- :return: the JWT token data, or 'None' if error
180
+ :return: the JWT token data, or *None* if error
164
181
  :raises InvalidTokenError: token is invalid
165
182
  :raises InvalidKeyError: authentication key is not in the proper format
166
183
  :raises ExpiredSignatureError: token and refresh period have expired
@@ -171,69 +188,63 @@ class JwtData:
171
188
  :raises InvalidIssuerError: 'iss' claim does not match the expected issuer
172
189
  :raises InvalidIssuedAtError: 'iat' claim is non-numeric
173
190
  :raises MissingRequiredClaimError: a required claim is not contained in the claimset
174
- :raises RuntimeError: access data not found for the given *reference_url*, or
191
+ :raises RuntimeError: access data not found for the given *account_id*, or
175
192
  the remote JWT provider failed to return a token
176
193
  """
177
194
  # declare the return variable
178
195
  result: dict[str, Any]
179
196
 
180
197
  # obtain the item in storage
181
- item_data: dict[str, Any] = self.retrieve_access_data(reference_url=reference_url,
198
+ item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
182
199
  logger=logger)
183
200
  # was the JWT data obtained ?
184
201
  if item_data:
185
202
  # yes, proceed
186
203
  control_data: dict[str, Any] = item_data.get("control-data")
204
+ reserved_claims: dict[str, Any] = item_data.get("reserved-claims")
187
205
  custom_claims: dict[str, Any] = item_data.get("custom-claims")
188
- standard_claims: dict[str, Any] = item_data.get("standard-claims")
189
- just_now: datetime = datetime.now(tz=timezone.utc)
190
-
191
- # is the current token still valid ?
192
- if just_now > standard_claims.get("exp"):
193
- # no, obtain a new token
194
- reference_url: str = control_data.get("reference-url")
195
- claims: dict[str, Any] = standard_claims.copy()
196
- claims.update(custom_claims)
206
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
197
207
 
208
+ # obtain a new token, if the current token has expired
209
+ if just_now > reserved_claims.get("exp"):
198
210
  # where is the locus of the JWT service provider ?
199
211
  if control_data.get("remote-provider"):
200
212
  # JWT service is being provided by a remote server
201
- if reference_url.find("?") > 0:
202
- reference_url = reference_url[:reference_url.index("?")]
203
- claims.pop("exp", None)
204
213
  errors: list[str] = []
205
214
  result = jwt_request_token(errors=errors,
206
- reference_url=reference_url,
207
- claims=claims,
215
+ reference_url=reserved_claims.get("iss"),
216
+ claims=custom_claims,
208
217
  timeout=control_data.get("request-timeout"),
209
218
  logger=logger)
210
219
  if result:
211
220
  with self.access_lock:
212
221
  control_data["access-token"] = result.get("access_token")
213
222
  duration: int = result.get("expires_in")
214
- standard_claims["exp"] = just_now + timedelta(seconds=duration)
223
+ reserved_claims["exp"] = just_now + duration
215
224
  else:
216
225
  raise RuntimeError(" - ".join(errors))
217
226
  else:
218
227
  # JWT service is being provided locally
219
- claims["exp"] = just_now + timedelta(seconds=control_data.get("access-max-age") + 10)
228
+ reserved_claims["iat"] = just_now
229
+ reserved_claims["exp"] = just_now + control_data.get("access-max-age")
230
+ claims: dict[str, Any] = item_data.get("public-claims").copy()
231
+ claims.update(reserved_claims)
232
+ claims.update(custom_claims)
220
233
  # may raise an exception
221
234
  token: str = jwt.encode(payload=claims,
222
235
  key=control_data.get("secret-key") or control_data.get("private-key"),
223
236
  algorithm=control_data.get("algorithm"))
224
237
  with self.access_lock:
225
238
  control_data["access-token"] = token
226
- standard_claims["exp"] = claims.get("exp")
227
239
 
228
240
  # return the token
229
- diff: timedelta = standard_claims.get("exp") - just_now - timedelta(seconds=10)
230
241
  result = {
231
242
  "access_token": control_data.get("access-token"),
232
- "expires_in": math.trunc(diff.total_seconds())
243
+ "expires_in": reserved_claims.get("exp") - just_now
233
244
  }
234
245
  else:
235
246
  # JWT access data not found
236
- err_msg: str = f"No JWT access data found for '{reference_url}'"
247
+ err_msg: str = f"No JWT access data found for '{account_id}'"
237
248
  if logger:
238
249
  logger.error(err_msg)
239
250
  raise RuntimeError(err_msg)
@@ -275,50 +286,14 @@ class JwtData:
275
286
 
276
287
  return result
277
288
 
278
- def assert_access_data(self,
279
- reference_url: str) -> bool:
280
- # noinspection HttpUrlsUsage
281
- """
282
- Assert whether access data exists for *reference_url*.
283
-
284
- For the purpose of locating access data, Protocol indication in *reference_url*
285
- (typically, *http://* or *https://*), is disregarded. This guarantees
286
- that processing herein will not be affected by in-transit protocol changes.
287
-
288
- :param reference_url: the reference URL for obtaining JWT tokens
289
- :return: *True" is access data is in storage, *False* otherwise
290
- """
291
- # initialize the return variable
292
- result: bool = False
293
-
294
- # disregard protocol
295
- if reference_url.find("://") > 0:
296
- reference_url = reference_url[reference_url.index("://")+3:]
297
-
298
- # assert the data
299
- with self.access_lock:
300
- for item_data in self.access_data:
301
- item_url: str = item_data.get("control-data").get("reference-url")
302
- if item_url.find("://") > 0:
303
- item_url = item_url[item_url.index("://")+3:]
304
- if reference_url == item_url:
305
- result = True
306
- break
307
-
308
- return result
309
-
310
289
  def retrieve_access_data(self,
311
- reference_url: str,
290
+ account_id: str,
312
291
  logger: Logger = None) -> dict[str, dict[str, Any]]:
313
292
  # noinspection HttpUrlsUsage
314
293
  """
315
- Retrieve and return the access data in storage for *reference_url*.
316
-
317
- For the purpose of locating access data, Protocol indication in *reference_url*
318
- (typically, *http://* or *https://*), is disregarded. This guarantees
319
- that processing herein will not be affected by in-transit protocol changes.
294
+ Retrieve and return the access data in storage for *account_id*.
320
295
 
321
- :param reference_url: the reference URL for obtaining JWT tokens
296
+ :param account_id: the account identification
322
297
  :param logger: optional logger
323
298
  :return: the corresponding item in storage, or *None* if not found
324
299
  """
@@ -326,19 +301,11 @@ class JwtData:
326
301
  result: dict[str, dict[str, Any]] | None = None
327
302
 
328
303
  if logger:
329
- logger.debug(f"Retrieve access data for reference URL '{reference_url}'")
330
-
331
- # disregard protocol
332
- if reference_url.find("://") > 0:
333
- reference_url = reference_url[reference_url.index("://")+3:]
334
-
304
+ logger.debug(f"Retrieve access data for account id '{account_id}'")
335
305
  # retrieve the data
336
306
  with self.access_lock:
337
307
  for item_data in self.access_data:
338
- item_url: str = item_data.get("control-data").get("reference-url")
339
- if item_url.find("://") > 0:
340
- item_url = item_url[item_url.index("://")+3:]
341
- if reference_url == item_url:
308
+ if account_id == item_data.get("reserved-claims").get("sub"):
342
309
  result = item_data
343
310
  break
344
311
  if logger:
@@ -350,7 +317,7 @@ class JwtData:
350
317
  def jwt_request_token(errors: list[str],
351
318
  reference_url: str,
352
319
  claims: dict[str, Any],
353
- timeout: float = None,
320
+ timeout: int = None,
354
321
  logger: Logger = None) -> dict[str, Any]:
355
322
  """
356
323
  Obtain and return the JWT token associated with *reference_url*, along with its duration.
@@ -389,7 +356,7 @@ def jwt_request_token(errors: list[str],
389
356
  logger.debug(f"JWT token obtained: {result}")
390
357
  else:
391
358
  # no, report the problem
392
- err_msg: str = f"POST request of '{reference_url}' failed: {response.reason}"
359
+ err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
393
360
  if response.text:
394
361
  err_msg += f" - {response.text}"
395
362
  if logger:
@@ -418,7 +385,9 @@ def jwt_validate_token(token: str,
418
385
  :raises InvalidSignatureError: signature does not match the one provided as part of the token
419
386
  """
420
387
  if logger:
421
- logger.debug(msg=f"Verify request for JWT token '{token}'")
388
+ logger.debug(msg=f"Validate JWT token '{token}'")
422
389
  jwt.decode(jwt=token,
423
390
  key=key,
424
391
  algorithms=[algorithm])
392
+ if logger:
393
+ logger.debug(msg=f"Token '{token}' is valid")
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import contextlib
2
+ from cryptography.hazmat.primitives import serialization
3
+ from cryptography.hazmat.primitives.asymmetric import rsa
4
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
2
5
  from flask import Request, Response, request, jsonify
3
6
  from logging import Logger
4
- from OpenSSL import crypto
5
7
  from pypomes_core import APP_PREFIX, env_get_str, env_get_bytes, env_get_int
6
8
  from secrets import token_bytes
7
9
  from typing import Any, Final, Literal
@@ -15,18 +17,24 @@ JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_A
15
17
  JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
16
18
  def_value=43200)
17
19
  JWT_HS_SECRET_KEY: Final[bytes] = env_get_bytes(key=f"{APP_PREFIX}_JWT_HS_SECRET_KEY",
18
- def_value=token_bytes(32))
19
- # must invoke 'jwt_service()' below
20
+ def_value=token_bytes(nbytes=32))
21
+ # the endpoint must invoke 'jwt_service()' below
20
22
  JWT_ENDPOINT_URL: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_ENDPOINT_URL")
21
23
 
22
- __priv_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
23
- __pub_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
24
- if not __priv_key or not __pub_key:
25
- pk = crypto.PKey()
26
- __priv_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, pk)
27
- __pub_key = crypto.dump_publickey(crypto.FILETYPE_PEM, pk)
28
- JWT_RSA_PRIVATE_KEY: Final[bytes] = __priv_key
29
- JWT_RSA_PUBLIC_KEY: Final[bytes] = __pub_key
24
+ # obtain a RSA private/public key pair
25
+ __priv_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
26
+ __pub_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
27
+ if not __priv_bytes or not __pub_bytes:
28
+ __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
29
+ key_size=2048)
30
+ __priv_bytes = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
31
+ format=serialization.PrivateFormat.PKCS8,
32
+ encryption_algorithm=serialization.NoEncryption())
33
+ __pub_key: RSAPublicKey = __priv_key.public_key()
34
+ __pub_bytes = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
35
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
36
+ JWT_RSA_PRIVATE_KEY: Final[bytes] = __priv_bytes
37
+ JWT_RSA_PUBLIC_KEY: Final[bytes] = __pub_bytes
30
38
 
31
39
  # the JWT data object
32
40
  __jwt_data: JwtData = JwtData()
@@ -49,21 +57,33 @@ def jwt_needed(func: callable) -> callable:
49
57
  return wrapper
50
58
 
51
59
 
52
- def jwt_set_service_access(reference_url: str,
53
- claims: dict[str, Any],
54
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
55
- access_max_age: int = JWT_ACCESS_MAX_AGE,
56
- refresh_max_age: int = JWT_REFRESH_MAX_AGE,
57
- secret_key: bytes = JWT_HS_SECRET_KEY,
58
- private_key: bytes = JWT_RSA_PRIVATE_KEY,
59
- public_key: bytes = JWT_RSA_PUBLIC_KEY,
60
- request_timeout: int = None,
61
- remote_provider: bool = True,
62
- logger: Logger = None) -> None:
60
+ def jwt_assert_access(account_id: str) -> bool:
63
61
  """
64
- Set the data needed to obtain JWT tokens from *reference_url*.
62
+ Determine whether access for *ccount_id* has been established.
65
63
 
66
- :param reference_url: the reference URL
64
+ :param account_id: the account identification
65
+ :return: *True* if access data exists for *account_id*, *False* otherwise
66
+ """
67
+ return __jwt_data.retrieve_access_data(account_id=account_id) is not None
68
+
69
+
70
+ def jwt_set_access(account_id: str,
71
+ reference_url: str,
72
+ claims: dict[str, Any],
73
+ algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
74
+ access_max_age: int = JWT_ACCESS_MAX_AGE,
75
+ refresh_max_age: int = JWT_REFRESH_MAX_AGE,
76
+ secret_key: bytes = JWT_HS_SECRET_KEY,
77
+ private_key: bytes = JWT_RSA_PRIVATE_KEY,
78
+ public_key: bytes = JWT_RSA_PUBLIC_KEY,
79
+ request_timeout: int = None,
80
+ remote_provider: bool = True,
81
+ logger: Logger = None) -> None:
82
+ """
83
+ Set the data needed to obtain JWT tokens for *account_id*.
84
+
85
+ :param account_id: the account identification
86
+ :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
67
87
  :param claims: the JWT claimset, as key-value pairs
68
88
  :param algorithm: the authentication type
69
89
  :param access_max_age: token duration, in seconds
@@ -71,23 +91,24 @@ def jwt_set_service_access(reference_url: str,
71
91
  :param secret_key: secret key for HS authentication
72
92
  :param private_key: private key for RSA authentication
73
93
  :param public_key: public key for RSA authentication
74
- :param request_timeout: timeout for the requests to the service URL
94
+ :param request_timeout: timeout for the requests to the reference URL
75
95
  :param remote_provider: whether the JWT provider is a remote server
76
96
  :param logger: optional logger
77
97
  """
78
98
  if logger:
79
- logger.debug(msg=f"Register access data for '{reference_url}'")
80
- # extract the extra claims
99
+ logger.debug(msg=f"Register access data for '{account_id}'")
100
+
101
+ # extract the claims provided in the reference URL's query string
81
102
  pos: int = reference_url.find("?")
82
103
  if pos > 0:
83
- if remote_provider:
84
- params: list[str] = reference_url[pos+1:].split(sep="&")
85
- for param in params:
86
- claims[param.split("=")[0]] = param.split("=")[1]
104
+ params: list[str] = reference_url[pos+1:].split(sep="&")
105
+ for param in params:
106
+ claims[param.split("=")[0]] = param.split("=")[1]
87
107
  reference_url = reference_url[:pos]
88
108
 
89
109
  # register the JWT service
90
- __jwt_data.add_access_data(reference_url=reference_url,
110
+ __jwt_data.add_access_data(account_id=account_id,
111
+ reference_url=reference_url,
91
112
  claims=claims,
92
113
  algorithm=algorithm,
93
114
  access_max_age=access_max_age,
@@ -100,40 +121,40 @@ def jwt_set_service_access(reference_url: str,
100
121
  logger=logger)
101
122
 
102
123
 
103
- def jwt_remove_service_access(reference_url: str,
104
- logger: Logger = None) -> None:
124
+ def jwt_remove_access(account_id: str,
125
+ logger: Logger = None) -> None:
105
126
  """
106
- Remove from storage the JWT access data for *reference_url*.
127
+ Remove from storage the JWT access data for *account_id*.
107
128
 
108
- :param reference_url: the reference URL
129
+ :param account_id: the account identification
109
130
  :param logger: optional logger
110
131
  """
111
132
  if logger:
112
- logger.debug(msg=f"Remove access data for '{reference_url}'")
133
+ logger.debug(msg=f"Remove access data for '{account_id}'")
113
134
 
114
- __jwt_data.remove_access_data(reference_url=reference_url,
135
+ __jwt_data.remove_access_data(account_id=account_id,
115
136
  logger=logger)
116
137
 
117
138
 
118
139
  def jwt_get_token(errors: list[str],
119
- reference_url: str,
140
+ account_id: str,
120
141
  logger: Logger = None) -> str:
121
142
  """
122
- Obtain and return a JWT token from *reference_url*.
143
+ Obtain and return a JWT token for *account_id*.
123
144
 
124
145
  :param errors: incidental error messages
125
- :param reference_url: the reference URL
146
+ :param account_id: the account identification
126
147
  :param logger: optional logger
127
- :return: the JWT token, or 'None' if an error ocurred
148
+ :return: the JWT token, or *None* if an error ocurred
128
149
  """
129
150
  # inicialize the return variable
130
151
  result: str | None = None
131
152
 
132
153
  if logger:
133
- logger.debug(msg=f"Obtain a JWT token for '{reference_url}'")
154
+ logger.debug(msg=f"Obtain a JWT token for '{account_id}'")
134
155
 
135
156
  try:
136
- token_data: dict[str, Any] = __jwt_data.get_token_data(reference_url=reference_url,
157
+ token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
137
158
  logger=logger)
138
159
  result = token_data.get("access_token")
139
160
  if logger:
@@ -147,10 +168,10 @@ def jwt_get_token(errors: list[str],
147
168
 
148
169
 
149
170
  def jwt_get_token_data(errors: list[str],
150
- reference_url: str,
171
+ account_id: str,
151
172
  logger: Logger = None) -> dict[str, Any]:
152
173
  """
153
- Obtain and return the JWT token associated with *reference_url*, along with its duration.
174
+ Obtain and return the JWT token associated with *account_id*, along with its duration.
154
175
 
155
176
  Structure of the return data:
156
177
  {
@@ -159,17 +180,17 @@ def jwt_get_token_data(errors: list[str],
159
180
  }
160
181
 
161
182
  :param errors: incidental error messages
162
- :param reference_url: the reference URL for obtaining JWT tokens
183
+ :param account_id: the account identification
163
184
  :param logger: optional logger
164
- :return: the JWT token data, or 'None' if error
185
+ :return: the JWT token data, or *None* if error
165
186
  """
166
187
  # inicialize the return variable
167
188
  result: dict[str, Any] | None = None
168
189
 
169
190
  if logger:
170
- logger.debug(msg=f"Retrieve JWT token data for '{reference_url}'")
191
+ logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
171
192
  try:
172
- result = __jwt_data.get_token_data(reference_url=reference_url,
193
+ result = __jwt_data.get_token_data(account_id=account_id,
173
194
  logger=logger)
174
195
  if logger:
175
196
  logger.debug(msg=f"Data is '{result}'")
@@ -181,11 +202,11 @@ def jwt_get_token_data(errors: list[str],
181
202
  return result
182
203
 
183
204
 
184
- def jwt_get_claims(errors: list[str],
185
- token: str,
186
- logger: Logger = None) -> dict[str, Any]:
205
+ def jwt_get_token_claims(errors: list[str],
206
+ token: str,
207
+ logger: Logger = None) -> dict[str, Any]:
187
208
  """
188
- Obtain and return the claimset of a JWT *token*.
209
+ Obtain and return the claims set of a JWT *token*.
189
210
 
190
211
  :param errors: incidental error messages
191
212
  :param token: the token to be inspected for claims
@@ -252,20 +273,22 @@ def jwt_verify_request(request: Request,
252
273
  return result
253
274
 
254
275
 
255
- def jwt_service(reference_url: str = None,
276
+ def jwt_service(account_id: str = None,
256
277
  service_params: dict[str, Any] = None,
257
278
  logger: Logger = None) -> Response:
258
279
  """
259
280
  Entry point for obtaining JWT tokens.
260
281
 
261
- In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request,
262
- a JSON containing:
282
+ In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request:
263
283
  {
264
- "reference-url": "<url>", - the JWT reference URL (if not as parameter)
265
- "<custom-claim-key-1>": "<custom-claim-value-1>", - the registered custom claims
284
+ "account-id": "<string>" - required account identification
285
+ "<custom-claim-key-1>": "<custom-claim-value-1>", - optional custom claims
266
286
  ...
267
287
  "<custom-claim-key-n>": "<custom-claim-value-n>"
268
288
  }
289
+ If provided, the additional custom claims will be sent to the remote provider, if applicable
290
+ (custom claims currently registered for the account may be overridden).
291
+
269
292
 
270
293
  Structure of the return data:
271
294
  {
@@ -273,7 +296,7 @@ def jwt_service(reference_url: str = None,
273
296
  "expires_in": <seconds-to-expiration>
274
297
  }
275
298
 
276
- :param reference_url: the JWT reference URL, alternatively passed in JSON
299
+ :param account_id: the account identification, alternatively passed in JSON
277
300
  :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
278
301
  :param logger: optional logger
279
302
  :return: the requested JWT token, along with its duration.
@@ -287,43 +310,39 @@ def jwt_service(reference_url: str = None,
287
310
  msg += f" from '{request.base_url}'"
288
311
  logger.debug(msg=msg)
289
312
 
290
- # obtain the parameters
313
+ # retrieve the parameters
291
314
  # noinspection PyUnusedLocal
292
315
  params: dict[str, Any] = service_params or {}
293
316
  if not params:
294
317
  with contextlib.suppress(Exception):
295
318
  params = request.get_json()
319
+ if not account_id:
320
+ account_id = params.get("account-id")
296
321
 
297
- # validate the parameters
298
- valid: bool = False
299
- if not reference_url:
300
- reference_url = params.get("reference-url")
301
- if reference_url:
322
+ # has the account been identified ?
323
+ if account_id:
324
+ # yes, proceed
302
325
  if logger:
303
- logger.debug(msg=f"Reference URL is '{reference_url}'")
304
- item_data: dict[str, dict[str, Any]] = __jwt_data.retrieve_access_data(reference_url=reference_url,
305
- logger=logger)
306
- if item_data:
307
- valid = True
308
- custom_claims: dict[str, Any] = item_data.get("custom-claims")
309
- for key, value in custom_claims.items():
310
- if key not in params or params.get(key) != value:
311
- valid = False
312
- break
313
-
314
- # obtain the token data
315
- if valid:
326
+ 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 {}
329
+ custom_claims: dict[str, Any] = item_data.get("custom-claims").copy()
330
+ for key, value in params.items():
331
+ custom_claims[key] = value
332
+
333
+ # obtain the token data
316
334
  try:
317
- token_data: dict[str, Any] = __jwt_data.get_token_data(reference_url=reference_url,
335
+ token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
318
336
  logger=logger)
319
337
  result = jsonify(token_data)
320
338
  except Exception as e:
321
- # validation failed
339
+ # token validation failed
322
340
  if logger:
323
341
  logger.error(msg=str(e))
324
342
  result = Response(response=str(e),
325
343
  status=401)
326
344
  else:
345
+ # no, report the problem
327
346
  if logger:
328
347
  logger.debug(msg=f"Invalid parameters {service_params}")
329
348
  result = Response(response="Invalid parameters",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.5.9
3
+ Version: 0.6.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
@@ -10,6 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
+ Requires-Dist: cryptography>=44.0.1
13
14
  Requires-Dist: pyjwt>=2.10.1
14
- Requires-Dist: pyopenssl>=25.0.0
15
- Requires-Dist: pypomes-core>=1.7.1
15
+ Requires-Dist: pypomes-core>=1.7.7
@@ -0,0 +1,7 @@
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,,
@@ -1,7 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=1IyBb94cZjkXMibHrH_vh043b06QFh5UQ6HTYSDau28,978
2
- pypomes_jwt/jwt_data.py,sha256=6Y3a_GoiLy9zailSmYvN144Sbx1Rffrj_x3Hhp13iUQ,18940
3
- pypomes_jwt/jwt_pomes.py,sha256=UgqRWdgOEu4GzjNIsDUGtWzY-JQc3JUaHMOZs07ofeQ,12713
4
- pypomes_jwt-0.5.9.dist-info/METADATA,sha256=Xw7H7sCKKy64-vWYMLVhHc9t7_wRyx4Gtvjq4UhU-Uo,596
5
- pypomes_jwt-0.5.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.5.9.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.5.9.dist-info/RECORD,,