pypomes-jwt 0.8.9__py3-none-any.whl → 0.9.0__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/jwt_data.py CHANGED
@@ -1,8 +1,8 @@
1
- import base64
2
1
  import jwt
3
2
  import requests
4
3
  import string
5
4
  import sys
5
+ from base64 import urlsafe_b64encode
6
6
  from datetime import datetime, timezone
7
7
  from logging import Logger
8
8
  from pypomes_core import str_random
@@ -33,11 +33,13 @@ class JwtData:
33
33
  "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
34
34
  "refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
35
35
  "grace-interval": <int> # time to wait for token to be valid, in seconds
36
+ # optional
36
37
  "token-audience": <string> # the audience the token is intended for
37
38
  "token_nonce": <string> # value used to associate a client session with a token
38
39
  "claims": {
39
40
  "valid-from": <string> # token's start (<YYYY-MM-DDThh:mm:ss+00:00>)
40
41
  "valid-until": <string> # token's finish (<YYYY-MM-DDThh:mm:ss+00:00>)
42
+ # optional
41
43
  "birthdate": <string>, # subject's birth date
42
44
  "email": <string>, # subject's email
43
45
  "gender": <string>, # subject's gender
@@ -63,7 +65,6 @@ class JwtData:
63
65
  "jti": <string> # JWT id
64
66
  "sub": <string> # subject (the account identification)
65
67
  "nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
66
- # optional:
67
68
  "aud": <string> # token audience
68
69
  "nbt": <timestamp> # not before time
69
70
 
@@ -81,8 +82,10 @@ class JwtData:
81
82
  The token header has these items:
82
83
  "alg": <string> # the algorithm used to sign the token (one of 'HS256', 'HS512', 'RSA256', 'RSA512')
83
84
  "typ": <string> # the token type (fixed to 'JWT'
84
- "kid": <string> # a reference to the encoding/decoding keys used
85
- # (if issued by the local server, holds the public key, if assimetric keys were used)
85
+ "kid": <string> # a reference to the token type and the key to its location in the token database
86
+
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.
86
89
  """
87
90
  def __init__(self) -> None:
88
91
  """
@@ -242,17 +245,23 @@ class JwtData:
242
245
  # JWT service is being provided locally
243
246
  just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
244
247
  current_claims["iat"] = just_now
245
-
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()
246
256
  # issue a candidate refresh token first, and persist it
247
257
  current_claims["exp"] = just_now + account_data.get("refresh-max-age")
248
- current_claims["valid-from"] = datetime.fromtimestamp(timestamp=current_claims["iat"],
249
- tz=timezone.utc).isoformat()
250
258
  current_claims["valid-until"] = datetime.fromtimestamp(timestamp=current_claims["exp"],
251
259
  tz=timezone.utc).isoformat()
252
260
  # may raise an exception
253
261
  refresh_token: str = jwt.encode(payload=current_claims,
254
262
  key=JWT_ENCODING_KEY,
255
- algorithm=JWT_DEFAULT_ALGORITHM)
263
+ algorithm=JWT_DEFAULT_ALGORITHM,
264
+ headers={"kid": "R0"})
256
265
  # obtain a DB connection (may raise an exception)
257
266
  db_conn: Any = db_connect(errors=errors,
258
267
  logger=logger)
@@ -266,7 +275,7 @@ class JwtData:
266
275
  refresh_token = jwt.encode(payload=current_claims,
267
276
  key=JWT_ENCODING_KEY,
268
277
  algorithm=JWT_DEFAULT_ALGORITHM,
269
- headers={"kid": str(token_id)})
278
+ headers={"kid": f"R{token_id}"})
270
279
  # persist it
271
280
  db_update(errors=errors,
272
281
  update_stmt=f"UPDATE {JWT_DB_TABLE}",
@@ -286,7 +295,8 @@ class JwtData:
286
295
  # may raise an exception
287
296
  access_token: str = jwt.encode(payload=current_claims,
288
297
  key=JWT_ENCODING_KEY,
289
- algorithm=JWT_DEFAULT_ALGORITHM)
298
+ algorithm=JWT_DEFAULT_ALGORITHM,
299
+ headers={"kid": f"A{token_id}"})
290
300
  # return the token data
291
301
  result = {
292
302
  "access_token": access_token,
@@ -453,7 +463,7 @@ def _jwt_persist_token(errors: list[str],
453
463
  insert_data={JWT_DB_COL_ACCOUNT: account_id,
454
464
  JWT_DB_COL_TOKEN: jwt_token,
455
465
  JWT_DB_COL_ALGORITHM: JWT_DEFAULT_ALGORITHM,
456
- JWT_DB_COL_DECODER: base64.urlsafe_b64encode(JWT_DECODING_KEY).decode()},
466
+ JWT_DB_COL_DECODER: urlsafe_b64encode(JWT_DECODING_KEY).decode()},
457
467
  connection=db_conn,
458
468
  logger=logger)
459
469
  if errors:
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,9 +1,9 @@
1
- import base64
2
1
  import jwt
2
+ from base64 import urlsafe_b64decode
3
3
  from flask import Request, Response, request
4
4
  from logging import Logger
5
5
  from pypomes_db import db_select, db_delete
6
- from typing import Any, Literal
6
+ from typing import Any
7
7
 
8
8
  from . import JWT_DB_COL_ACCOUNT
9
9
  from .jwt_constants import (
@@ -61,7 +61,7 @@ def jwt_verify_request(request: Request,
61
61
  logger.debug(msg="Token was found")
62
62
  errors: list[str] = []
63
63
  jwt_validate_token(errors=errors,
64
- nature="A",
64
+ nature=["A"],
65
65
  token=token)
66
66
  if errors:
67
67
  err_msg = "; ".join(errors)
@@ -157,17 +157,20 @@ def jwt_remove_account(account_id: str,
157
157
 
158
158
  def jwt_validate_token(errors: list[str] | None,
159
159
  token: str,
160
- nature: Literal["A", "R"] = None,
160
+ nature: 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
170
 
168
171
  :param errors: incidental error messages
169
172
  :param token: the token to be validated
170
- :param nature: optionally validate the token's nature ("A": access token, "R": refresh token)
173
+ :param nature: one of more prefixes identifying the nature of locally issued tokens
171
174
  :param account_id: optionally, validate the token's account owner
172
175
  :param logger: optional logger
173
176
  :return: The token's claims (header and payload) if if is valid, *None* otherwise
@@ -179,17 +182,16 @@ def jwt_validate_token(errors: list[str] | None,
179
182
 
180
183
  # extract needed data from token header
181
184
  token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
182
- token_kid: int = int(token_header.get("kid") or 0)
185
+ token_kid: str = token_header.get("kid")
183
186
  token_alg: str | None = None
184
187
  token_decoder: bytes | None = None
185
188
  op_errors: list[str] = []
186
189
 
187
190
  # retrieve token data from database
188
- if (nature == "R" and not token_kid) or (nature == "A" and token_kid):
189
- nat: str = "an access" if nature == "A" else "a refresh"
190
- op_errors.append(f"Token is not {nat} token")
191
+ if nature and not (token_kid and token_kid[0:1] in nature):
192
+ op_errors.append("Invalid token")
191
193
  elif token_kid:
192
- where_data: dict[str, str] = {JWT_DB_COL_KID: token_kid}
194
+ where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
193
195
  if account_id:
194
196
  where_data[JWT_DB_COL_ACCOUNT] = account_id
195
197
  recs: list[tuple[str]] = db_select(errors=op_errors,
@@ -199,7 +201,7 @@ def jwt_validate_token(errors: list[str] | None,
199
201
  logger=logger)
200
202
  if recs:
201
203
  token_alg = recs[0][0]
202
- token_decoder = base64.urlsafe_b64decode(recs[0][1])
204
+ token_decoder = urlsafe_b64decode(recs[0][1])
203
205
  else:
204
206
  op_errors.append("Invalid token")
205
207
  else:
@@ -215,9 +217,7 @@ def jwt_validate_token(errors: list[str] | None,
215
217
  # ExpiredSignatureError: token and refresh period have expired
216
218
  # InvalidSignatureError: signature does not match the one provided as part of the token
217
219
  # ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
218
- # InvalidAudienceError: 'aud' claim does not match one of the expected audience
219
220
  # InvalidAlgorithmError: the specified algorithm is not recognized
220
- # InvalidIssuerError: 'iss' claim does not match the expected issuer
221
221
  # InvalidIssuedAtError: 'iat' claim is non-numeric
222
222
  # MissingRequiredClaimError: a required claim is not contained in the claimset
223
223
  payload: dict[str, Any] = jwt.decode(jwt=token,
@@ -227,7 +227,7 @@ def jwt_validate_token(errors: list[str] | None,
227
227
  "verify_nbf": True
228
228
  },
229
229
  key=token_decoder,
230
- require=["exp"],
230
+ require=["iat", "iss", "exp", "sub"],
231
231
  algorithms=token_alg)
232
232
  if account_id and payload.get("sub") != account_id:
233
233
  op_errors.append("Token does not belong to account")
@@ -275,15 +275,15 @@ def jwt_revoke_token(errors: list[str] | None,
275
275
  op_errors: list[str] = []
276
276
  token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
277
277
  token=refresh_token,
278
- nature="R",
278
+ nature=["A", "R"],
279
279
  account_id=account_id,
280
280
  logger=logger)
281
281
  if not op_errors:
282
- token_kid: int = int(token_claims["header"].get("kid") or 0)
282
+ token_kid: str = token_claims["header"].get("kid")
283
283
  db_delete(errors=op_errors,
284
284
  delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
285
285
  where_data={
286
- JWT_DB_COL_KID: token_kid,
286
+ JWT_DB_COL_KID: int(token_kid[1:]),
287
287
  JWT_DB_COL_ACCOUNT: account_id
288
288
  },
289
289
  logger=logger)
@@ -334,7 +334,7 @@ def jwt_get_tokens(errors: list[str] | None,
334
334
  # verify whether this refresh token is legitimate
335
335
  account_claims = (jwt_validate_token(errors=op_errors,
336
336
  token=refresh_token,
337
- nature="R",
337
+ nature=["R"],
338
338
  account_id=account_id,
339
339
  logger=logger) or {}).get("payload")
340
340
  if account_claims:
@@ -380,7 +380,7 @@ def jwt_get_claims(errors: list[str] | None,
380
380
  "header": {
381
381
  "alg": "RS256",
382
382
  "typ": "JWT",
383
- "kid": "1234"
383
+ "kid": "A1234"
384
384
  },
385
385
  "payload": {
386
386
  "valid-from": <YYYY-MM-DDThh:mm:ss+00:00>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.8.9
3
+ Version: 0.9.0
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,4 +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.5
16
- Requires-Dist: pypomes-db>=1.9.6
16
+ Requires-Dist: pypomes-db>=1.9.7
@@ -0,0 +1,8 @@
1
+ pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
2
+ pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
3
+ pypomes_jwt/jwt_data.py,sha256=keC95-2DgWYZIzAtYEbtLtfkFSH-V_sTBiTbszsZqho,23286
4
+ pypomes_jwt/jwt_pomes.py,sha256=0C4u9UnzqM1ZWTW4rpw18kyvnR1biLEq5nAwZ8MMrz8,17244
5
+ pypomes_jwt-0.9.0.dist-info/METADATA,sha256=0KthAWPxm04v3KMnYfsaHal4_kN0tFj8er2a52A__AI,632
6
+ pypomes_jwt-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.9.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.9.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=P7rT6ZVE2BzU3ntYOr83H5iOf5JcCmjDUYakNbrRAP0,1266
2
- pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
3
- pypomes_jwt/jwt_data.py,sha256=FU4zvFr-1ZbClM8ozt9asR2EGZe-xYX3046dxWnGays,22529
4
- pypomes_jwt/jwt_pomes.py,sha256=XWnP4U2K5FPbP-N5npQboNdMZ-MMLpXTfd8Yf82aFls,17217
5
- pypomes_jwt-0.8.9.dist-info/METADATA,sha256=orVERU_gL14nmPjxxNMrxyEKy3q4LLBD2vMokmyECSQ,632
6
- pypomes_jwt-0.8.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-0.8.9.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-0.8.9.dist-info/RECORD,,