pypomes-jwt 0.6.0__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:
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import contextlib
2
- from cryptography.hazmat.backends import default_backend
3
2
  from cryptography.hazmat.primitives import serialization
4
3
  from cryptography.hazmat.primitives.asymmetric import rsa
5
4
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
@@ -18,8 +17,8 @@ JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_A
18
17
  JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
19
18
  def_value=43200)
20
19
  JWT_HS_SECRET_KEY: Final[bytes] = env_get_bytes(key=f"{APP_PREFIX}_JWT_HS_SECRET_KEY",
21
- def_value=token_bytes(32))
22
- # must invoke 'jwt_service()' below
20
+ def_value=token_bytes(nbytes=32))
21
+ # the endpoint must invoke 'jwt_service()' below
23
22
  JWT_ENDPOINT_URL: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_ENDPOINT_URL")
24
23
 
25
24
  # obtain a RSA private/public key pair
@@ -27,10 +26,9 @@ __priv_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
27
26
  __pub_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
28
27
  if not __priv_bytes or not __pub_bytes:
29
28
  __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
30
- key_size=2058,
31
- backend=default_backend())
29
+ key_size=2048)
32
30
  __priv_bytes = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
33
- format=serialization.PrivateFormat.TraditionalOpenSSL,
31
+ format=serialization.PrivateFormat.PKCS8,
34
32
  encryption_algorithm=serialization.NoEncryption())
35
33
  __pub_key: RSAPublicKey = __priv_key.public_key()
36
34
  __pub_bytes = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
@@ -59,21 +57,33 @@ def jwt_needed(func: callable) -> callable:
59
57
  return wrapper
60
58
 
61
59
 
62
- def jwt_set_service_access(reference_url: str,
63
- claims: dict[str, Any],
64
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
65
- access_max_age: int = JWT_ACCESS_MAX_AGE,
66
- refresh_max_age: int = JWT_REFRESH_MAX_AGE,
67
- secret_key: bytes = JWT_HS_SECRET_KEY,
68
- private_key: bytes = JWT_RSA_PRIVATE_KEY,
69
- public_key: bytes = JWT_RSA_PUBLIC_KEY,
70
- request_timeout: int = None,
71
- remote_provider: bool = True,
72
- logger: Logger = None) -> None:
60
+ def jwt_assert_access(account_id: str) -> bool:
73
61
  """
74
- Set the data needed to obtain JWT tokens from *reference_url*.
62
+ Determine whether access for *ccount_id* has been established.
75
63
 
76
- :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)
77
87
  :param claims: the JWT claimset, as key-value pairs
78
88
  :param algorithm: the authentication type
79
89
  :param access_max_age: token duration, in seconds
@@ -81,23 +91,24 @@ def jwt_set_service_access(reference_url: str,
81
91
  :param secret_key: secret key for HS authentication
82
92
  :param private_key: private key for RSA authentication
83
93
  :param public_key: public key for RSA authentication
84
- :param request_timeout: timeout for the requests to the service URL
94
+ :param request_timeout: timeout for the requests to the reference URL
85
95
  :param remote_provider: whether the JWT provider is a remote server
86
96
  :param logger: optional logger
87
97
  """
88
98
  if logger:
89
- logger.debug(msg=f"Register access data for '{reference_url}'")
90
- # 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
91
102
  pos: int = reference_url.find("?")
92
103
  if pos > 0:
93
- if remote_provider:
94
- params: list[str] = reference_url[pos+1:].split(sep="&")
95
- for param in params:
96
- 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]
97
107
  reference_url = reference_url[:pos]
98
108
 
99
109
  # register the JWT service
100
- __jwt_data.add_access_data(reference_url=reference_url,
110
+ __jwt_data.add_access_data(account_id=account_id,
111
+ reference_url=reference_url,
101
112
  claims=claims,
102
113
  algorithm=algorithm,
103
114
  access_max_age=access_max_age,
@@ -110,40 +121,40 @@ def jwt_set_service_access(reference_url: str,
110
121
  logger=logger)
111
122
 
112
123
 
113
- def jwt_remove_service_access(reference_url: str,
114
- logger: Logger = None) -> None:
124
+ def jwt_remove_access(account_id: str,
125
+ logger: Logger = None) -> None:
115
126
  """
116
- Remove from storage the JWT access data for *reference_url*.
127
+ Remove from storage the JWT access data for *account_id*.
117
128
 
118
- :param reference_url: the reference URL
129
+ :param account_id: the account identification
119
130
  :param logger: optional logger
120
131
  """
121
132
  if logger:
122
- logger.debug(msg=f"Remove access data for '{reference_url}'")
133
+ logger.debug(msg=f"Remove access data for '{account_id}'")
123
134
 
124
- __jwt_data.remove_access_data(reference_url=reference_url,
135
+ __jwt_data.remove_access_data(account_id=account_id,
125
136
  logger=logger)
126
137
 
127
138
 
128
139
  def jwt_get_token(errors: list[str],
129
- reference_url: str,
140
+ account_id: str,
130
141
  logger: Logger = None) -> str:
131
142
  """
132
- Obtain and return a JWT token from *reference_url*.
143
+ Obtain and return a JWT token for *account_id*.
133
144
 
134
145
  :param errors: incidental error messages
135
- :param reference_url: the reference URL
146
+ :param account_id: the account identification
136
147
  :param logger: optional logger
137
- :return: the JWT token, or 'None' if an error ocurred
148
+ :return: the JWT token, or *None* if an error ocurred
138
149
  """
139
150
  # inicialize the return variable
140
151
  result: str | None = None
141
152
 
142
153
  if logger:
143
- logger.debug(msg=f"Obtain a JWT token for '{reference_url}'")
154
+ logger.debug(msg=f"Obtain a JWT token for '{account_id}'")
144
155
 
145
156
  try:
146
- 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,
147
158
  logger=logger)
148
159
  result = token_data.get("access_token")
149
160
  if logger:
@@ -157,10 +168,10 @@ def jwt_get_token(errors: list[str],
157
168
 
158
169
 
159
170
  def jwt_get_token_data(errors: list[str],
160
- reference_url: str,
171
+ account_id: str,
161
172
  logger: Logger = None) -> dict[str, Any]:
162
173
  """
163
- 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.
164
175
 
165
176
  Structure of the return data:
166
177
  {
@@ -169,17 +180,17 @@ def jwt_get_token_data(errors: list[str],
169
180
  }
170
181
 
171
182
  :param errors: incidental error messages
172
- :param reference_url: the reference URL for obtaining JWT tokens
183
+ :param account_id: the account identification
173
184
  :param logger: optional logger
174
- :return: the JWT token data, or 'None' if error
185
+ :return: the JWT token data, or *None* if error
175
186
  """
176
187
  # inicialize the return variable
177
188
  result: dict[str, Any] | None = None
178
189
 
179
190
  if logger:
180
- logger.debug(msg=f"Retrieve JWT token data for '{reference_url}'")
191
+ logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
181
192
  try:
182
- result = __jwt_data.get_token_data(reference_url=reference_url,
193
+ result = __jwt_data.get_token_data(account_id=account_id,
183
194
  logger=logger)
184
195
  if logger:
185
196
  logger.debug(msg=f"Data is '{result}'")
@@ -191,11 +202,11 @@ def jwt_get_token_data(errors: list[str],
191
202
  return result
192
203
 
193
204
 
194
- def jwt_get_claims(errors: list[str],
195
- token: str,
196
- 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]:
197
208
  """
198
- Obtain and return the claimset of a JWT *token*.
209
+ Obtain and return the claims set of a JWT *token*.
199
210
 
200
211
  :param errors: incidental error messages
201
212
  :param token: the token to be inspected for claims
@@ -262,20 +273,22 @@ def jwt_verify_request(request: Request,
262
273
  return result
263
274
 
264
275
 
265
- def jwt_service(reference_url: str = None,
276
+ def jwt_service(account_id: str = None,
266
277
  service_params: dict[str, Any] = None,
267
278
  logger: Logger = None) -> Response:
268
279
  """
269
280
  Entry point for obtaining JWT tokens.
270
281
 
271
- In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request,
272
- a JSON containing:
282
+ In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request:
273
283
  {
274
- "reference-url": "<url>", - the JWT reference URL (if not as parameter)
275
- "<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
276
286
  ...
277
287
  "<custom-claim-key-n>": "<custom-claim-value-n>"
278
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
+
279
292
 
280
293
  Structure of the return data:
281
294
  {
@@ -283,7 +296,7 @@ def jwt_service(reference_url: str = None,
283
296
  "expires_in": <seconds-to-expiration>
284
297
  }
285
298
 
286
- :param reference_url: the JWT reference URL, alternatively passed in JSON
299
+ :param account_id: the account identification, alternatively passed in JSON
287
300
  :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
288
301
  :param logger: optional logger
289
302
  :return: the requested JWT token, along with its duration.
@@ -297,43 +310,39 @@ def jwt_service(reference_url: str = None,
297
310
  msg += f" from '{request.base_url}'"
298
311
  logger.debug(msg=msg)
299
312
 
300
- # obtain the parameters
313
+ # retrieve the parameters
301
314
  # noinspection PyUnusedLocal
302
315
  params: dict[str, Any] = service_params or {}
303
316
  if not params:
304
317
  with contextlib.suppress(Exception):
305
318
  params = request.get_json()
319
+ if not account_id:
320
+ account_id = params.get("account-id")
306
321
 
307
- # validate the parameters
308
- valid: bool = False
309
- if not reference_url:
310
- reference_url = params.get("reference-url")
311
- if reference_url:
322
+ # has the account been identified ?
323
+ if account_id:
324
+ # yes, proceed
312
325
  if logger:
313
- logger.debug(msg=f"Reference URL is '{reference_url}'")
314
- item_data: dict[str, dict[str, Any]] = __jwt_data.retrieve_access_data(reference_url=reference_url,
315
- logger=logger)
316
- if item_data:
317
- valid = True
318
- custom_claims: dict[str, Any] = item_data.get("custom-claims")
319
- for key, value in custom_claims.items():
320
- if key not in params or params.get(key) != value:
321
- valid = False
322
- break
323
-
324
- # obtain the token data
325
- 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
326
334
  try:
327
- 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,
328
336
  logger=logger)
329
337
  result = jsonify(token_data)
330
338
  except Exception as e:
331
- # validation failed
339
+ # token validation failed
332
340
  if logger:
333
341
  logger.error(msg=str(e))
334
342
  result = Response(response=str(e),
335
343
  status=401)
336
344
  else:
345
+ # no, report the problem
337
346
  if logger:
338
347
  logger.debug(msg=f"Invalid parameters {service_params}")
339
348
  result = Response(response="Invalid parameters",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.6.0
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.0
13
+ Requires-Dist: cryptography>=44.0.1
14
14
  Requires-Dist: pyjwt>=2.10.1
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=z-cQjhWDAtYs67CCxaPm-CeJFOF__OyupFIz0jWugiI,19001
3
- pypomes_jwt/jwt_pomes.py,sha256=kcoQDepMdeeriCe3oCkQfNctaNyDcIHmhY6uV5Ll6B8,13594
4
- pypomes_jwt-0.6.0.dist-info/METADATA,sha256=jECOxmllsm_0b4SxSQ_CV0SLTqR0PrIxUQXV9nk1Y-0,599
5
- pypomes_jwt-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.6.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.6.0.dist-info/RECORD,,