pypomes-jwt 0.8.1__py3-none-any.whl → 0.8.3__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.
pypomes_jwt/__init__.py CHANGED
@@ -1,27 +1,29 @@
1
1
  from .jwt_constants import (
2
2
  JWT_DB_ENGINE, JWT_DB_HOST, JWT_DB_NAME,
3
3
  JWT_DB_PORT, JWT_DB_USER, JWT_DB_PWD,
4
- JWT_DB_TABLE, JWT_DB_COL_ACCOUNT, JWT_DB_COL_TOKEN,
4
+ JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
5
+ JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN,
5
6
  JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
6
7
  JWT_ENCODING_KEY, JWT_DECODING_KEY
7
8
  )
8
9
  from .jwt_pomes import (
9
10
  jwt_needed, jwt_verify_request,
10
- jwt_get_tokens, jwt_get_claims, jwt_validate_token,
11
- jwt_assert_account, jwt_set_account, jwt_remove_account, jwt_revoke_token
11
+ jwt_assert_account, jwt_set_account, jwt_remove_account,
12
+ jwt_get_tokens, jwt_get_claims, jwt_validate_token, jwt_revoke_token
12
13
  )
13
14
 
14
15
  __all__ = [
15
16
  # jwt_constants
16
17
  "JWT_DB_ENGINE", "JWT_DB_HOST", "JWT_DB_NAME",
17
18
  "JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
18
- "JWT_DB_TABLE", "JWT_DB_COL_ACCOUNT", "JWT_DB_COL_TOKEN",
19
+ "JWT_DB_TABLE", "JWT_DB_COL_KID", "JWT_DB_COL_ACCOUNT",
20
+ "JWT_DB_COL_ALGORITHM", "JWT_DB_COL_DECODER", "JWT_DB_COL_TOKEN",
19
21
  "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
20
22
  "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
21
23
  # jwt_pomes
22
24
  "jwt_needed", "jwt_verify_request",
23
- "jwt_get_tokens", "jwt_get_claims", "jwt_validate_token",
24
- "jwt_assert_account", "jwt_set_account", "jwt_remove_account", "jwt_revoke_token"
25
+ "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
26
+ "jwt_get_tokens", "jwt_get_claims", "jwt_validate_token", "jwt_revoke_token"
25
27
  ]
26
28
 
27
29
  from importlib.metadata import version
@@ -5,7 +5,9 @@ from pypomes_core import (
5
5
  APP_PREFIX,
6
6
  env_get_str, env_get_bytes, env_get_int
7
7
  )
8
+ from pypomes_db import DbEngine, db_setup
8
9
  from secrets import token_bytes
10
+ from sys import stderr
9
11
  from typing import Final
10
12
 
11
13
  # database specs for token persistence
@@ -18,14 +20,14 @@ JWT_DB_CLIENT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_CLIENT") # fo
18
20
  JWT_DB_DRIVER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_DRIVER") # for SQLServer, only
19
21
  JWT_DB_TABLE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
20
22
  JWT_DB_COL_ACCOUNT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ACCOUNT")
21
- JWT_DB_COL_HASH: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_HASH")
23
+ JWT_DB_COL_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ALGORITHM")
24
+ JWT_DB_COL_DECODER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_DECODER")
25
+ JWT_DB_COL_KID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_KID")
22
26
  JWT_DB_COL_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_TOKEN")
23
- # define the database engine
24
- __db_engine: str | None = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
25
- if __db_engine:
26
- from pypomes_db import DbEngine, db_setup, db_assert_access, db_delete
27
- from sys import stderr
28
- if db_setup(engine=DbEngine(__db_engine),
27
+
28
+ # define and validate the database engine
29
+ JWT_DB_ENGINE: Final[DbEngine] = DbEngine(env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE"))
30
+ if not db_setup(engine=JWT_DB_ENGINE,
29
31
  db_name=JWT_DB_NAME,
30
32
  db_user=JWT_DB_USER,
31
33
  db_pwd=JWT_DB_PWD,
@@ -33,22 +35,11 @@ if __db_engine:
33
35
  db_port=JWT_DB_PORT,
34
36
  db_client=JWT_DB_CLIENT,
35
37
  db_driver=JWT_DB_DRIVER):
36
- __errors: list[str] = []
37
- if not db_assert_access(errors=__errors) or \
38
- db_delete(errors=__errors,
39
- delete_stmt=f"DELETE FROM {JWT_DB_TABLE}") is None:
40
- stderr.write(f"{'; '.join(__errors)}\n")
41
- __db_engine = None
42
- else:
43
- stderr.write("Invalid database parameters\n")
44
- __db_engine = None
45
- # if set to 'None', no further attempt will be made to access the database
46
- JWT_DB_ENGINE: Final[DbEngine] = DbEngine(__db_engine) if __db_engine else None
38
+ stderr.write("Invalid database parameters\n")
47
39
 
48
- # one of HS256, HS512, RSA256, RSA512
40
+ # one of HS256, HS512, RS256, RS512
49
41
  JWT_DEFAULT_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
50
42
  def_value="RS256")
51
-
52
43
  # recommended: between 5 min and 1 hour (set to 5 min)
53
44
  JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_AGE",
54
45
  def_value=300)
pypomes_jwt/jwt_data.py CHANGED
@@ -1,17 +1,19 @@
1
- import hashlib
2
1
  import jwt
3
2
  import requests
4
3
  import string
4
+ import sys
5
5
  from datetime import datetime, timezone
6
6
  from logging import Logger
7
7
  from pypomes_core import str_random
8
+ from pypomes_db import db_connect, db_commit, db_update, db_delete
8
9
  from requests import Response
9
10
  from threading import Lock
10
11
  from typing import Any
11
12
 
12
13
  from .jwt_constants import (
13
- JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY, JWT_DECODING_KEY, JWT_ACCOUNT_LIMIT,
14
- JWT_DB_ENGINE, JWT_DB_TABLE, JWT_DB_COL_ACCOUNT, JWT_DB_COL_HASH, JWT_DB_COL_TOKEN
14
+ JWT_DEFAULT_ALGORITHM, JWT_ACCOUNT_LIMIT, JWT_ENCODING_KEY, JWT_DECODING_KEY,
15
+ JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ACCOUNT,
16
+ JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER, JWT_DB_COL_TOKEN
15
17
  )
16
18
 
17
19
 
@@ -149,12 +151,10 @@ class JwtData:
149
151
  with self.access_lock:
150
152
  account_data = self.access_data.pop(account_id, None)
151
153
 
152
- if account_data and JWT_DB_ENGINE:
153
- from pypomes_db import db_delete
154
- db_delete(errors=None,
155
- delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
156
- where_data={JWT_DB_COL_ACCOUNT: account_id},
157
- logger=logger)
154
+ db_delete(errors=None,
155
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
156
+ where_data={JWT_DB_COL_ACCOUNT: account_id},
157
+ logger=logger)
158
158
  if logger:
159
159
  if account_data:
160
160
  logger.debug(f"Removed JWT data for '{account_id}'")
@@ -192,8 +192,7 @@ class JwtData:
192
192
  :raises InvalidIssuerError: 'iss' claim does not match the expected issuer
193
193
  :raises InvalidIssuedAtError: 'iat' claim is non-numeric
194
194
  :raises MissingRequiredClaimError: a required claim is not contained in the claimset
195
- :raises RuntimeError: error accessing the revocation database, or
196
- the remote JWT provider failed to return a token
195
+ :raises RuntimeError: error accessing the token database
197
196
  """
198
197
  # initialize the return variable
199
198
  result: dict[str, Any] | None = None
@@ -209,8 +208,6 @@ class JwtData:
209
208
  current_claims: dict[str, Any] = account_data.get("claims").copy()
210
209
  if account_claims:
211
210
  current_claims.update(account_claims)
212
-
213
- # obtain new tokens
214
211
  current_claims["jti"] = str_random(size=32,
215
212
  chars=string.ascii_letters + string.digits)
216
213
  current_claims["sub"] = account_id
@@ -233,41 +230,52 @@ class JwtData:
233
230
  timeout=account_data.get("request-timeout"),
234
231
  logger=logger)
235
232
  if errors:
236
- raise RuntimeError(" - ".join(errors))
233
+ raise RuntimeError("; ".join(errors))
237
234
  else:
238
235
  # JWT service is being provided locally
239
236
  just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
240
237
  current_claims["iat"] = just_now
241
- token_header: dict[str, Any] = None \
242
- if JWT_DEFAULT_ALGORITHM not in ["RSA256", "RSA512"] \
243
- else {"kid": JWT_DECODING_KEY}
244
-
245
- # issue the access token first
246
- current_claims["nat"] = "A"
247
- current_claims["exp"] = just_now + account_data.get("access-max-age")
248
- # may raise an exception
249
- access_token: str = jwt.encode(payload=current_claims,
250
- key=JWT_ENCODING_KEY,
251
- algorithm=JWT_DEFAULT_ALGORITHM,
252
- headers=token_header)
253
238
 
254
- # then issue the refresh token
239
+ # issue a candidate refresh token first, and persist it
255
240
  current_claims["exp"] = just_now + account_data.get("refresh-max-age")
256
- current_claims["nat"] = "R"
257
241
  # may raise an exception
258
242
  refresh_token: str = jwt.encode(payload=current_claims,
259
243
  key=JWT_ENCODING_KEY,
260
- algorithm=JWT_DEFAULT_ALGORITHM,
261
- headers=token_header)
262
- if JWT_DB_ENGINE:
263
- # persist the refresh token
264
- _jwt_persist_token(errors=errors,
265
- account_id=account_id,
266
- jwt_token=refresh_token,
267
- logger=logger)
268
- if errors:
269
- raise RuntimeError("; ".join(errors))
244
+ algorithm=JWT_DEFAULT_ALGORITHM)
245
+ # obtain a DB connection (may raise an exception)
246
+ db_conn: Any = db_connect(errors=errors,
247
+ logger=logger)
248
+ # persist the candidate token (may raise an exception)
249
+ token_id: int = _jwt_persist_token(errors=errors,
250
+ account_id=account_id,
251
+ jwt_token=refresh_token,
252
+ db_conn=db_conn,
253
+ logger=logger)
254
+ # issue the definitive refresh token
255
+ refresh_token = jwt.encode(payload=current_claims,
256
+ key=JWT_ENCODING_KEY,
257
+ algorithm=JWT_DEFAULT_ALGORITHM,
258
+ headers={"kid": str(token_id)})
259
+ # persist it
260
+ db_update(errors=errors,
261
+ update_stmt=f"UPDATE {JWT_DB_TABLE}",
262
+ update_data={JWT_DB_COL_TOKEN: refresh_token},
263
+ where_data={JWT_DB_COL_KID: token_id},
264
+ connection=db_conn,
265
+ logger=logger)
266
+ # commit the transaction
267
+ db_commit(errors=errors,
268
+ connection=db_conn,
269
+ logger=logger)
270
+ if errors:
271
+ raise RuntimeError("; ".join(errors))
270
272
 
273
+ # issue the access token
274
+ current_claims["exp"] = just_now + account_data.get("access-max-age")
275
+ # may raise an exception
276
+ access_token: str = jwt.encode(payload=current_claims,
277
+ key=JWT_ENCODING_KEY,
278
+ algorithm=JWT_DEFAULT_ALGORITHM)
271
279
  # return the token data
272
280
  result = {
273
281
  "access_token": access_token,
@@ -342,64 +350,109 @@ def _jwt_request_token(errors: list[str],
342
350
  def _jwt_persist_token(errors: list[str],
343
351
  account_id: str,
344
352
  jwt_token: str,
345
- logger: Logger = None) -> None:
353
+ db_conn: Any = None,
354
+ logger: Logger = None) -> int:
346
355
  """
347
356
  Persist the given token, making sure that the account limit is adhered to.
348
357
 
358
+ The tokens in storage, associated with *account_id*, are examined for their expiration timestamp.
359
+ If a token's expiration timestamp is in the past, it is removed from storage. If the maximum number
360
+ of active tokens for *account_id* has been reached, the oldest active one is alse removed,
361
+ to make room for the new *jwt_token*.
362
+
363
+ If *db_conn* is provided, then all DB operations will be carried out in the scope of a single transaction.
364
+
349
365
  :param errors: incidental errors
350
366
  :param account_id: the account identification
351
367
  :param jwt_token: the JWT token to persist
368
+ :param db_conn: the database connection to use
352
369
  :param logger: optional logger
370
+ :return: the storage id of the inserted token
371
+ :raises RuntimeError: error accessing the revocation database
353
372
  """
354
373
  from pypomes_db import db_select, db_insert, db_delete
355
374
  from .jwt_pomes import jwt_get_claims
356
375
 
357
376
  # retrieve the account's tokens
358
- recs: list[tuple[str]] = db_select(errors=errors,
359
- sel_stmt=f"SELECT {JWT_DB_COL_HASH}, {JWT_DB_COL_TOKEN} FROM {JWT_DB_TABLE} ",
360
- where_data={JWT_DB_COL_ACCOUNT: account_id})
361
- if not errors:
377
+ # noinspection PyTypeChecker
378
+ recs: list[tuple[int, str, str, str]] = \
379
+ db_select(errors=errors,
380
+ sel_stmt=f"SELECT {JWT_DB_COL_KID}, {JWT_DB_COL_TOKEN} "
381
+ f"FROM {JWT_DB_TABLE}",
382
+ where_data={JWT_DB_COL_ACCOUNT: account_id},
383
+ connection=db_conn)
384
+ if errors:
385
+ raise RuntimeError("; ".join(errors))
386
+
387
+ if logger:
388
+ logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
389
+ # remove the expired tokens
390
+ oldest: int = sys.maxsize
391
+ surplus: int | None = None
392
+ expired: list[int] = []
393
+ for rec in recs:
394
+ token: str = rec[1]
395
+ token_kid: int = rec[0]
396
+ token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
397
+ token=token,
398
+ validate=False,
399
+ logger=logger)
400
+ if errors:
401
+ raise RuntimeError("; ".join(errors))
402
+
403
+ exp: int = token_claims["payload"].get("exp")
404
+ if exp < datetime.now(tz=timezone.utc).timestamp():
405
+ expired.append(token_kid)
406
+ elif exp < oldest:
407
+ oldest = exp
408
+ surplus = token_kid
409
+
410
+ # remove expired tokens from persistence
411
+ # ruff: noqa: SIM102
412
+ if expired:
413
+ db_delete(errors=errors,
414
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
415
+ where_data={JWT_DB_COL_KID: expired},
416
+ connection=db_conn,
417
+ logger=logger)
418
+ if errors:
419
+ raise RuntimeError("; ".join(errors))
420
+ if logger:
421
+ logger.debug(msg=f"{len(expired)} tokens of account "
422
+ f"'{account_id}' removed from storage")
423
+
424
+ if 0 < JWT_ACCOUNT_LIMIT <= len(recs) - len(expired):
425
+ # delete the oldest persisted token to make way for the new one
426
+ db_delete(errors=errors,
427
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
428
+ where_data={JWT_DB_COL_KID: surplus},
429
+ connection=db_conn,
430
+ logger=logger)
431
+ if errors:
432
+ raise RuntimeError("; ".join(errors))
362
433
  if logger:
363
- logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
364
- # remove the expired tokens
365
- expired: list[str] = []
366
- for rec in recs:
367
- token: str = rec[1]
368
- token_hash: str = rec[0]
369
- token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
370
- token=token,
371
- validate=False,
372
- logger=logger)
373
- if errors:
374
- break
375
- exp: int = token_claims["payload"]["exp"]
376
- if exp < datetime.now(tz=timezone.utc).timestamp():
377
- expired.append(token_hash)
378
-
379
- if not errors:
380
- # remove expired tokens from persistence
381
- # ruff: noqa: SIM102
382
- if expired:
383
- if db_delete(errors=errors,
384
- delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
385
- where_data={
386
- JWT_DB_COL_ACCOUNT: account_id,
387
- JWT_DB_COL_HASH: expired
388
- },
389
- logger=logger) is not None:
390
- if logger:
391
- logger.debug(msg=f"{len(expired)} tokens removed from storage")
392
- if 0 < JWT_ACCOUNT_LIMIT <= len(recs) - len(expired):
393
- errors.append("Maximum number of active sessions "
394
- f"({JWT_ACCOUNT_LIMIT}) exceeded for account '{account_id}'")
395
- # persist token
396
- if not errors:
397
- # ruff: noqa: S324
398
- hasher = hashlib.new(name="md5",
399
- data=jwt_token.encode())
400
- token_hash: str = hasher.digest().hex()
401
- db_insert(errors=errors,
402
- insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
403
- insert_data={JWT_DB_COL_ACCOUNT: account_id,
404
- JWT_DB_COL_HASH: token_hash,
405
- JWT_DB_COL_TOKEN: jwt_token})
434
+ logger.debug(msg="Oldest active token of account "
435
+ f"'{account_id}' removed from storage")
436
+ # persist token
437
+ db_insert(errors=errors,
438
+ insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
439
+ insert_data={JWT_DB_COL_ACCOUNT: account_id,
440
+ JWT_DB_COL_TOKEN: jwt_token,
441
+ JWT_DB_COL_ALGORITHM: JWT_DEFAULT_ALGORITHM,
442
+ JWT_DB_COL_DECODER: JWT_DECODING_KEY.hex()},
443
+ connection=db_conn,
444
+ logger=logger)
445
+ if errors:
446
+ raise RuntimeError("; ".join(errors))
447
+
448
+ # obtain the token's storage id
449
+ reply: list[tuple[int]] = db_select(errors=errors,
450
+ sel_stmt=f"SELECT {JWT_DB_COL_KID} "
451
+ f"FROM {JWT_DB_TABLE}",
452
+ where_data={JWT_DB_COL_TOKEN: jwt_token},
453
+ connection=db_conn,
454
+ logger=logger)
455
+ if errors:
456
+ raise RuntimeError("; ".join(errors))
457
+
458
+ return reply[0][0]
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,14 +1,14 @@
1
- import hashlib
2
1
  import jwt
3
2
  from flask import Request, Response, request
4
3
  from logging import Logger
4
+ from pypomes_db import db_select, db_delete
5
5
  from typing import Any, Literal
6
6
 
7
+ from . import JWT_DB_COL_ACCOUNT
7
8
  from .jwt_constants import (
8
9
  JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
9
10
  JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
10
- JWT_DB_ENGINE, JWT_DB_TABLE,
11
- JWT_DB_COL_ACCOUNT, JWT_DB_COL_HASH, JWT_DB_COL_TOKEN
11
+ JWT_DB_TABLE, JWT_DB_COL_KID, JWT_DB_COL_ALGORITHM, JWT_DB_COL_DECODER
12
12
  )
13
13
  from .jwt_data import JwtData
14
14
 
@@ -74,7 +74,6 @@ def jwt_verify_request(request: Request,
74
74
  logger.error(msg=err_msg)
75
75
  result = Response(response="Authorization failed",
76
76
  status=401)
77
-
78
77
  return result
79
78
 
80
79
 
@@ -158,7 +157,8 @@ def jwt_remove_account(account_id: str,
158
157
  def jwt_validate_token(errors: list[str] | None,
159
158
  token: str,
160
159
  nature: Literal["A", "R"] = None,
161
- logger: Logger = None) -> bool:
160
+ account_id: str = None,
161
+ logger: Logger = None) -> dict[str, Any] | None:
162
162
  """
163
163
  Verify if *token* ia a valid JWT token.
164
164
 
@@ -167,37 +167,87 @@ def jwt_validate_token(errors: list[str] | None,
167
167
  :param errors: incidental error messages
168
168
  :param token: the token to be validated
169
169
  :param nature: optionally validate the token's nature ("A": access token, "R": refresh token)
170
+ :param account_id: optionally, validate the token's account owner
170
171
  :param logger: optional logger
171
- :return: *True* if token is valid, *False* otherwise
172
+ :return: The token's claims (header and payload) if if is valid, *None* otherwise
172
173
  """
174
+ # initialize the return variable
175
+ result: dict[str, Any] | None = None
173
176
  if logger:
174
177
  logger.debug(msg=f"Validate JWT token '{token}'")
175
178
 
176
- err_msg: str | None = None
177
- try:
178
- # raises:
179
- # InvalidTokenError: token is invalid
180
- # InvalidKeyError: authentication key is not in the proper format
181
- # ExpiredSignatureError: token and refresh period have expired
182
- # InvalidSignatureError: signature does not match the one provided as part of the token
183
- claims: dict[str, Any] = jwt.decode(jwt=token,
184
- key=JWT_DECODING_KEY,
185
- algorithms=[JWT_DEFAULT_ALGORITHM])
186
- if nature and nature != claims.get("nat"):
187
- nat: str = "an access" if nature == "A" else "a refresh"
188
- err_msg = f"Token is not {nat} token"
189
- except Exception as e:
190
- err_msg = str(e)
179
+ # extract needed data from token header
180
+ token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
181
+ token_kid: int = int(token_header.get("kid") or 0)
182
+ token_alg: str | None = None
183
+ token_decoder: bytes | None = None
184
+ op_errors: list[str] = []
191
185
 
192
- if err_msg:
186
+ # retrieve token data from database
187
+ if (nature == "R" and not token_kid) or (nature == "A" and token_kid):
188
+ nat: str = "an access" if nature == "A" else "a refresh"
189
+ op_errors.append(f"Token is not {nat} token")
190
+ elif token_kid:
191
+ where_data: dict[str, str] = {JWT_DB_COL_KID: token_kid}
192
+ if account_id:
193
+ where_data[JWT_DB_COL_ACCOUNT] = account_id
194
+ recs: list[tuple[str]] = db_select(errors=op_errors,
195
+ sel_stmt=f"SELECT {JWT_DB_COL_ALGORITHM}, {JWT_DB_COL_DECODER} "
196
+ f"FROM {JWT_DB_TABLE}",
197
+ where_data=where_data,
198
+ logger=logger)
199
+ if recs:
200
+ token_alg = recs[0][0]
201
+ token_decoder = bytes.fromhex(recs[0][1])
202
+ else:
203
+ op_errors.append("Invalid token")
204
+ else:
205
+ token_alg = JWT_DEFAULT_ALGORITHM
206
+ token_decoder = JWT_DECODING_KEY
207
+
208
+ # validate the token
209
+ if not op_errors:
210
+ try:
211
+ # raises:
212
+ # InvalidTokenError: token is invalid
213
+ # InvalidKeyError: authentication key is not in the proper format
214
+ # ExpiredSignatureError: token and refresh period have expired
215
+ # InvalidSignatureError: signature does not match the one provided as part of the token
216
+ # ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
217
+ # InvalidAudienceError: 'aud' claim does not match one of the expected audience
218
+ # InvalidAlgorithmError: the specified algorithm is not recognized
219
+ # InvalidIssuerError: 'iss' claim does not match the expected issuer
220
+ # InvalidIssuedAtError: 'iat' claim is non-numeric
221
+ # MissingRequiredClaimError: a required claim is not contained in the claimset
222
+ payload: dict[str, Any] = jwt.decode(jwt=token,
223
+ options={
224
+ "verify_signature": True,
225
+ "verify_exp": True,
226
+ "verify_nbf": True
227
+ },
228
+ key=token_decoder,
229
+ require=["exp", "nbf"],
230
+ algorithms=token_alg)
231
+ if account_id and payload.get("sub") != account_id:
232
+ op_errors.append("Token does not belong to account")
233
+ else:
234
+ result = {
235
+ "header": token_header,
236
+ "payload": payload
237
+ }
238
+ except Exception as e:
239
+ op_errors.append(str(e))
240
+
241
+ if op_errors:
242
+ err_msg: str = "; ".join(op_errors)
193
243
  if logger:
194
244
  logger.error(msg=err_msg)
195
245
  if isinstance(errors, list):
196
- errors.append(err_msg)
246
+ errors.extend(op_errors)
197
247
  elif logger:
198
248
  logger.debug(msg=f"Token '{token}' is valid")
199
249
 
200
- return err_msg is None
250
+ return result
201
251
 
202
252
 
203
253
  def jwt_revoke_token(errors: list[str] | None,
@@ -222,25 +272,20 @@ def jwt_revoke_token(errors: list[str] | None,
222
272
  logger.debug(msg=f"Revoking refresh token of '{account_id}'")
223
273
 
224
274
  op_errors: list[str] = []
225
- if JWT_DB_ENGINE:
226
- from pypomes_db import db_exists, db_delete
227
- # ruff: noqa: S324
228
- hasher = hashlib.new(name="md5",
229
- data=refresh_token.encode())
230
- token_hash: str = hasher.digest().hex()
231
- if db_exists(errors=op_errors,
232
- table=JWT_DB_TABLE,
233
- where_data={JWT_DB_COL_HASH: token_hash},
234
- logger=logger):
235
- db_delete(errors=errors,
236
- delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
237
- where_data={JWT_DB_COL_HASH: token_hash},
238
- logger=logger)
239
- elif not op_errors:
240
- op_errors.append("Token was not found")
241
- else:
242
- op_errors.append("Database access for token revocation has not been specified")
243
-
275
+ token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
276
+ token=refresh_token,
277
+ nature="R",
278
+ account_id=account_id,
279
+ logger=logger)
280
+ if not op_errors:
281
+ token_kid: int = int(token_claims["header"].get("kid") or 0)
282
+ db_delete(errors=op_errors,
283
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
284
+ where_data={
285
+ JWT_DB_COL_KID: token_kid,
286
+ JWT_DB_COL_ACCOUNT: account_id
287
+ },
288
+ logger=logger)
244
289
  if op_errors:
245
290
  if logger:
246
291
  logger.error(msg="; ".join(op_errors))
@@ -273,7 +318,7 @@ def jwt_get_tokens(errors: list[str] | None,
273
318
 
274
319
  :param errors: incidental error messages
275
320
  :param account_id: the account identification
276
- :param account_claims: if provided, may supercede registered custom claims
321
+ :param account_claims: if provided, may supercede registered claims
277
322
  :param refresh_token: if provided, defines a token refresh operation
278
323
  :param logger: optional logger
279
324
  :return: the JWT token data, or *None* if error
@@ -286,29 +331,24 @@ def jwt_get_tokens(errors: list[str] | None,
286
331
  op_errors: list[str] = []
287
332
  if refresh_token:
288
333
  # verify whether this refresh token is legitimate
289
- if JWT_DB_ENGINE:
290
- from pypomes_db import db_select
291
- recs: list[tuple[str]] = db_select(errors=op_errors,
292
- sel_stmt=f"SELECT {JWT_DB_COL_TOKEN} "
293
- f"FROM {JWT_DB_TABLE}",
294
- where_data={JWT_DB_COL_ACCOUNT: account_id},
295
- logger=logger)
296
- if not op_errors and \
297
- (len(recs) == 0 or recs[0][0] != refresh_token):
298
- op_errors.append("Invalid refresh token")
299
- if not op_errors:
300
- account_claims = jwt_get_claims(errors=op_errors,
301
- token=refresh_token)
302
- if not op_errors and account_claims.get("nat") != "R":
303
- op_errors.append("Invalid parameters")
304
-
334
+ account_claims = (jwt_validate_token(errors=op_errors,
335
+ token=refresh_token,
336
+ nature="R",
337
+ account_id=account_id,
338
+ logger=logger) or {}).get("payload")
339
+ if account_claims:
340
+ account_claims.pop("iat", None)
341
+ account_claims.pop("jti", None)
342
+ account_claims.pop("iss", None)
343
+ account_claims.pop("exp", None)
344
+ account_claims.pop("nbt", None)
305
345
  if not op_errors:
306
346
  try:
307
347
  result = __jwt_data.issue_tokens(account_id=account_id,
308
348
  account_claims=account_claims,
309
349
  logger=logger)
310
350
  if logger:
311
- logger.debug(msg=f"Data is '{result}'")
351
+ logger.debug(msg=f"Token data is '{result}'")
312
352
  except Exception as e:
313
353
  # token issuing failed
314
354
  op_errors.append(str(e))
@@ -324,8 +364,8 @@ def jwt_get_tokens(errors: list[str] | None,
324
364
 
325
365
  def jwt_get_claims(errors: list[str] | None,
326
366
  token: str,
327
- validate: bool = True,
328
- logger: Logger = None) -> dict[str, Any]:
367
+ validate: bool = False,
368
+ logger: Logger = None) -> dict[str, Any] | None:
329
369
  """
330
370
  Obtain and return the claims set of a JWT *token*.
331
371
 
@@ -361,7 +401,7 @@ def jwt_get_claims(errors: list[str] | None,
361
401
 
362
402
  :param errors: incidental error messages
363
403
  :param token: the token to be inspected for claims
364
- :param validate: If *True*, verifies the token's data
404
+ :param validate: If *True*, verifies the token's data (defaults to *False*)
365
405
  :param logger: optional logger
366
406
  :return: the token's claimset, or *None* if error
367
407
  """
@@ -372,26 +412,19 @@ def jwt_get_claims(errors: list[str] | None,
372
412
  logger.debug(msg=f"Retrieve claims for token '{token}'")
373
413
 
374
414
  try:
375
- # retrieve the token's payload
415
+ # retrieve the token's claims
376
416
  if validate:
377
- payload: dict[str, Any] = jwt.decode(jwt=token,
378
- options={
379
- "verify_signature": True,
380
- "verify_exp": True,
381
- "verify_nbf": True
382
- },
383
- key=JWT_DECODING_KEY,
384
- require=["exp", "nbf"],
385
- algorithms=[JWT_DEFAULT_ALGORITHM])
417
+ result = jwt_validate_token(errors=errors,
418
+ token=token,
419
+ logger=logger)
386
420
  else:
421
+ header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
387
422
  payload: dict[str, Any] = jwt.decode(jwt=token,
388
423
  options={"verify_signature": False})
389
- # retrieve the token's header
390
- header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
391
- result = {
392
- "header": header,
393
- "payload": payload
394
- }
424
+ result = {
425
+ "header": header,
426
+ "payload": payload
427
+ }
395
428
  except Exception as e:
396
429
  if logger:
397
430
  logger.error(msg=str(e))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.8.1
3
+ Version: 0.8.3
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
@@ -13,3 +13,4 @@ Requires-Python: >=3.12
13
13
  Requires-Dist: cryptography>=44.0.2
14
14
  Requires-Dist: pyjwt>=2.10.1
15
15
  Requires-Dist: pypomes-core>=1.8.3
16
+ Requires-Dist: pypomes-db>=1.9.5
@@ -0,0 +1,8 @@
1
+ pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
2
+ pypomes_jwt/jwt_constants.py,sha256=FA50jKQ3D09MxXkUpVkXW5IQqm_UX6qm3bU5gHvkU-4,3980
3
+ pypomes_jwt/jwt_data.py,sha256=4WT19eHowrMyXJIRt3nLv8FLxEorgP2k-fgKgYY0Vgk,21534
4
+ pypomes_jwt/jwt_pomes.py,sha256=m-seMYrQLgTrdxR7bH-RMdlY7Jc9QtmAKmkdyEAhQGY,17156
5
+ pypomes_jwt-0.8.3.dist-info/METADATA,sha256=kagRg42MtBTc2zewA2douDPWBYBmeOCsSwSuciJZnNA,632
6
+ pypomes_jwt-0.8.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.8.3.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.8.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=06WdwiP2m5jtrFjpPSacg4fRd2Dh6gVo93xJhmu73J4,1134
2
- pypomes_jwt/jwt_constants.py,sha256=EjdrTP5AptGoOdI0gzsxexmM4lrgm2r0KHX-DyyGhFc,4330
3
- pypomes_jwt/jwt_data.py,sha256=q4KUVOuLXHA9tVIfuVEPo8uZPulElWM04wqtGVxcV-0,19239
4
- pypomes_jwt/jwt_pomes.py,sha256=lACMvNHRVpGgOGmQJ67zbURnR6p4kcxU4UomoZYahto,15246
5
- pypomes_jwt-0.8.1.dist-info/METADATA,sha256=g4cWSIxewY90pfkQ2gLJlLdE_LqHeX2E6mU2CfX0eak,599
6
- pypomes_jwt-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-0.8.1.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-0.8.1.dist-info/RECORD,,