pypomes-jwt 0.9.6__tar.gz → 0.9.8__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.6
3
+ Version: 0.9.8
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.6"
9
+ version = "0.9.8"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -4,7 +4,7 @@ from .jwt_constants import (
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
6
  JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
7
- JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
7
+ JWT_DEFAULT_ALGORITHM, JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE
8
8
  )
9
9
  from .jwt_pomes import (
10
10
  jwt_needed, jwt_verify_request,
@@ -20,7 +20,7 @@ __all__ = [
20
20
  "JWT_DB_TABLE", "JWT_DB_COL_KID", "JWT_DB_COL_ACCOUNT",
21
21
  "JWT_DB_COL_ALGORITHM", "JWT_DB_COL_DECODER", "JWT_DB_COL_TOKEN",
22
22
  "JWT_ACCOUNT_LIMIT", "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
23
- "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
23
+ "JWT_DEFAULT_ALGORITHM", "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
24
24
  # jwt_pomes
25
25
  "jwt_needed", "jwt_verify_request",
26
26
  "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
@@ -5,11 +5,11 @@ from logging import Logger
5
5
  from pypomes_db import db_select, db_delete
6
6
  from typing import Any
7
7
 
8
- from . import JWT_DB_COL_ACCOUNT
9
- from .jwt_constants import (
8
+ from . import (
10
9
  JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
11
10
  JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
12
- JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
11
+ JWT_DB_TABLE, JWT_DB_COL_KID,
12
+ JWT_DB_COL_ACCOUNT, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
13
13
  )
14
14
  from .jwt_registry import JwtRegistry
15
15
 
@@ -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_registry.access_data.get(account_id) is not None
88
+ return __jwt_registry.access_registry.get(account_id) is not None
89
89
 
90
90
 
91
91
  def jwt_set_account(account_id: str,
@@ -93,44 +93,31 @@ def jwt_set_account(account_id: str,
93
93
  access_max_age: int = JWT_ACCESS_MAX_AGE,
94
94
  refresh_max_age: int = JWT_REFRESH_MAX_AGE,
95
95
  grace_interval: int = None,
96
- request_timeout: int = None,
97
- remote_url: str = None,
98
96
  logger: Logger = None) -> None:
99
97
  """
100
98
  Establish the data needed to obtain JWT tokens for *account_id*.
101
99
 
100
+ The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
101
+ at a minimum, *iss*, *birthdate*, *email*, *gender*, *name*, and *roles*.
102
+ It is enforced that the parameter *refresh_max_age* should be at least 300 seconds greater
103
+ than *access-max-age*.
104
+
102
105
  :param account_id: the account identification
103
106
  :param claims: the JWT claimset, as key-value pairs
104
107
  :param access_max_age: access token duration, in seconds
105
108
  :param refresh_max_age: refresh token duration, in seconds
106
109
  :param grace_interval: optional time to wait for token to be valid, in seconds
107
- :param request_timeout: timeout for the requests to the reference URL
108
- :param remote_url: remote server's URL to obtain the JWT tokens from
109
110
  :param logger: optional logger
110
111
  """
111
112
  if logger:
112
- logger.debug(msg=f"Register account data for '{account_id}'")
113
-
114
- # extract the claims provided in the remote_url URL's query string
115
- if remote_url:
116
- pos: int = remote_url.find("?")
117
- if pos > 0:
118
- claims = claims or {}
119
- params: list[str] = remote_url[pos+1:].split(sep="&")
120
- for param in params:
121
- key: str = param.split("=")[0]
122
- value: str = param.split("=")[1]
123
- claims[key] = value
124
- remote_url = remote_url[:pos]
113
+ logger.debug(msg=f"Registering account data for '{account_id}'")
125
114
 
126
115
  # register the JWT service
127
116
  __jwt_registry.add_account(account_id=account_id,
128
117
  claims=claims,
129
118
  access_max_age=access_max_age,
130
- refresh_max_age=refresh_max_age,
119
+ refresh_max_age=max(refresh_max_age, access_max_age + 300),
131
120
  grace_interval=grace_interval,
132
- request_timeout=request_timeout,
133
- remote_url=remote_url,
134
121
  logger=logger)
135
122
 
136
123
 
@@ -158,10 +145,12 @@ def jwt_validate_token(errors: list[str] | None,
158
145
  """
159
146
  Verify if *token* ia a valid JWT token.
160
147
 
161
- Raise an appropriate exception if validation failed.
162
- if *nature* is provided, verify whether *token* has been locally issued and is of a appropriate nature.
148
+ Raise an appropriate exception if validation failed. Attempt to validate non locally issued tokens
149
+ will not succeed. if *nature* is provided, validate whether *token* is of that nature.
163
150
  A token issued locally has the header claim *kid* starting with *A* (for *Access*) or *R* (for *Refresh*),
164
151
  followed by its id in the token database, or as a single letter in the range *[B-Z]*, less *R*.
152
+ If the *kid* claim contains such an id, then the cryptographic key needed for validation
153
+ will be obtained from the token database. Otherwise, the current decoding key is used.
165
154
 
166
155
  :param errors: incidental error messages
167
156
  :param token: the token to be validated
@@ -250,7 +239,7 @@ def jwt_validate_token(errors: list[str] | None,
250
239
 
251
240
  def jwt_revoke_token(errors: list[str] | None,
252
241
  account_id: str,
253
- refresh_token: str,
242
+ token: str,
254
243
  logger: Logger = None) -> bool:
255
244
  """
256
245
  Revoke the *refresh_token* associated with *account_id*.
@@ -259,7 +248,7 @@ def jwt_revoke_token(errors: list[str] | None,
259
248
 
260
249
  :param errors: incidental error messages
261
250
  :param account_id: the account identification
262
- :param refresh_token: the token to be revoked
251
+ :param token: the token to be revoked
263
252
  :param logger: optional logger
264
253
  :return: *True* if operation could be performed, *False* otherwise
265
254
  """
@@ -271,7 +260,7 @@ def jwt_revoke_token(errors: list[str] | None,
271
260
 
272
261
  op_errors: list[str] = []
273
262
  token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
274
- token=refresh_token,
263
+ token=token,
275
264
  account_id=account_id,
276
265
  logger=logger)
277
266
  if not op_errors:
@@ -362,10 +351,10 @@ def jwt_issue_tokens(errors: list[str] | None,
362
351
 
363
352
  Structure of the return data:
364
353
  {
365
- "access_token": <jwt-token>,
366
- "created_in": <timestamp>,
367
- "expires_in": <seconds-to-expiration>,
368
- "refresh_token": <jwt-token>
354
+ "access-token": <jwt-token>,
355
+ "created-in": <timestamp>,
356
+ "expires-in": <seconds-to-expiration>,
357
+ "refresh-token": <jwt-token>
369
358
  }
370
359
 
371
360
  :param errors: incidental error messages
@@ -411,10 +400,10 @@ def jwt_refresh_tokens(errors: list[str] | None,
411
400
 
412
401
  Structure of the return data:
413
402
  {
414
- "access_token": <jwt-token>,
415
- "created_in": <timestamp>,
416
- "expires_in": <seconds-to-expiration>,
417
- "refresh_token": <jwt-token>
403
+ "access-token": <jwt-token>,
404
+ "created-in": <timestamp>,
405
+ "expires-in": <seconds-to-expiration>,
406
+ "refresh-token": <jwt-token>
418
407
  }
419
408
 
420
409
  :param errors: incidental error messages
@@ -430,24 +419,26 @@ def jwt_refresh_tokens(errors: list[str] | None,
430
419
  logger.debug(msg=f"Refreshing a JWT token pair for '{account_id}'")
431
420
  op_errors: list[str] = []
432
421
 
433
- # verify whether this refresh token is legitimate
422
+ # assert the refresh token
434
423
  if refresh_token:
435
- account_claims: dict[str, Any] = (jwt_validate_token(errors=op_errors,
436
- token=refresh_token,
437
- nature="R",
438
- account_id=account_id,
439
- logger=logger) or {}).get("payload")
440
- # revoke current refresh token
441
- if account_claims and jwt_revoke_token(errors=errors,
424
+ # is the refresh token valid ?
425
+ account_claims = jwt_validate_token(errors=op_errors,
426
+ token=refresh_token,
427
+ nature="R",
428
+ account_id=account_id,
429
+ logger=logger)
430
+ # if it is, revoke current refresh token
431
+ if account_claims and jwt_revoke_token(errors=op_errors,
442
432
  account_id=account_id,
443
- refresh_token=refresh_token,
433
+ token=refresh_token,
444
434
  logger=logger):
445
435
  # issue tokens
446
- result = jwt_issue_tokens(errors=errors,
436
+ result = jwt_issue_tokens(errors=op_errors,
447
437
  account_id=account_id,
448
438
  account_claims=account_claims,
449
439
  logger=logger)
450
440
  else:
441
+ # refresh token not found
451
442
  op_errors.append("Refresh token was not provided")
452
443
 
453
444
  if op_errors:
@@ -461,17 +452,13 @@ def jwt_refresh_tokens(errors: list[str] | None,
461
452
 
462
453
  def jwt_get_claims(errors: list[str] | None,
463
454
  token: str,
464
- validate: bool = False,
465
455
  logger: Logger = None) -> dict[str, Any] | None:
466
456
  """
467
- Obtain and return the claims set of a JWT *token*.
457
+ Retrieve and return the claims set of a JWT *token*.
468
458
 
469
- If *validate* is set to *True*, tha following pieces of information are verified:
470
- - the token was issued and signed by the local provider, and is not corrupted
471
- - the claim 'exp' is present and is in the future
472
- - the claim 'nbf' is present and is in the past
459
+ Any valid JWT token may be provided in *token*, as this operation is not restricted to locally issued tokens.
473
460
 
474
- Structure of the returned data:
461
+ Structure of the returned data, for locally issued tokens:
475
462
  {
476
463
  "header": {
477
464
  "alg": "RS256",
@@ -485,7 +472,7 @@ def jwt_get_claims(errors: list[str] | None,
485
472
  "email": "jdoe@mail.com",
486
473
  "exp": 1516640454,
487
474
  "iat": 1516239022,
488
- "iss": "https://my_id_provider/issue",
475
+ "iss": "my_jwt_provider.com",
489
476
  "jti": "Uhsdfgr67FGH567qwSDF33er89retert",
490
477
  "gender": "M",
491
478
  "name": "John Doe",
@@ -500,7 +487,6 @@ def jwt_get_claims(errors: list[str] | None,
500
487
 
501
488
  :param errors: incidental error messages
502
489
  :param token: the token to be inspected for claims
503
- :param validate: If *True*, verifies the token's data (defaults to *False*)
504
490
  :param logger: optional logger
505
491
  :return: the token's claimset, or *None* if error
506
492
  """
@@ -511,19 +497,13 @@ def jwt_get_claims(errors: list[str] | None,
511
497
  logger.debug(msg="Retrieve claims for token")
512
498
 
513
499
  try:
514
- # retrieve the token's claims
515
- if validate:
516
- result = jwt_validate_token(errors=errors,
517
- token=token,
518
- logger=logger)
519
- else:
520
- header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
521
- payload: dict[str, Any] = jwt.decode(jwt=token,
522
- options={"verify_signature": False})
523
- result = {
524
- "header": header,
525
- "payload": payload
526
- }
500
+ header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
501
+ payload: dict[str, Any] = jwt.decode(jwt=token,
502
+ options={"verify_signature": False})
503
+ result = {
504
+ "header": header,
505
+ "payload": payload
506
+ }
527
507
  except Exception as e:
528
508
  if logger:
529
509
  logger.error(msg=str(e))
@@ -1,5 +1,4 @@
1
1
  import jwt
2
- import requests
3
2
  import string
4
3
  import sys
5
4
  from base64 import urlsafe_b64encode
@@ -7,12 +6,12 @@ from datetime import datetime, timezone
7
6
  from logging import Logger
8
7
  from pypomes_core import str_random
9
8
  from pypomes_db import db_connect, db_commit, db_update, db_delete
10
- from requests import Response
11
9
  from threading import Lock
12
10
  from typing import Any
13
11
 
14
- from .jwt_constants import (
15
- JWT_DEFAULT_ALGORITHM, JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
12
+ from . import (
13
+ JWT_DEFAULT_ALGORITHM, JWT_ACCOUNT_LIMIT,
14
+ JWT_ENCODING_KEY, JWT_DECODING_KEY,
16
15
  JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
17
16
  JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN
18
17
  )
@@ -27,22 +26,20 @@ class JwtRegistry:
27
26
  - access_data: dictionary holding the JWT token data, organized by account id:
28
27
  {
29
28
  <account-id>: {
30
- "access-max-age": <int>, # defaults to JWT_ACCESS_MAX_AGE (in seconds)
31
- "refresh-max-age": <int>, # defaults to JWT_REFRESH_MAX_AGE (in seconds)
32
- "grace-interval": <int>, # time to wait for token to be valid, in seconds
33
- "request-timeout": <int>, # timeout for the requests to the reference URL (in seconds)
34
- "remote_url": <string>, # remote server's URL to obtain the JWT tokens from
29
+ "access-max-age": <int>, # defaults to JWT_ACCESS_MAX_AGE (in seconds)
30
+ "refresh-max-age": <int>, # defaults to JWT_REFRESH_MAX_AGE (in seconds)
31
+ "grace-interval": <int>, # time to wait for token to be valid, in seconds
35
32
  "claims": {
36
- "iss": <string>, # token'ss issuer
37
- "birthdate": <string>, # subject's birth date
38
- "email": <string>, # subject's email
39
- "gender": <string>, # subject's gender
40
- "name": <string>, # subject's name
41
- "roles": <List[str]>, # subject roles
42
- "nonce": <string>, # used to associate a Client session with a token
33
+ "iss": <string>, # token'ss issuer
34
+ "birthdate": <string>, # subject's birth date
35
+ "email": <string>, # subject's email
36
+ "gender": <string>, # subject's gender
37
+ "name": <string>, # subject's name
38
+ "roles": <List[str]>, # subject roles
39
+ "nonce": <string>, # used to associate a Client session with a token
43
40
  # output, only
44
- "valid-from": <string>, # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
45
- "valid-until": <string>, # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
41
+ "valid-from": <string>, # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
42
+ "valid-until": <string>, # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
46
43
  ...
47
44
  }
48
45
  },
@@ -79,8 +76,8 @@ class JwtRegistry:
79
76
 
80
77
  The token header has these items:
81
78
  "alg": <string> the algorithm used to sign the token (one of *HS256*, *HS51*', *RSA256*, *RSA512*)
82
- "typ": <string> the token type (fixed to *JWT*
83
- "kid": <string> a reference to the token type and the key to its location in the token database
79
+ "typ": <string> the token type (fixed to *JWT*)
80
+ "kid": <string> a token type and key to its location in the token database
84
81
 
85
82
  If issued by the local server, "kid" holds the key to the corresponding record in the token database,
86
83
  if starting with *A* for (*Access*) or *R* (for *Refresh*), followed an integer.
@@ -90,7 +87,7 @@ class JwtRegistry:
90
87
  Initizalize the token access data.
91
88
  """
92
89
  self.access_lock: Lock = Lock()
93
- self.access_data: dict[str, Any] = {}
90
+ self.access_registry: dict[str, Any] = {}
94
91
 
95
92
  def add_account(self,
96
93
  account_id: str,
@@ -98,35 +95,28 @@ class JwtRegistry:
98
95
  access_max_age: int,
99
96
  refresh_max_age: int,
100
97
  grace_interval: int | None,
101
- request_timeout: int | None,
102
- remote_url: str | None,
103
98
  logger: Logger = None) -> None:
104
99
  """
105
100
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
106
101
 
107
102
  The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
108
- at a minimum, *birthdate*, *email*, *gender*, *name*, and *roles*.
109
- If the token provider is local, then the token-related claims are created at token issuing time.
110
- If the token provider is remote, all claims are sent to it at token request time.
103
+ at a minimum, *iss*, *birthdate*, *email*, *gender*, *name*, and *roles*.
104
+ The parameter *refresh_max_age* should be at least 300 seconds greater than *access-max-age*.
111
105
 
112
106
  :param account_id: the account identification
113
107
  :param claims: the JWT claimset, as key-value pairs
114
- :param access_max_age: access token duration, in seconds
115
- :param refresh_max_age: refresh token duration, in seconds
108
+ :param access_max_age: access token duration, in seconds (at least 60 seconds)
109
+ :param refresh_max_age: refresh token duration, in seconds (greater than *access_max_age*)
116
110
  :param grace_interval: time to wait for token to be valid, in seconds
117
- :param request_timeout: timeout for the requests to the reference URL (in seconds)
118
- :param remote_url: remote server's URL to obtain the JWT tokens from
119
111
  :param logger: optional logger
120
112
  """
121
113
  # build and store the access data for the account
122
114
  with self.access_lock:
123
- if account_id not in self.access_data:
124
- self.access_data[account_id] = {
115
+ if account_id not in self.access_registry:
116
+ self.access_registry[account_id] = {
125
117
  "access-max-age": access_max_age,
126
118
  "refresh-max-age": refresh_max_age,
127
119
  "grace-interval": grace_interval,
128
- "request-timeout": request_timeout,
129
- "remote-url": remote_url,
130
120
  "claims": claims or {}
131
121
  }
132
122
  if logger:
@@ -147,7 +137,7 @@ class JwtRegistry:
147
137
  # remove from internal storage
148
138
  account_data: dict[str, Any] | None
149
139
  with self.access_lock:
150
- account_data = self.access_data.pop(account_id, None)
140
+ account_data = self.access_registry.pop(account_id, None)
151
141
 
152
142
  # remove from database
153
143
  db_delete(errors=None,
@@ -242,10 +232,10 @@ class JwtRegistry:
242
232
 
243
233
  Structure of the return data:
244
234
  {
245
- "access_token": <jwt-token>,
246
- "created_in": <timestamp>,
247
- "expires_in": <seconds-to-expiration>,
248
- "refresh_token": <jwt-token>
235
+ "access-token": <jwt-token>,
236
+ "created-in": <timestamp>,
237
+ "expires-in": <seconds-to-expiration>,
238
+ "refresh-token": <jwt-token>
249
239
  }
250
240
 
251
241
  :param account_id: the account identification
@@ -254,9 +244,6 @@ class JwtRegistry:
254
244
  :return: the JWT token data
255
245
  :raises RuntimeError: invalid account id, or error accessing the token database
256
246
  """
257
- # initialize the return variable
258
- result: dict[str, Any] | None = None
259
-
260
247
  # process the account data in storage
261
248
  with (self.access_lock):
262
249
  account_data: dict[str, Any] = self.__get_account_data(account_id=account_id,
@@ -269,81 +256,67 @@ class JwtRegistry:
269
256
  current_claims["sub"] = account_id
270
257
  errors: list[str] = []
271
258
 
272
- # where is the JWT service provider ?
273
- if account_data.get("remote-url"):
274
- # JWT service is being provided by a remote server
275
- result = _jwt_request_token(errors=errors,
276
- remote_url=account_data.get("remote-url"),
277
- claims=current_claims,
278
- timeout=account_data.get("request-timeout"),
279
- logger=logger)
280
- if errors:
281
- raise RuntimeError("; ".join(errors))
259
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
260
+ current_claims["iat"] = just_now
261
+ grace_interval = account_data.get("grace-interval")
262
+ if grace_interval:
263
+ current_claims["nbf"] = just_now + grace_interval
264
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
265
+ tz=timezone.utc).isoformat()
282
266
  else:
283
- # JWT service is being provided locally
284
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
285
- current_claims["iat"] = just_now
286
- grace_interval = account_data.get("grace-interval")
287
- if grace_interval:
288
- current_claims["nbf"] = just_now + grace_interval
289
- current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["nbf"],
290
- tz=timezone.utc).isoformat()
291
- else:
292
- current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
293
- tz=timezone.utc).isoformat()
294
- # issue a candidate refresh token first, and persist it
295
- current_claims["exp"] = just_now + account_data.get("refresh-max-age")
296
- current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
297
- tz=timezone.utc).isoformat()
298
- # may raise an exception
299
- refresh_token: str = jwt.encode(payload=current_claims,
300
- key=JWT_ENCODING_KEY,
301
- algorithm=JWT_DEFAULT_ALGORITHM,
302
- headers={"kid": "R0"})
303
- # obtain a DB connection (may raise an exception)
304
- db_conn: Any = db_connect(errors=errors,
305
- logger=logger)
306
- # persist the candidate token (may raise an exception)
307
- token_id: int = _jwt_persist_token(errors=errors,
308
- account_id=account_id,
309
- jwt_token=refresh_token,
310
- db_conn=db_conn,
311
- logger=logger)
312
- # issue the definitive refresh token
313
- refresh_token = jwt.encode(payload=current_claims,
267
+ current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
268
+ tz=timezone.utc).isoformat()
269
+ # issue a candidate refresh token first, and persist it
270
+ current_claims["exp"] = just_now + account_data.get("refresh-max-age")
271
+ current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
272
+ tz=timezone.utc).isoformat()
273
+ # may raise an exception
274
+ refresh_token: str = jwt.encode(payload=current_claims,
275
+ key=JWT_ENCODING_KEY,
276
+ algorithm=JWT_DEFAULT_ALGORITHM,
277
+ headers={"kid": "R0"})
278
+ # obtain a DB connection (may raise an exception)
279
+ db_conn: Any = db_connect(errors=errors,
280
+ logger=logger)
281
+ # persist the candidate token (may raise an exception)
282
+ token_id: int = _jwt_persist_token(errors=errors,
283
+ account_id=account_id,
284
+ jwt_token=refresh_token,
285
+ db_conn=db_conn,
286
+ logger=logger)
287
+ # issue the definitive refresh token
288
+ refresh_token = jwt.encode(payload=current_claims,
289
+ key=JWT_ENCODING_KEY,
290
+ algorithm=JWT_DEFAULT_ALGORITHM,
291
+ headers={"kid": f"R{token_id}"})
292
+ # persist it
293
+ db_update(errors=errors,
294
+ update_stmt=f"UPDATE {JWT_DB_TABLE}",
295
+ update_data={JWT_DB_COL_TOKEN: refresh_token},
296
+ where_data={JWT_DB_COL_KID: token_id},
297
+ connection=db_conn,
298
+ logger=logger)
299
+ # commit the transaction
300
+ db_commit(errors=errors,
301
+ connection=db_conn,
302
+ logger=logger)
303
+ if errors:
304
+ raise RuntimeError("; ".join(errors))
305
+
306
+ # issue the access token
307
+ current_claims["exp"] = just_now + account_data.get("access-max-age")
308
+ # may raise an exception
309
+ access_token: str = jwt.encode(payload=current_claims,
314
310
  key=JWT_ENCODING_KEY,
315
311
  algorithm=JWT_DEFAULT_ALGORITHM,
316
- headers={"kid": f"R{token_id}"})
317
- # persist it
318
- db_update(errors=errors,
319
- update_stmt=f"UPDATE {JWT_DB_TABLE}",
320
- update_data={JWT_DB_COL_TOKEN: refresh_token},
321
- where_data={JWT_DB_COL_KID: token_id},
322
- connection=db_conn,
323
- logger=logger)
324
- # commit the transaction
325
- db_commit(errors=errors,
326
- connection=db_conn,
327
- logger=logger)
328
- if errors:
329
- raise RuntimeError("; ".join(errors))
330
-
331
- # issue the access token
332
- current_claims["exp"] = just_now + account_data.get("access-max-age")
333
- # may raise an exception
334
- access_token: str = jwt.encode(payload=current_claims,
335
- key=JWT_ENCODING_KEY,
336
- algorithm=JWT_DEFAULT_ALGORITHM,
337
- headers={"kid": f"A{token_id}"})
338
- # return the token data
339
- result = {
340
- "access_token": access_token,
341
- "created_in": current_claims.get("iat"),
342
- "expires_in": current_claims.get("exp"),
343
- "refresh_token": refresh_token
344
- }
345
-
346
- return result
312
+ headers={"kid": f"A{token_id}"})
313
+ # return the token data
314
+ return {
315
+ "access-token": access_token,
316
+ "created-in": current_claims.get("iat"),
317
+ "expires-in": current_claims.get("exp"),
318
+ "refresh-token": refresh_token
319
+ }
347
320
 
348
321
  def __get_account_data(self,
349
322
  account_id: str,
@@ -355,7 +328,7 @@ class JwtRegistry:
355
328
  :raises RuntimeError: No JWT access data exists for *account_id*
356
329
  """
357
330
  # retrieve the access data
358
- result: dict[str, Any] = self.access_data.get(account_id)
331
+ result: dict[str, Any] = self.access_registry.get(account_id)
359
332
  if not result:
360
333
  # JWT access data not found
361
334
  err_msg: str = f"No JWT access data found for '{account_id}'"
@@ -366,60 +339,6 @@ class JwtRegistry:
366
339
  return result
367
340
 
368
341
 
369
- def _jwt_request_token(errors: list[str],
370
- remote_url: str,
371
- claims: dict[str, Any],
372
- timeout: int = None,
373
- logger: Logger = None) -> dict[str, Any]:
374
- """
375
- Obtain and return the JWT token from *reference_url*, along with its duration.
376
-
377
- Expected structure of the return data:
378
- {
379
- "access_token": <jwt-token>,
380
- "created_in": <timestamp>,
381
- "expires_in": <seconds-to-expiration>,
382
- "refresh_token": <token>
383
- }
384
- It is up to the invoker to make sure that the *claims* data conform to the requirements
385
- of the provider issuing the JWT token.
386
-
387
- :param errors: incidental errors
388
- :param remote_url: the reference URL for obtaining JWT tokens
389
- :param claims: the JWT claimset, as expected by the issuing server
390
- :param timeout: request timeout, in seconds (defaults to *None*)
391
- :param logger: optional logger
392
- """
393
- # initialize the return variable
394
- result: dict[str, Any] | None = None
395
-
396
- # request the JWT token
397
- if logger:
398
- logger.debug(f"POST request JWT token to '{remote_url}'")
399
- response: Response = requests.post(
400
- url=remote_url,
401
- json=claims,
402
- timeout=timeout
403
- )
404
-
405
- # was the request successful ?
406
- if response.status_code in [200, 201, 202]:
407
- # yes, save the access token data returned
408
- result = response.json()
409
- if logger:
410
- logger.debug(f"JWT token obtained: {result}")
411
- else:
412
- # no, report the problem
413
- err_msg: str = f"POST request to '{remote_url}' failed: {response.reason}"
414
- if response.text:
415
- err_msg += f" - {response.text}"
416
- if logger:
417
- logger.error(err_msg)
418
- errors.append(err_msg)
419
-
420
- return result
421
-
422
-
423
342
  def _jwt_persist_token(errors: list[str],
424
343
  account_id: str,
425
344
  jwt_token: str,
@@ -468,7 +387,6 @@ def _jwt_persist_token(errors: list[str],
468
387
  token_kid: int = rec[0]
469
388
  token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
470
389
  token=token,
471
- validate=False,
472
390
  logger=logger)
473
391
  if errors:
474
392
  raise RuntimeError("; ".join(errors))
File without changes
File without changes
File without changes