pypomes-jwt 0.9.0__tar.gz → 0.9.2__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.9.0
3
+ Version: 0.9.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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_jwt"
9
- version = "0.9.0"
9
+ version = "0.9.2"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -3,13 +3,14 @@ from .jwt_constants import (
3
3
  JWT_DB_PORT, JWT_DB_USER, JWT_DB_PWD,
4
4
  JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
5
5
  JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN,
6
- JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
7
- JWT_ENCODING_KEY, JWT_DECODING_KEY
6
+ JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
7
+ JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
8
8
  )
9
9
  from .jwt_pomes import (
10
10
  jwt_needed, jwt_verify_request,
11
11
  jwt_assert_account, jwt_set_account, jwt_remove_account,
12
- jwt_get_tokens, jwt_get_claims, jwt_validate_token, jwt_revoke_token
12
+ jwt_issue_token, jwt_issue_tokens, jwt_get_claims,
13
+ jwt_validate_token, jwt_revoke_token
13
14
  )
14
15
 
15
16
  __all__ = [
@@ -18,12 +19,13 @@ __all__ = [
18
19
  "JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
19
20
  "JWT_DB_TABLE", "JWT_DB_COL_KID", "JWT_DB_COL_ACCOUNT",
20
21
  "JWT_DB_COL_ALGORITHM", "JWT_DB_COL_DECODER", "JWT_DB_COL_TOKEN",
22
+ "JWT_ACCOUNT_LIMIT", "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
21
23
  "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
22
- "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
23
24
  # jwt_pomes
24
25
  "jwt_needed", "jwt_verify_request",
25
26
  "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
26
- "jwt_get_tokens", "jwt_get_claims", "jwt_validate_token", "jwt_revoke_token"
27
+ "jwt_issue_token", "jwt_issue_tokens", "jwt_get_claims",
28
+ "jwt_validate_token", "jwt_revoke_token"
27
29
  ]
28
30
 
29
31
  from importlib.metadata import version
@@ -11,10 +11,10 @@ from .jwt_constants import (
11
11
  JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
12
12
  JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
13
13
  )
14
- from .jwt_data import JwtData
14
+ from .jwt_registry import JwtRegistry
15
15
 
16
16
  # the JWT data object
17
- __jwt_data: JwtData = JwtData()
17
+ __jwt_registry: JwtRegistry = JwtRegistry()
18
18
 
19
19
 
20
20
  def jwt_needed(func: callable) -> callable:
@@ -58,10 +58,10 @@ def jwt_verify_request(request: Request,
58
58
  # yes, extract and validate the JWT access token
59
59
  token: str = auth_header.split(" ")[1]
60
60
  if logger:
61
- logger.debug(msg="Token was found")
61
+ logger.debug(msg="Bearer token was retrieved")
62
62
  errors: list[str] = []
63
63
  jwt_validate_token(errors=errors,
64
- nature=["A"],
64
+ natures=["A"],
65
65
  token=token)
66
66
  if errors:
67
67
  err_msg = "; ".join(errors)
@@ -85,7 +85,7 @@ def jwt_assert_account(account_id: str) -> bool:
85
85
  :param account_id: the account identification
86
86
  :return: *True* if access data exists for *account_id*, *False* otherwise
87
87
  """
88
- return __jwt_data.access_data.get(account_id) is not None
88
+ return __jwt_registry.access_data.get(account_id) is not None
89
89
 
90
90
 
91
91
  def jwt_set_account(account_id: str,
@@ -126,17 +126,17 @@ def jwt_set_account(account_id: str,
126
126
  reference_url = reference_url[:pos]
127
127
 
128
128
  # register the JWT service
129
- __jwt_data.add_account(account_id=account_id,
130
- reference_url=reference_url,
131
- claims=claims,
132
- access_max_age=access_max_age,
133
- refresh_max_age=refresh_max_age,
134
- grace_interval=grace_interval,
135
- token_audience=token_audience,
136
- token_nonce=token_nonce,
137
- request_timeout=request_timeout,
138
- remote_provider=remote_provider,
139
- logger=logger)
129
+ __jwt_registry.add_account(account_id=account_id,
130
+ reference_url=reference_url,
131
+ claims=claims,
132
+ access_max_age=access_max_age,
133
+ refresh_max_age=refresh_max_age,
134
+ grace_interval=grace_interval,
135
+ token_audience=token_audience,
136
+ token_nonce=token_nonce,
137
+ request_timeout=request_timeout,
138
+ remote_provider=remote_provider,
139
+ logger=logger)
140
140
 
141
141
 
142
142
  def jwt_remove_account(account_id: str,
@@ -151,26 +151,26 @@ def jwt_remove_account(account_id: str,
151
151
  if logger:
152
152
  logger.debug(msg=f"Remove access data for '{account_id}'")
153
153
 
154
- return __jwt_data.remove_account(account_id=account_id,
155
- logger=logger)
154
+ return __jwt_registry.remove_account(account_id=account_id,
155
+ logger=logger)
156
156
 
157
157
 
158
158
  def jwt_validate_token(errors: list[str] | None,
159
159
  token: str,
160
- nature: list[str] = None,
160
+ natures: list[str] = None,
161
161
  account_id: str = None,
162
162
  logger: Logger = None) -> dict[str, Any] | None:
163
163
  """
164
164
  Verify if *token* ia a valid JWT token.
165
165
 
166
166
  Raise an appropriate exception if validation failed.
167
- if *nature* is provided, it is checked whether *token* has been locally issued and is of a appropriate nature.
168
- A token issued locally has the header claim *kid* starting with "A" (for *Access*) or "R" (for *Refresh*),
169
- followed by its id in the token database.
167
+ if *nature* is provided, verify whether *token* has been locally issued and is of a appropriate nature.
168
+ A token issued locally has the header claim *kid* starting with *A* (for *Access*) or *R* (for *Refresh*),
169
+ followed by its id in the token database, or as a single letter in the range *[B-Z]*, less *R*.
170
170
 
171
171
  :param errors: incidental error messages
172
172
  :param token: the token to be validated
173
- :param nature: one of more prefixes identifying the nature of locally issued tokens
173
+ :param natures: one or more prefixes identifying the nature of locally issued tokens
174
174
  :param account_id: optionally, validate the token's account owner
175
175
  :param logger: optional logger
176
176
  :return: The token's claims (header and payload) if if is valid, *None* otherwise
@@ -188,9 +188,10 @@ def jwt_validate_token(errors: list[str] | None,
188
188
  op_errors: list[str] = []
189
189
 
190
190
  # retrieve token data from database
191
- if nature and not (token_kid and token_kid[0:1] in nature):
191
+ if natures and not (token_kid and token_kid[0:1] in natures):
192
192
  op_errors.append("Invalid token")
193
- elif token_kid:
193
+ elif token_kid and len(token_kid) > 1 and token_kid[0:1].isupper() and token[1:].isdigit():
194
+ # token was likely issued locally
194
195
  where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
195
196
  if account_id:
196
197
  where_data[JWT_DB_COL_ACCOUNT] = account_id
@@ -275,7 +276,7 @@ def jwt_revoke_token(errors: list[str] | None,
275
276
  op_errors: list[str] = []
276
277
  token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
277
278
  token=refresh_token,
278
- nature=["A", "R"],
279
+ natures=["A", "R"],
279
280
  account_id=account_id,
280
281
  logger=logger)
281
282
  if not op_errors:
@@ -298,16 +299,71 @@ def jwt_revoke_token(errors: list[str] | None,
298
299
  return result
299
300
 
300
301
 
301
- def jwt_get_tokens(errors: list[str] | None,
302
- account_id: str,
303
- account_claims: dict[str, Any] = None,
304
- refresh_token: str = None,
305
- logger: Logger = None) -> dict[str, Any]:
302
+ def jwt_issue_token(errors: list[str] | None,
303
+ account_id: str,
304
+ nature: str,
305
+ duration: int,
306
+ grace_interval: int = None,
307
+ claims: dict[str, Any] = None,
308
+ logger: Logger = None) -> str:
309
+ """
310
+ Issue or refresh, and return, a JWT token associated with *account_id*, of the specified *nature*.
311
+
312
+ The parameter *nature* must be a single letter in the range *[B-Z]*, less *R*
313
+ (*A* is reserved for *access* tokens, and *R* for *refresh* tokens).
314
+ The parameter *duration* specifies the token's validity interval (at least 60 seconds).
315
+ These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
316
+
317
+ :param errors: incidental error messages
318
+ :param account_id: the account identification
319
+ :param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
320
+ :param duration: the number of seconds for the token to remain valid (at least 60 seconds)
321
+ :param claims: optional token's claims
322
+ :param grace_interval: optional interval for the token to become active (in seconds)
323
+ :param logger: optional logger
324
+ :return: the JWT token data, or *None* if error
325
+ """
326
+ # inicialize the return variable
327
+ result: str | None = None
328
+
329
+ if logger:
330
+ logger.debug(msg=f"Issue a JWT token for '{account_id}'")
331
+ op_errors: list[str] = []
332
+
333
+ try:
334
+ result = __jwt_registry.issue_token(account_id=account_id,
335
+ nature=nature,
336
+ duration=duration,
337
+ claims=claims,
338
+ grace_interval=grace_interval,
339
+ logger=logger)
340
+ if logger:
341
+ logger.debug(msg=f"Token is '{result}'")
342
+ except Exception as e:
343
+ # token issuing failed
344
+ op_errors.append(str(e))
345
+
346
+ if op_errors:
347
+ if logger:
348
+ logger.error("; ".join(op_errors))
349
+ if isinstance(errors, list):
350
+ errors.extend(op_errors)
351
+
352
+ return result
353
+
354
+
355
+ def jwt_issue_tokens(errors: list[str] | None,
356
+ account_id: str,
357
+ account_claims: dict[str, Any] = None,
358
+ refresh_token: str = None,
359
+ logger: Logger = None) -> dict[str, Any]:
306
360
  """
307
- Issue or refresh, and return, the JWT token data associated with *account_id*.
361
+ Issue the JWT tokens associated with *account_id*, for access and refresh operations.
308
362
 
309
- If *refresh_token* is provided, its claims are used on issuing the new tokens,
310
- and claims in *account_claims*, if any, are ignored.
363
+ If *refresh_token* is provided, its claims are used on issuing the new tokens, and
364
+ claims in *account_claims*, if any, are ignored. Furthermore, these claims are ignored,
365
+ if provided in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
366
+ Other claims specified therein may supercede registered account-related claims.
311
367
 
312
368
  Structure of the return data:
313
369
  {
@@ -328,26 +384,29 @@ def jwt_get_tokens(errors: list[str] | None,
328
384
  result: dict[str, Any] | None = None
329
385
 
330
386
  if logger:
331
- logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
387
+ logger.debug(msg=f"Return JWT token data for '{account_id}'")
332
388
  op_errors: list[str] = []
389
+
390
+ # verify whether this refresh token is legitimate
333
391
  if refresh_token:
334
- # verify whether this refresh token is legitimate
335
392
  account_claims = (jwt_validate_token(errors=op_errors,
336
393
  token=refresh_token,
337
- nature=["R"],
394
+ natures=["R"],
338
395
  account_id=account_id,
339
396
  logger=logger) or {}).get("payload")
340
397
  if account_claims:
398
+ account_claims.pop("exp", None)
341
399
  account_claims.pop("iat", None)
342
- account_claims.pop("jti", None)
343
400
  account_claims.pop("iss", None)
344
- account_claims.pop("exp", None)
401
+ account_claims.pop("jti", None)
345
402
  account_claims.pop("nbt", None)
403
+ account_claims.pop("sub", None)
404
+
346
405
  if not op_errors:
347
406
  try:
348
- result = __jwt_data.issue_tokens(account_id=account_id,
349
- account_claims=account_claims,
350
- logger=logger)
407
+ result = __jwt_registry.issue_tokens(account_id=account_id,
408
+ account_claims=account_claims,
409
+ logger=logger)
351
410
  if logger:
352
411
  logger.debug(msg=f"Token data is '{result}'")
353
412
  except Exception as e:
@@ -18,9 +18,9 @@ from .jwt_constants import (
18
18
  )
19
19
 
20
20
 
21
- class JwtData:
21
+ class JwtRegistry:
22
22
  """
23
- Shared JWT data for security token access.
23
+ Shared JWT registry for security token access.
24
24
 
25
25
  Instance variables:
26
26
  - access_lock: lock for safe multi-threading access
@@ -59,12 +59,13 @@ class JwtData:
59
59
  as token-related and account-related. All times are UTC.
60
60
 
61
61
  Token-related claims are mostly required claims, and convey information about the token itself:
62
+ # required
62
63
  "exp": <timestamp> # expiration time
63
64
  "iat": <timestamp> # issued at
64
65
  "iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
65
66
  "jti": <string> # JWT id
66
67
  "sub": <string> # subject (the account identification)
67
- "nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
68
+ # optional
68
69
  "aud": <string> # token audience
69
70
  "nbt": <timestamp> # not before time
70
71
 
@@ -72,20 +73,20 @@ class JwtData:
72
73
  Alhough they can be freely specified, these are some of the most commonly used claims:
73
74
  "valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
74
75
  "valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00.00>)
75
- "birthdate": <string> # subject's birth date
76
- "email": <string> # subject's email
77
- "gender": <string> # subject's gender
78
- "name": <string> # subject's name
79
- "roles": <List[str]> # subject roles
80
- "nonce": <string> # value used to associate a client session with a token
76
+ "birthdate": <string> # subject's birth date
77
+ "email": <string> # subject's email
78
+ "gender": <string> # subject's gender
79
+ "name": <string> # subject's name
80
+ "roles": <List[str]> # subject roles
81
+ "nonce": <string> # value used to associate a client session with a token
81
82
 
82
83
  The token header has these items:
83
- "alg": <string> # the algorithm used to sign the token (one of 'HS256', 'HS512', 'RSA256', 'RSA512')
84
- "typ": <string> # the token type (fixed to 'JWT'
84
+ "alg": <string> # the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
85
+ "typ": <string> # the token type (fixed to *JWT*
85
86
  "kid": <string> # a reference to the token type and the key to its location in the token database
86
87
 
87
- If issued by the local server, "kid" holds the key to the corresponding record in the token database.
88
- It starts with *A* for (*Access*) or *R* (for *Refresh*) followed its integer key.
88
+ If issued by the local server, "kid" holds the key to the corresponding record in the token database,
89
+ if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
89
90
  """
90
91
  def __init__(self) -> None:
91
92
  """
@@ -110,7 +111,7 @@ class JwtData:
110
111
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
111
112
 
112
113
  The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
113
- at a minimum, "birthdate", "email", "gender", "name", and "roles".
114
+ at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
114
115
  If the token provider is local, then the token-related claims are created at token issuing time.
115
116
  If the token provider is remote, all claims are sent to it at token request time.
116
117
 
@@ -173,6 +174,73 @@ class JwtData:
173
174
 
174
175
  return account_data is not None
175
176
 
177
+ def issue_token(self,
178
+ account_id: str,
179
+ nature: str,
180
+ duration: int,
181
+ grace_interval: int = None,
182
+ claims: dict[str, Any] = None,
183
+ logger: Logger = None) -> str:
184
+ """
185
+ Issue an return a JWT token associated with *account_id*.
186
+
187
+ The parameter *nature* must be a single letter in the range *[B-Z]*, less *R*
188
+ (*A* is reserved for *access* tokens, and *R* for *refresh* tokens).
189
+ The parameter *duration* specifies the token's validity interval (at least 60 seconds).
190
+ These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
191
+
192
+ :param account_id: the account identification
193
+ :param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
194
+ :param duration: the number of seconds for the token to remain valid (at least 60 seconds)
195
+ :param claims: optional token's claims
196
+ :param grace_interval: optional interval for the token to become active (in seconds)
197
+ :param logger: optional logger
198
+ :return: the JWT token
199
+ :raises RuntimeError: invalid parameter
200
+ """
201
+ # validate some parameters
202
+ err_msg: str | None = None
203
+ if not isinstance(nature, str) or \
204
+ len(nature) != 1 or nature < "A" or nature > "Z":
205
+ err_msg: str = f"Invalid nature '{nature}'"
206
+ elif not isinstance(claims, dict) or "iss" not in claims:
207
+ err_msg = f"invalid claims '{claims}'"
208
+ elif not isinstance(duration, int) or duration < 60:
209
+ err_msg = f"Invalid duration '{duration}'"
210
+ if err_msg:
211
+ if logger:
212
+ logger.error(err_msg)
213
+ raise RuntimeError(err_msg)
214
+
215
+ # obtain the account data in storage (may raise an exception)
216
+ account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
217
+ logger=logger)
218
+ # issue the token
219
+ current_claims: dict[str, Any] = {}
220
+ if claims:
221
+ current_claims.update(claims)
222
+ current_claims["jti"] = str_random(size=32,
223
+ chars=string.ascii_letters + string.digits)
224
+ current_claims["iss"] = account_data.get("reference-url")
225
+ current_claims["sub"] = account_id
226
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
227
+ current_claims["iat"] = just_now
228
+ if grace_interval:
229
+ current_claims["nbf"] = just_now + grace_interval
230
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
231
+ tz=timezone.utc).isoformat()
232
+ else:
233
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
234
+ tz=timezone.utc).isoformat()
235
+ current_claims["exp"] = just_now + duration
236
+ current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
237
+ tz=timezone.utc).isoformat()
238
+ # may raise an exception
239
+ return jwt.encode(payload=current_claims,
240
+ key=JWT_ENCODING_KEY,
241
+ algorithm=JWT_DEFAULT_ALGORITHM,
242
+ headers={"kid": nature})
243
+
176
244
  def issue_tokens(self,
177
245
  account_id: str,
178
246
  account_claims: dict[str, Any] = None,
@@ -180,6 +248,9 @@ class JwtData:
180
248
  """
181
249
  Issue and return the JWT access and refresh tokens for *account_id*.
182
250
 
251
+ These claims are ignored, if specified in *account_claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
252
+ Other claims specified therein may supercede registered account-related claims.
253
+
183
254
  Structure of the return data:
184
255
  {
185
256
  "access_token": <jwt-token>,
@@ -191,125 +262,118 @@ class JwtData:
191
262
  :param account_id: the account identification
192
263
  :param account_claims: if provided, may supercede registered account-related claims
193
264
  :param logger: optional logger
194
- :return: the JWT token data, or *None* if error
195
- :raises InvalidTokenError: token is invalid
196
- :raises InvalidKeyError: authentication key is not in the proper format
197
- :raises ExpiredSignatureError: token and refresh period have expired
198
- :raises InvalidSignatureError: signature does not match the one provided as part of the token
199
- :raises ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
200
- :raises InvalidAudienceError: 'aud' claim does not match one of the expected audience
201
- :raises InvalidAlgorithmError: the specified algorithm is not recognized
202
- :raises InvalidIssuerError: 'iss' claim does not match the expected issuer
203
- :raises InvalidIssuedAtError: 'iat' claim is non-numeric
204
- :raises MissingRequiredClaimError: a required claim is not contained in the claimset
205
- :raises RuntimeError: error accessing the token database
265
+ :return: the JWT token data
266
+ :raises RuntimeError: invalid account id, or error accessing the token database
206
267
  """
207
268
  # initialize the return variable
208
269
  result: dict[str, Any] | None = None
209
270
 
210
- # process the data in storage
271
+ # process the account data in storage
211
272
  with (self.access_lock):
212
- account_data: dict[str, Any] = self.access_data.get(account_id)
213
-
214
- # was the JWT data obtained ?
215
- if account_data:
216
- # yes, proceed
217
- errors: list[str] = []
218
- current_claims: dict[str, Any] = account_data.get("claims").copy()
219
- if account_claims:
220
- current_claims.update(account_claims)
221
- current_claims["jti"] = str_random(size=32,
222
- chars=string.ascii_letters + string.digits)
223
- current_claims["sub"] = account_id
224
- current_claims["iss"] = account_data.get("reference-url")
225
-
226
- # where is the JWT service provider ?
227
- if account_data.get("remote-provider"):
228
- # JWT service is being provided by a remote server
229
- # Structure of the return data:
230
- # {
231
- # "access_token": <jwt-token>,
232
- # "created_in": <timestamp>,
233
- # "expires_in": <seconds-to-expiration>,
234
- # "refresh_token": <jwt-token>
235
- # ...
236
- # }
237
- result = _jwt_request_token(errors=errors,
238
- reference_url=current_claims.get("iss"),
239
- claims=current_claims,
240
- timeout=account_data.get("request-timeout"),
241
- logger=logger)
242
- if errors:
243
- raise RuntimeError("; ".join(errors))
273
+ account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
274
+ logger=logger)
275
+ current_claims: dict[str, Any] = account_data.get("claims").copy()
276
+ if account_claims:
277
+ current_claims.update(account_claims)
278
+ current_claims["jti"] = str_random(size=32,
279
+ chars=string.ascii_letters + string.digits)
280
+ current_claims["sub"] = account_id
281
+ current_claims["iss"] = account_data.get("reference-url")
282
+ errors: list[str] = []
283
+
284
+ # where is the JWT service provider ?
285
+ if account_data.get("remote-provider"):
286
+ # JWT service is being provided by a remote server
287
+ result = _jwt_request_token(errors=errors,
288
+ reference_url=current_claims.get("iss"),
289
+ claims=current_claims,
290
+ timeout=account_data.get("request-timeout"),
291
+ logger=logger)
292
+ if errors:
293
+ raise RuntimeError("; ".join(errors))
294
+ else:
295
+ # JWT service is being provided locally
296
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
297
+ current_claims["iat"] = just_now
298
+ grace_interval = account_data.get("grace-interval")
299
+ if grace_interval:
300
+ current_claims["nbf"] = just_now + grace_interval
301
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
302
+ tz=timezone.utc).isoformat()
244
303
  else:
245
- # JWT service is being provided locally
246
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
247
- current_claims["iat"] = just_now
248
- grace_interval = account_data.get("grace-interval")
249
- if grace_interval:
250
- account_data["nbf"] = just_now + grace_interval
251
- current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
252
- tz=timezone.utc).isoformat()
253
- else:
254
- current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
255
- tz=timezone.utc).isoformat()
256
- # issue a candidate refresh token first, and persist it
257
- current_claims["exp"] = just_now + account_data.get("refresh-max-age")
258
- current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
259
- tz=timezone.utc).isoformat()
260
- # may raise an exception
261
- refresh_token: str = jwt.encode(payload=current_claims,
262
- key=JWT_ENCODING_KEY,
263
- algorithm=JWT_DEFAULT_ALGORITHM,
264
- headers={"kid": "R0"})
265
- # obtain a DB connection (may raise an exception)
266
- db_conn: Any = db_connect(errors=errors,
267
- logger=logger)
268
- # persist the candidate token (may raise an exception)
269
- token_id: int = _jwt_persist_token(errors=errors,
270
- account_id=account_id,
271
- jwt_token=refresh_token,
272
- db_conn=db_conn,
273
- logger=logger)
274
- # issue the definitive refresh token
275
- refresh_token = jwt.encode(payload=current_claims,
304
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
305
+ tz=timezone.utc).isoformat()
306
+ # issue a candidate refresh token first, and persist it
307
+ current_claims["exp"] = just_now + account_data.get("refresh-max-age")
308
+ current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
309
+ tz=timezone.utc).isoformat()
310
+ # may raise an exception
311
+ refresh_token: str = jwt.encode(payload=current_claims,
312
+ key=JWT_ENCODING_KEY,
313
+ algorithm=JWT_DEFAULT_ALGORITHM,
314
+ headers={"kid": "R0"})
315
+ # obtain a DB connection (may raise an exception)
316
+ db_conn: Any = db_connect(errors=errors,
317
+ logger=logger)
318
+ # persist the candidate token (may raise an exception)
319
+ token_id: int = _jwt_persist_token(errors=errors,
320
+ account_id=account_id,
321
+ jwt_token=refresh_token,
322
+ db_conn=db_conn,
323
+ logger=logger)
324
+ # issue the definitive refresh token
325
+ refresh_token = jwt.encode(payload=current_claims,
326
+ key=JWT_ENCODING_KEY,
327
+ algorithm=JWT_DEFAULT_ALGORITHM,
328
+ headers={"kid": f"R{token_id}"})
329
+ # persist it
330
+ db_update(errors=errors,
331
+ update_stmt=f"UPDATE {JWT_DB_TABLE}",
332
+ update_data={JWT_DB_COL_TOKEN: refresh_token},
333
+ where_data={JWT_DB_COL_KID: token_id},
334
+ connection=db_conn,
335
+ logger=logger)
336
+ # commit the transaction
337
+ db_commit(errors=errors,
338
+ connection=db_conn,
339
+ logger=logger)
340
+ if errors:
341
+ raise RuntimeError("; ".join(errors))
342
+
343
+ # issue the access token
344
+ current_claims["exp"] = just_now + account_data.get("access-max-age")
345
+ # may raise an exception
346
+ access_token: str = jwt.encode(payload=current_claims,
276
347
  key=JWT_ENCODING_KEY,
277
348
  algorithm=JWT_DEFAULT_ALGORITHM,
278
- headers={"kid": f"R{token_id}"})
279
- # persist it
280
- db_update(errors=errors,
281
- update_stmt=f"UPDATE {JWT_DB_TABLE}",
282
- update_data={JWT_DB_COL_TOKEN: refresh_token},
283
- where_data={JWT_DB_COL_KID: token_id},
284
- connection=db_conn,
285
- logger=logger)
286
- # commit the transaction
287
- db_commit(errors=errors,
288
- connection=db_conn,
289
- logger=logger)
290
- if errors:
291
- raise RuntimeError("; ".join(errors))
292
-
293
- # issue the access token
294
- current_claims["exp"] = just_now + account_data.get("access-max-age")
295
- # may raise an exception
296
- access_token: str = jwt.encode(payload=current_claims,
297
- key=JWT_ENCODING_KEY,
298
- algorithm=JWT_DEFAULT_ALGORITHM,
299
- headers={"kid": f"A{token_id}"})
300
- # return the token data
301
- result = {
302
- "access_token": access_token,
303
- "created_in": current_claims.get("iat"),
304
- "expires_in": current_claims.get("exp"),
305
- "refresh_token": refresh_token
306
- }
307
- else:
308
- # JWT access data not found
309
- err_msg: str = f"No JWT access data found for '{account_id}'"
310
- if logger:
311
- logger.error(err_msg)
312
- raise RuntimeError(err_msg)
349
+ headers={"kid": f"A{token_id}"})
350
+ # return the token data
351
+ result = {
352
+ "access_token": access_token,
353
+ "created_in": current_claims.get("iat"),
354
+ "expires_in": current_claims.get("exp"),
355
+ "refresh_token": refresh_token
356
+ }
357
+
358
+ return result
359
+
360
+ def __get_account_data(self,
361
+ account_id: str,
362
+ logger: Logger = None) -> dict[str, Any]:
363
+ """
364
+ Retrieve the JWT access data associated with *account_id*.
365
+
366
+ :return: the JWT access data associated with *account_id*
367
+ :raises RuntimeError: No JWT access data exists for *account_id*
368
+ """
369
+ # retrieve the access data
370
+ result: dict[str, Any] = self.access_data.get(account_id)
371
+ if not result:
372
+ # JWT access data not found
373
+ err_msg: str = f"No JWT access data found for '{account_id}'"
374
+ if logger:
375
+ logger.error(err_msg)
376
+ raise RuntimeError(err_msg)
313
377
 
314
378
  return result
315
379
 
@@ -389,7 +453,7 @@ def _jwt_persist_token(errors: list[str],
389
453
  :param db_conn: the database connection to use
390
454
  :param logger: optional logger
391
455
  :return: the storage id of the inserted token
392
- :raises RuntimeError: error accessing the revocation database
456
+ :raises RuntimeError: error accessing the token database
393
457
  """
394
458
  from pypomes_db import db_select, db_insert, db_delete
395
459
  from .jwt_pomes import jwt_get_claims
File without changes
File without changes
File without changes
File without changes