pypomes-jwt 0.6.0__py3-none-any.whl → 0.6.2__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,9 +1,9 @@
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
6
5
  from logging import Logger
6
+ from pypomes_core import str_random
7
7
  from requests import Response
8
8
  from threading import Lock
9
9
  from typing import Any, Literal
@@ -18,26 +18,35 @@ class JwtData:
18
18
  - access_data: list with dictionaries holding the JWT token data:
19
19
  [
20
20
  {
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
21
+ "reserved-claims": { # reserved claims
22
+ "exp": <timestamp>, # expiration time
23
+ "iat": <timestamp> # issued at
24
+ "iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
25
+ "jti": <string>, # JWT id
26
+ "sub": <string> # subject (the account identification)
27
+ # not used:
28
+ # "aud": <string> # audience
29
+ # "nbt": <timestamp> # not before time
27
30
  },
28
- "custom-claims": { # custom claims
31
+ "public-claims": {
32
+ "birthdate": <string>, # subject's birth date
33
+ "email": <string>, # subject's email
34
+ "gender": <string>, # subject's gender
35
+ "name": <string>, # subject's name
36
+ "roles": <List[str]> # subject roles
37
+ },
38
+ "custom-claims": { # custom claims
29
39
  "<custom-claim-key-1>": "<custom-claim-value-1>",
30
40
  ...
31
41
  "<custom-claim-key-n>": "<custom-claim-value-n>"
32
42
  },
33
43
  "control-data": { # control data
44
+ "remote-provider": <bool>, # whether the JWT provider is a remote server
34
45
  "access-token": <jwt-token>, # access token
35
46
  "algorithm": <string>, # HS256, HS512, RSA256, RSA512
36
- "request-timeout": <float>, # in seconds - defaults to no timeout
47
+ "request-timeout": <int>, # in seconds - defaults to no timeout
37
48
  "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
38
49
  "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
50
  "secret-key": <bytes>, # HS secret key
42
51
  "private-key": <bytes>, # RSA private key
43
52
  "public-key": <bytes>, # RSA public key
@@ -54,6 +63,7 @@ class JwtData:
54
63
  self.access_data: list[dict[str, dict[str, Any]]] = []
55
64
 
56
65
  def add_access_data(self,
66
+ account_id: str,
57
67
  reference_url: str,
58
68
  claims: dict[str, Any],
59
69
  algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
@@ -62,33 +72,37 @@ class JwtData:
62
72
  secret_key: bytes,
63
73
  private_key: bytes,
64
74
  public_key: bytes,
65
- request_timeout: float,
75
+ request_timeout: int,
66
76
  remote_provider: bool,
67
77
  logger: Logger = None) -> None:
68
78
  """
69
- Add to storage the parameters needed to obtain and validate JWT tokens for *reference_url*.
79
+ Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
80
+
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.
70
84
 
71
85
  Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
72
86
  (typically, an acess-key/secret-key pair), have been previously validated elsewhere.
73
87
  This situation might change in the future.
74
88
 
75
- :param reference_url: the reference URL
89
+ :param account_id: the account identification
90
+ :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
76
91
  :param claims: the JWT claimset, as key-value pairs
77
92
  :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
93
+ :param access_max_age: token duration (in seconds)
94
+ :param refresh_max_age: duration for the refresh operation (in seconds)
80
95
  :param secret_key: secret key for HS authentication
81
96
  :param private_key: private key for RSA authentication
82
97
  :param public_key: public key for RSA authentication
83
- :param request_timeout: timeout for the requests to the service URL
98
+ :param request_timeout: timeout for the requests to the reference URL
84
99
  :param remote_provider: whether the JWT provider is a remote server
85
100
  :param logger: optional logger
86
101
  """
87
102
  # Do the access data already exist ?
88
- if not self.assert_access_data(reference_url=reference_url):
103
+ if not self.retrieve_access_data(account_id=account_id):
89
104
  # no, build control data
90
105
  control_data: dict[str, Any] = {
91
- "reference-url": reference_url,
92
106
  "algorithm": algorithm,
93
107
  "access-max-age": access_max_age,
94
108
  "request-timeout": request_timeout,
@@ -102,55 +116,59 @@ class JwtData:
102
116
  control_data["public-key"] = public_key
103
117
 
104
118
  # build claims
119
+ reserved_claims: dict[str, Any] = {
120
+ "sub": account_id,
121
+ "iss": reference_url,
122
+ "exp": "<numeric-UTC-datetime>",
123
+ "iat": "<numeric-UTC-datetime>",
124
+ "jti": "<jwt-id>",
125
+ }
105
126
  custom_claims: dict[str, Any] = {}
106
- standard_claims: dict[str, Any] = {}
127
+ public_claims: dict[str, Any] = {}
107
128
  for key, value in claims.items():
108
- if key in ["nbt", "iss", "aud", "iat"]:
109
- standard_claims[key] = value
129
+ if key in ["birthdate", "email", "gender", "name", "roles"]:
130
+ public_claims[key] = value
110
131
  else:
111
132
  custom_claims[key] = value
112
- standard_claims["exp"] = datetime(year=2000,
113
- month=1,
114
- day=1,
115
- tzinfo=timezone.utc)
116
133
  # store access data
117
134
  item_data = {
118
135
  "control-data": control_data,
119
- "standard-claims": standard_claims,
136
+ "reserved-claims": reserved_claims,
137
+ "public-claims": public_claims,
120
138
  "custom-claims": custom_claims
121
139
  }
122
140
  with self.access_lock:
123
141
  self.access_data.append(item_data)
124
142
  if logger:
125
- logger.debug(f"JWT data added for '{reference_url}': {item_data}")
143
+ logger.debug(f"JWT data added for '{account_id}': {item_data}")
126
144
  elif logger:
127
- logger.warning(f"JWT data already exists for '{reference_url}'")
145
+ logger.warning(f"JWT data already exists for '{account_id}'")
128
146
 
129
147
  def remove_access_data(self,
130
- reference_url: str,
148
+ account_id: str,
131
149
  logger: Logger) -> None:
132
150
  """
133
- Remove from storage the access data for *reference_url*.
151
+ Remove from storage the access data for *account_id*.
134
152
 
135
- :param reference_url: the reference URL
153
+ :param account_id: the account identification
136
154
  :param logger: optional logger
137
155
  """
138
156
  # obtain the access data item in storage
139
- item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(reference_url=reference_url,
157
+ item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
140
158
  logger=logger)
141
159
  if item_data:
142
160
  with self.access_lock:
143
161
  self.access_data.remove(item_data)
144
162
  if logger:
145
- logger.debug(f"Removed JWT data for '{reference_url}'")
163
+ logger.debug(f"Removed JWT data for '{account_id}'")
146
164
  elif logger:
147
- logger.warning(f"No JWT data found for '{reference_url}'")
165
+ logger.warning(f"No JWT data found for '{account_id}'")
148
166
 
149
167
  def get_token_data(self,
150
- reference_url: str,
168
+ account_id: str,
151
169
  logger: Logger = None) -> dict[str, Any]:
152
170
  """
153
- Obtain and return the JWT token for *reference_url*, along with its duration.
171
+ Obtain and return the JWT token for *account_id*, along with its duration.
154
172
 
155
173
  Structure of the return data:
156
174
  {
@@ -158,9 +176,9 @@ class JwtData:
158
176
  "expires_in": <seconds-to-expiration>
159
177
  }
160
178
 
161
- :param reference_url: the reference URL for obtaining JWT tokens
179
+ :param account_id: the account identification
162
180
  :param logger: optional logger
163
- :return: the JWT token data, or 'None' if error
181
+ :return: the JWT token data, or *None* if error
164
182
  :raises InvalidTokenError: token is invalid
165
183
  :raises InvalidKeyError: authentication key is not in the proper format
166
184
  :raises ExpiredSignatureError: token and refresh period have expired
@@ -171,69 +189,64 @@ class JwtData:
171
189
  :raises InvalidIssuerError: 'iss' claim does not match the expected issuer
172
190
  :raises InvalidIssuedAtError: 'iat' claim is non-numeric
173
191
  :raises MissingRequiredClaimError: a required claim is not contained in the claimset
174
- :raises RuntimeError: access data not found for the given *reference_url*, or
192
+ :raises RuntimeError: access data not found for the given *account_id*, or
175
193
  the remote JWT provider failed to return a token
176
194
  """
177
195
  # declare the return variable
178
196
  result: dict[str, Any]
179
197
 
180
198
  # obtain the item in storage
181
- item_data: dict[str, Any] = self.retrieve_access_data(reference_url=reference_url,
199
+ item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
182
200
  logger=logger)
183
201
  # was the JWT data obtained ?
184
202
  if item_data:
185
203
  # yes, proceed
186
204
  control_data: dict[str, Any] = item_data.get("control-data")
205
+ reserved_claims: dict[str, Any] = item_data.get("reserved-claims")
187
206
  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)
207
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
197
208
 
209
+ # obtain a new token, if the current token has expired
210
+ if just_now > reserved_claims.get("exp"):
198
211
  # where is the locus of the JWT service provider ?
199
212
  if control_data.get("remote-provider"):
200
213
  # 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
214
  errors: list[str] = []
205
215
  result = jwt_request_token(errors=errors,
206
- reference_url=reference_url,
207
- claims=claims,
216
+ reference_url=reserved_claims.get("iss"),
217
+ claims=custom_claims,
208
218
  timeout=control_data.get("request-timeout"),
209
219
  logger=logger)
210
220
  if result:
211
221
  with self.access_lock:
212
222
  control_data["access-token"] = result.get("access_token")
213
223
  duration: int = result.get("expires_in")
214
- standard_claims["exp"] = just_now + timedelta(seconds=duration)
224
+ reserved_claims["exp"] = just_now + duration
215
225
  else:
216
226
  raise RuntimeError(" - ".join(errors))
217
227
  else:
218
228
  # JWT service is being provided locally
219
- claims["exp"] = just_now + timedelta(seconds=control_data.get("access-max-age") + 10)
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
+ claims: dict[str, Any] = item_data.get("public-claims").copy()
233
+ claims.update(m=reserved_claims)
234
+ claims.update(m=custom_claims)
220
235
  # may raise an exception
221
236
  token: str = jwt.encode(payload=claims,
222
237
  key=control_data.get("secret-key") or control_data.get("private-key"),
223
238
  algorithm=control_data.get("algorithm"))
224
239
  with self.access_lock:
225
240
  control_data["access-token"] = token
226
- standard_claims["exp"] = claims.get("exp")
227
241
 
228
242
  # return the token
229
- diff: timedelta = standard_claims.get("exp") - just_now - timedelta(seconds=10)
230
243
  result = {
231
244
  "access_token": control_data.get("access-token"),
232
- "expires_in": math.trunc(diff.total_seconds())
245
+ "expires_in": reserved_claims.get("exp") - just_now
233
246
  }
234
247
  else:
235
248
  # JWT access data not found
236
- err_msg: str = f"No JWT access data found for '{reference_url}'"
249
+ err_msg: str = f"No JWT access data found for '{account_id}'"
237
250
  if logger:
238
251
  logger.error(err_msg)
239
252
  raise RuntimeError(err_msg)
@@ -275,50 +288,14 @@ class JwtData:
275
288
 
276
289
  return result
277
290
 
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
291
  def retrieve_access_data(self,
311
- reference_url: str,
292
+ account_id: str,
312
293
  logger: Logger = None) -> dict[str, dict[str, Any]]:
313
294
  # noinspection HttpUrlsUsage
314
295
  """
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.
296
+ Retrieve and return the access data in storage for *account_id*.
320
297
 
321
- :param reference_url: the reference URL for obtaining JWT tokens
298
+ :param account_id: the account identification
322
299
  :param logger: optional logger
323
300
  :return: the corresponding item in storage, or *None* if not found
324
301
  """
@@ -326,19 +303,11 @@ class JwtData:
326
303
  result: dict[str, dict[str, Any]] | None = None
327
304
 
328
305
  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
-
306
+ logger.debug(f"Retrieve access data for account id '{account_id}'")
335
307
  # retrieve the data
336
308
  with self.access_lock:
337
309
  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:
310
+ if account_id == item_data.get("reserved-claims").get("sub"):
342
311
  result = item_data
343
312
  break
344
313
  if logger:
@@ -350,7 +319,7 @@ class JwtData:
350
319
  def jwt_request_token(errors: list[str],
351
320
  reference_url: str,
352
321
  claims: dict[str, Any],
353
- timeout: float = None,
322
+ timeout: int = None,
354
323
  logger: Logger = None) -> dict[str, Any]:
355
324
  """
356
325
  Obtain and return the JWT token associated with *reference_url*, along with its duration.
@@ -389,7 +358,7 @@ def jwt_request_token(errors: list[str],
389
358
  logger.debug(f"JWT token obtained: {result}")
390
359
  else:
391
360
  # no, report the problem
392
- err_msg: str = f"POST request of '{reference_url}' failed: {response.reason}"
361
+ err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
393
362
  if response.text:
394
363
  err_msg += f" - {response.text}"
395
364
  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.2
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.8
@@ -0,0 +1,7 @@
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,,
@@ -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,,