pypomes-jwt 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pypomes-jwt might be problematic. Click here for more details.

pypomes_jwt/__init__.py CHANGED
@@ -1,23 +1,25 @@
1
- from .jwt_data import (
2
- jwt_request_token, jwt_validate_token
1
+ from .jwt_constants import (
2
+ JWT_DB_ENGINE, JWT_DB_HOST, JWT_DB_NAME,
3
+ JWT_DB_PORT, JWT_DB_USER, JWT_DB_PWD,
4
+ JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
5
+ JWT_ENCODING_KEY, JWT_DECODING_KEY
3
6
  )
4
7
  from .jwt_pomes import (
5
- JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
6
- JWT_HS_SECRET_KEY, JWT_RSA_PRIVATE_KEY, JWT_RSA_PUBLIC_KEY,
7
- jwt_needed, jwt_verify_request, jwt_claims, jwt_token,
8
- jwt_get_token_data, jwt_get_token_claims,
9
- jwt_assert_access, jwt_set_access, jwt_remove_access
8
+ jwt_needed, jwt_verify_request,
9
+ jwt_get_tokens, jwt_get_claims, jwt_validate_token,
10
+ jwt_assert_access, jwt_set_access, jwt_remove_access, jwt_revoke_tokens
10
11
  )
11
12
 
12
13
  __all__ = [
13
- # jwt_data
14
- "jwt_request_token", "jwt_validate_token",
15
- # jwt_pomes
14
+ # jwt_constants
15
+ "JWT_DB_ENGINE", "JWT_DB_HOST", "JWT_DB_NAME",
16
+ "JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
16
17
  "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
17
- "JWT_HS_SECRET_KEY", "JWT_RSA_PRIVATE_KEY", "JWT_RSA_PUBLIC_KEY",
18
- "jwt_needed", "jwt_verify_request", "jwt_claims", "jwt_token",
19
- "jwt_get_token_data", "jwt_get_token_claims",
20
- "jwt_assert_access", "jwt_set_access", "jwt_remove_access"
18
+ "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
19
+ # jwt_pomes
20
+ "jwt_needed", "jwt_verify_request",
21
+ "jwt_get_tokens", "jwt_get_claims", "jwt_validate_token",
22
+ "jwt_assert_access", "jwt_set_access", "jwt_remove_access", "jwt_revoke_tokens"
21
23
  ]
22
24
 
23
25
  from importlib.metadata import version
@@ -0,0 +1,76 @@
1
+ from cryptography.hazmat.primitives import serialization
2
+ from cryptography.hazmat.primitives.asymmetric import rsa
3
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
4
+ from pypomes_core import (
5
+ APP_PREFIX,
6
+ env_get_str, env_get_bytes, env_get_int, env_get_bool
7
+ )
8
+ from secrets import token_bytes
9
+ from typing import Final
10
+
11
+ # database specs for token persistence
12
+ JWT_DB_HOST: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_HOST")
13
+ JWT_DB_NAME: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_NAME")
14
+ JWT_DB_PORT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_DB_PORT")
15
+ JWT_DB_USER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_USER")
16
+ JWT_DB_PWD: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_PWD")
17
+ JWT_DB_CLIENT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_CLIENT") # for Oracle, only
18
+ JWT_DB_DRIVER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_DRIVER") # for SQLServer, only
19
+ JWT_DB_TABLE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
20
+ JWT_ROTATE_TOKENS: Final[bool] = env_get_bool(key=f"{APP_PREFIX}_JWT_ROTATE_TOKENS",
21
+ def_value=True)
22
+
23
+ __db_engine: str | None = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
24
+ __rotate_tokens: bool = False
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),
29
+ db_name=JWT_DB_NAME,
30
+ db_user=JWT_DB_USER,
31
+ db_pwd=JWT_DB_PWD,
32
+ db_host=JWT_DB_HOST,
33
+ db_port=JWT_DB_PORT,
34
+ db_client=JWT_DB_CLIENT,
35
+ 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
+ JWT_DB_ENGINE: Final[DbEngine] = DbEngine(__db_engine) if __db_engine else None
46
+
47
+ # one of HS256, HS512, RSA256, RSA512
48
+ JWT_DEFAULT_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
49
+ def_value="HS256")
50
+ # recommended: between 5 min and 1 hour (set to 5 min)
51
+ JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_AGE",
52
+ def_value=300)
53
+ # recommended: at least 2 hours (set to 24 hours)
54
+ JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
55
+ def_value=86400)
56
+
57
+ # recommended: allow the encode and decode keys to be generated anew when app starts
58
+ __encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODE_KEY")
59
+ __decoding_key: bytes
60
+ if JWT_DEFAULT_ALGORITHM in ["HS256", "HS512"]:
61
+ if not __encoding_key:
62
+ __encoding_key = token_bytes(nbytes=32)
63
+ __decoding_key = __encoding_key
64
+ else:
65
+ __decoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_DECODE_KEY")
66
+ if not __encoding_key or not __decoding_key:
67
+ __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
68
+ key_size=2048)
69
+ __encoding_key = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
70
+ format=serialization.PrivateFormat.PKCS8,
71
+ encryption_algorithm=serialization.NoEncryption())
72
+ __pub_key: RSAPublicKey = __priv_key.public_key()
73
+ __decoding_key = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
74
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
75
+ JWT_ENCODING_KEY: Final[bytes] = __encoding_key
76
+ JWT_DECODING_KEY: Final[bytes] = __decoding_key
pypomes_jwt/jwt_data.py CHANGED
@@ -2,12 +2,16 @@ import jwt
2
2
  import requests
3
3
  import string
4
4
  from datetime import datetime, timezone
5
- from jwt.exceptions import InvalidTokenError
6
5
  from logging import Logger
7
6
  from pypomes_core import str_random
8
7
  from requests import Response
9
8
  from threading import Lock
10
- from typing import Any, Literal
9
+ from typing import Any
10
+
11
+ from .jwt_constants import (
12
+ JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY,
13
+ JWT_ROTATE_TOKENS, JWT_DB_ENGINE, JWT_DB_TABLE
14
+ )
11
15
 
12
16
 
13
17
  class JwtData:
@@ -16,171 +20,153 @@ class JwtData:
16
20
 
17
21
  Instance variables:
18
22
  - access_lock: lock for safe multi-threading access
19
- - access_data: list with dictionaries holding the JWT token data:
20
- [
21
- {
22
- "control-data": { # control data
23
- "remote-provider": <bool>, # whether the JWT provider is a remote server
24
- "access-token": <jwt-token>, # access token
25
- "algorithm": <string>, # HS256, HS512, RSA256, RSA512
26
- "request-timeout": <int>, # in seconds - defaults to no timeout
27
- "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
28
- "refresh-exp": <timestamp>, # expiration time for the refresh operation
29
- "hs-secret-key": <bytes>, # HS secret key
30
- "rsa-private-key": <bytes>, # RSA private key
31
- "rsa-public-key": <bytes>, # RSA public key
32
- },
33
- "reserved-claims": { # reserved claims
34
- "exp": <timestamp>, # expiration time
35
- "iat": <timestamp> # issued at
36
- "iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
37
- "jti": <string>, # JWT id
38
- "sub": <string> # subject (the account identification)
39
- # not used:
40
- # "aud": <string> # audience
41
- # "nbt": <timestamp> # not before time
42
- },
43
- "public-claims": { # public claims (may be empty)
23
+ - access_data: dictionary holding the JWT token data, organized by account id:
24
+ {
25
+ <account-id>: {
26
+ "reference-url": # the reference URL
27
+ "remote-provider": <bool>, # whether the JWT provider is a remote server
28
+ "request-timeout": <int>, # in seconds - defaults to no timeout
29
+ "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
30
+ "refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
31
+ "grace-interval": <int> # time to wait for token to be valid, in seconds
32
+ "token-audience": <string> # the audience the token is intended for
33
+ "token_nonce": <string> # value used to associate a client session with a token
34
+ "claims": {
44
35
  "birthdate": <string>, # subject's birth date
45
36
  "email": <string>, # subject's email
46
37
  "gender": <string>, # subject's gender
47
38
  "name": <string>, # subject's name
48
- "roles": <List[str]> # subject roles
49
- },
50
- "custom-claims": { # custom claims (may be empty)
51
- "<custom-claim-key-1>": "<custom-claim-value-1>",
39
+ "roles": <List[str]>, # subject roles
40
+ "nonce": <string>, # value used to associate a Client session with a token
52
41
  ...
53
- "<custom-claim-key-n>": "<custom-claim-value-n>"
54
42
  }
55
43
  },
56
44
  ...
57
- ]
45
+ }
46
+
47
+ JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between
48
+ two parties. It is fully described in the RFC 7519, issued by the Internet Engineering Task Force
49
+ (see https://www.rfc-editor.org/rfc/rfc7519.html).
50
+ In this context, claims are pieces of information a token bears, and herein are loosely classified
51
+ as token-related and account-related. All times are UTC.
52
+
53
+ Token-related claims are mostly required claims, and convey information about the token itself:
54
+ "exp": <timestamp> # expiration time
55
+ "iat": <timestamp> # issued at
56
+ "iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
57
+ "jti": <string> # JWT id
58
+ "sub": <string> # subject (the account identification)
59
+ "nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
60
+ # optional:
61
+ "aud": <string> # token audience
62
+ "nbt": <timestamp> # not before time
63
+
64
+ Account-related claims are optional claims, and convey information about the registered account they belong to.
65
+ Alhough they can be freely specified, these are some of the most commonly used claims:
66
+ "birthdate": <string> # subject's birth date
67
+ "email": <string> # subject's email
68
+ "gender": <string> # subject's gender
69
+ "name": <string> # subject's name
70
+ "roles": <List[str]> # subject roles
71
+ "nonce": <string> # value used to associate a client session with a token
58
72
  """
59
73
  def __init__(self) -> None:
60
74
  """
61
75
  Initizalize the token access data.
62
76
  """
63
77
  self.access_lock: Lock = Lock()
64
- self.access_data: list[dict[str, dict[str, Any]]] = []
65
-
66
- def add_access_data(self,
67
- account_id: str,
68
- reference_url: str,
69
- claims: dict[str, Any],
70
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
71
- access_max_age: int,
72
- refresh_max_age: int,
73
- hs_secret_key: bytes,
74
- rsa_private_key: bytes,
75
- rsa_public_key: bytes,
76
- request_timeout: int,
77
- remote_provider: bool,
78
- logger: Logger = None) -> None:
78
+ self.access_data: dict[str, Any] = {}
79
+
80
+ def add_access(self,
81
+ account_id: str,
82
+ reference_url: str,
83
+ claims: dict[str, Any],
84
+ access_max_age: int,
85
+ refresh_max_age: int,
86
+ grace_interval: int,
87
+ token_audience: str,
88
+ token_nonce: str,
89
+ request_timeout: int,
90
+ remote_provider: bool,
91
+ logger: Logger = None) -> None:
79
92
  """
80
93
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
81
94
 
82
- The parameter *claims* may contain public and custom claims. Currently, the public claims supported
83
- are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else is considered to be custom
84
- claims, and sent to the remote JWT provided, if applicable.
85
-
86
- Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
87
- (typically, an acess-key/hs-secret-key pair), have been previously validated elsewhere.
88
- This situation might change in the future.
95
+ The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
96
+ at a minimum, "birthdate", "email", "gender", "name", and "roles".
97
+ If the token provider is local, then the token-related claims are created at token issuing time.
98
+ If the token provider is remote, all claims are sent to it at token request time.
89
99
 
90
100
  :param account_id: the account identification
91
101
  :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
92
102
  :param claims: the JWT claimset, as key-value pairs
93
- :param algorithm: the algorithm used to sign the token with
94
- :param access_max_age: token duration (in seconds)
95
- :param refresh_max_age: duration for the refresh operation (in seconds)
96
- :param hs_secret_key: secret key for HS authentication
97
- :param rsa_private_key: private key for RSA authentication
98
- :param rsa_public_key: public key for RSA authentication
103
+ :param access_max_age: access token duration, in seconds
104
+ :param refresh_max_age: refresh token duration, in seconds
105
+ :param grace_interval: time to wait for token to be valid, in seconds
106
+ :param token_audience: the audience the token is intended for
107
+ :param token_nonce: optional value used to associate a client session with a token
99
108
  :param request_timeout: timeout for the requests to the reference URL
100
109
  :param remote_provider: whether the JWT provider is a remote server
101
110
  :param logger: optional logger
102
111
  """
103
- # Do the access data already exist ?
104
- if not self.get_access_data(account_id=account_id):
105
- # no, build control data
106
- control_data: dict[str, Any] = {
107
- "algorithm": algorithm,
108
- "access-max-age": access_max_age,
109
- "request-timeout": request_timeout,
110
- "remote-provider": remote_provider,
111
- "refresh-exp": int(datetime.now(tz=timezone.utc).timestamp()) + refresh_max_age
112
- }
113
- if algorithm in ["HS256", "HS512"]:
114
- control_data["hs-secret-key"] = hs_secret_key
115
- else:
116
- control_data["rsa-private-key"] = rsa_private_key
117
- control_data["rsa-public-key"] = rsa_public_key
118
-
119
- # build claims
120
- reserved_claims: dict[str, Any] = {
121
- "sub": account_id,
122
- "iss": reference_url,
123
- "exp": 0,
124
- "iat": 0,
125
- "jti": "<jwt-id>",
126
- }
127
- custom_claims: dict[str, Any] = {}
128
- public_claims: dict[str, Any] = {}
129
- for key, value in claims.items():
130
- if key in ["birthdate", "email", "gender", "name", "roles"]:
131
- public_claims[key] = value
132
- else:
133
- custom_claims[key] = value
134
- # store access data
135
- item_data = {
136
- "control-data": control_data,
137
- "reserved-claims": reserved_claims,
138
- "public-claims": public_claims,
139
- "custom-claims": custom_claims
140
- }
141
- with self.access_lock:
142
- self.access_data.append(item_data)
143
- if logger:
144
- logger.debug(f"JWT data added for '{account_id}': {item_data}")
145
- elif logger:
146
- logger.warning(f"JWT data already exists for '{account_id}'")
147
-
148
- def remove_access_data(self,
149
- account_id: str,
150
- logger: Logger) -> None:
112
+ # build and store the access data for the account
113
+ with self.access_lock:
114
+ if account_id not in self.access_data:
115
+ self.access_data[account_id] = {
116
+ "reference_url": reference_url,
117
+ "access-max-age": access_max_age,
118
+ "refresh-max-age": refresh_max_age,
119
+ "grace-interval": grace_interval,
120
+ "token-audience": token_audience,
121
+ "token-nonce": token_nonce,
122
+ "request-timeout": request_timeout,
123
+ "remote-provider": remote_provider,
124
+ "claims": claims or {}
125
+ }
126
+ if logger:
127
+ logger.debug(f"JWT data added for '{account_id}'")
128
+ elif logger:
129
+ logger.warning(f"JWT data already exists for '{account_id}'")
130
+
131
+ def remove_access(self,
132
+ account_id: str,
133
+ logger: Logger) -> bool:
151
134
  """
152
135
  Remove from storage the access data for *account_id*.
153
136
 
154
137
  :param account_id: the account identification
155
138
  :param logger: optional logger
139
+ return: *True* if the access data was removed, *False* otherwise
156
140
  """
157
- # obtain the access data item in storage
158
- item_data: dict[str, dict[str, Any]] = self.get_access_data(account_id=account_id,
159
- logger=logger)
160
- if item_data:
161
- with self.access_lock:
162
- self.access_data.remove(item_data)
163
- if logger:
141
+ account_data: dict[str, Any] | None
142
+ with self.access_lock:
143
+ account_data = self.access_data.pop(account_id, None)
144
+
145
+ if logger:
146
+ if account_data:
164
147
  logger.debug(f"Removed JWT data for '{account_id}'")
165
- elif logger:
166
- logger.warning(f"No JWT data found for '{account_id}'")
148
+ else:
149
+ logger.warning(f"No JWT data found for '{account_id}'")
167
150
 
168
- def get_token_data(self,
169
- account_id: str,
170
- superceding_claims: dict[str, Any] = None,
171
- logger: Logger = None) -> dict[str, Any]:
151
+ return account_data is not None
152
+
153
+ def issue_tokens(self,
154
+ account_id: str,
155
+ account_claims: dict[str, Any] = None,
156
+ logger: Logger = None) -> dict[str, Any]:
172
157
  """
173
- Obtain and return the JWT token for *account_id*, along with its duration.
158
+ Issue and return the JWT access and refresh tokens for *account_id*.
174
159
 
175
160
  Structure of the return data:
176
161
  {
177
162
  "access_token": <jwt-token>,
178
163
  "created_in": <timestamp>,
179
- "expires_in": <seconds-to-expiration>
164
+ "expires_in": <seconds-to-expiration>,
165
+ "refresh_token": <jwt-token>
180
166
  }
181
167
 
182
168
  :param account_id: the account identification
183
- :param superceding_claims: if provided, may supercede registered custom claims
169
+ :param account_claims: if provided, may supercede registered account-related claims
184
170
  :param logger: optional logger
185
171
  :return: the JWT token data, or *None* if error
186
172
  :raises InvalidTokenError: token is invalid
@@ -193,171 +179,121 @@ class JwtData:
193
179
  :raises InvalidIssuerError: 'iss' claim does not match the expected issuer
194
180
  :raises InvalidIssuedAtError: 'iat' claim is non-numeric
195
181
  :raises MissingRequiredClaimError: a required claim is not contained in the claimset
196
- :raises RuntimeError: access data not found for the given *account_id*, or
182
+ :raises RuntimeError: error accessing the revocation database, or
197
183
  the remote JWT provider failed to return a token
198
184
  """
199
- # declare the return variable
200
- result: dict[str, Any]
201
-
202
- # obtain the item in storage
203
- access_data: dict[str, Any] = self.get_access_data(account_id=account_id,
204
- logger=logger)
205
- # was the JWT data obtained ?
206
- if access_data:
207
- # yes, proceed
208
- control_data: dict[str, Any] = access_data.get("control-data")
209
- reserved_claims: dict[str, Any] = access_data.get("reserved-claims")
210
- custom_claims: dict[str, Any] = access_data.get("custom-claims")
211
- if superceding_claims:
212
- custom_claims = custom_claims.copy()
213
- custom_claims.update(superceding_claims)
214
-
215
- # obtain a new token, if the current token has expired
216
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
217
- if just_now > reserved_claims.get("exp"):
218
- token_jti: str = str_random(size=32,
219
- chars=string.ascii_letters + string.digits)
185
+ # initialize the return variable
186
+ result: dict[str, Any] | None = None
187
+
188
+ # process the data in storage
189
+ with (self.access_lock):
190
+ account_data: dict[str, Any] = self.access_data.get(account_id)
191
+
192
+ # was the JWT data obtained ?
193
+ if account_data:
194
+ # yes, proceed
195
+ errors: list[str] = []
196
+ current_claims: dict[str, Any] = account_data.get("claims").copy()
197
+ if account_claims:
198
+ current_claims.update(account_claims)
199
+
200
+ # obtain new tokens
201
+ current_claims["jti"] = str_random(size=32,
202
+ chars=string.ascii_letters + string.digits)
203
+ current_claims["iss"] = account_data.get("reference-url")
204
+
220
205
  # where is the JWT service provider ?
221
- if control_data.get("remote-provider"):
206
+ if account_data.get("remote-provider"):
222
207
  # JWT service is being provided by a remote server
223
- errors: list[str] = []
224
208
  # Structure of the return data:
225
209
  # {
226
210
  # "access_token": <jwt-token>,
227
211
  # "created_in": <timestamp>,
228
212
  # "expires_in": <seconds-to-expiration>,
213
+ # "refresh_token": <jwt-token>
229
214
  # ...
230
215
  # }
231
- reply: dict[str, Any] = jwt_request_token(errors=errors,
232
- reference_url=reserved_claims.get("iss"),
233
- claims=custom_claims,
234
- timeout=control_data.get("request-timeout"),
235
- logger=logger)
236
- if reply:
237
- with self.access_lock:
238
- control_data["access-token"] = reply.get("access_token")
239
- reserved_claims["jti"] = token_jti
240
- reserved_claims["iat"] = reply.get("created_in")
241
- reserved_claims["exp"] = reply.get("created_in") + reply.get("expires_in")
242
- else:
216
+ result = _jwt_request_token(errors=errors,
217
+ reference_url=current_claims.get("iss"),
218
+ claims=current_claims,
219
+ timeout=account_data.get("request-timeout"),
220
+ logger=logger)
221
+ if errors:
243
222
  raise RuntimeError(" - ".join(errors))
244
223
  else:
245
224
  # JWT service is being provided locally
246
- token_iat: int = just_now
247
- token_exp: int = just_now + control_data.get("access-max-age")
248
- claims: dict[str, Any] = access_data.get("public-claims").copy()
249
- claims.update(reserved_claims)
250
- claims.update(custom_claims)
251
- claims["jti"] = token_jti
252
- claims["iat"] = token_iat
253
- claims["exp"] = token_exp
225
+ just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
226
+ current_claims["iat"] = just_now
227
+
228
+ # retrieve the refresh token associated with the account id
229
+ refresh_token: str | None = None
230
+ if JWT_DB_ENGINE:
231
+ from pypomes_db import db_select, db_delete
232
+ if JWT_ROTATE_TOKENS:
233
+ db_delete(errors=errors,
234
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE} "
235
+ f"WHERE account_id = '{account_id}'",
236
+ logger=logger)
237
+ else:
238
+ recs: list[tuple[str]] = db_select(errors=errors,
239
+ sel_stmt=f"SELECT jwt_token FROM {JWT_DB_TABLE} "
240
+ f"WHERE account_id = '{account_id}'",
241
+ max_count=1,
242
+ logger=logger)
243
+ if recs:
244
+ refresh_token = recs[0][0]
245
+ if errors:
246
+ raise RuntimeError(" - ".join(errors))
247
+
248
+ # was it obtained ?
249
+ if not refresh_token:
250
+ # no, issue a new one
251
+ current_claims["exp"] = just_now + account_data.get("refresh-max-age")
252
+ current_claims["nat"] = "R"
253
+ # may raise an exception
254
+ refresh_token: str = jwt.encode(payload=current_claims,
255
+ key=JWT_ENCODING_KEY,
256
+ algorithm=JWT_DEFAULT_ALGORITHM)
257
+ # persist the new refresh token
258
+ if JWT_DB_ENGINE:
259
+ from pypomes_db import db_insert
260
+ db_insert(errors=errors,
261
+ insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
262
+ insert_data={"account_id": account_id,
263
+ "jwt_token": refresh_token},
264
+ logger=logger)
265
+ if errors:
266
+ raise RuntimeError(" - ".join(errors))
267
+
268
+ # issue the access token
269
+ current_claims["nat"] = "A"
270
+ current_claims["exp"] = just_now + account_data.get("access-max-age")
254
271
  # may raise an exception
255
- token: str = jwt.encode(payload=claims,
256
- key=(control_data.get("hs-secret-key") or
257
- control_data.get("rsa-private-key")),
258
- algorithm=control_data.get("algorithm"))
259
- with self.access_lock:
260
- reserved_claims["jti"] = token_jti
261
- reserved_claims["iat"] = token_iat
262
- reserved_claims["exp"] = token_exp
263
- control_data["access-token"] = token
264
-
265
- # return the token data
266
- result = {
267
- "access_token": control_data.get("access-token"),
268
- "created_in": reserved_claims.get("iat"),
269
- "expires_in": reserved_claims.get("exp") - reserved_claims.get("iat")
270
- }
271
- else:
272
- # JWT access data not found
273
- err_msg: str = f"No JWT access data found for '{account_id}'"
274
- if logger:
275
- logger.error(err_msg)
276
- raise RuntimeError(err_msg)
277
-
278
- return result
279
-
280
- def get_token_claims(self,
281
- token: str,
282
- logger: Logger = None) -> dict[str, Any]:
283
- """
284
- Obtain and return the claims of a JWT *token*.
285
-
286
- :param token: the token to be inspected for claims
287
- :param logger: optional logger
288
- :return: the token's claimset, or *None* if error
289
- :raises InvalidTokenError: token is not valid
290
- :raises ExpiredSignatureError: token has expired
291
- :raises InvalidAlgorithmError: the specified algorithm is not recognized
292
- """
293
- # declare the return variable
294
- result: dict[str, Any]
295
-
296
- if logger:
297
- logger.debug(msg=f"Retrieve claims for JWT token '{token}'")
298
-
299
- access_data: dict[str, Any] = self.get_access_data(access_token=token,
300
- logger=logger)
301
- if access_data:
302
- control_data: dict[str, Any] = access_data.get("control-data")
303
- if control_data.get("remote-provider"):
304
- # provider is remote
305
- result = control_data.get("custom-claims")
272
+ access_token: str = jwt.encode(payload=current_claims,
273
+ key=JWT_ENCODING_KEY,
274
+ algorithm=JWT_DEFAULT_ALGORITHM)
275
+ # return the token data
276
+ result = {
277
+ "access_token": access_token,
278
+ "created_in": current_claims.get("iat"),
279
+ "expires_in": current_claims.get("exp"),
280
+ "refresh_token": refresh_token
281
+ }
306
282
  else:
307
- # may raise an exception
308
- result = jwt.decode(jwt=token,
309
- key=(control_data.get("hs-secret-key") or
310
- control_data.get("rsa-public-key")),
311
- algorithms=[control_data.get("algorithm")])
312
- else:
313
- raise InvalidTokenError("JWT token is not valid")
314
-
315
- if logger:
316
- logger.debug(f"Retrieved claims for JWT token '{token}': {result}")
317
-
318
- return result
319
-
320
- def get_access_data(self,
321
- account_id: str = None,
322
- access_token: str = None,
323
- logger: Logger = None) -> dict[str, dict[str, Any]]:
324
- # noinspection HttpUrlsUsage
325
- """
326
- Retrieve and return the access data in storage for *account_id*, or optionally, for *access_token*.
327
-
328
- Either *account_id* or *access_token* must be provided, the former having precedence over the later.
329
- Note that, whereas *account_id* uniquely identifies an access dataset, *access_token* might not,
330
- and thus, the first dataset associated with it would be returned.
331
-
332
- :param account_id: the account identification
333
- :param access_token: the access token
334
- :param logger: optional logger
335
- :return: the corresponding item in storage, or *None* if not found
336
- """
337
- # initialize the return variable
338
- result: dict[str, dict[str, Any]] | None = None
339
-
340
- if logger:
341
- target: str = f"account id '{account_id}'" if account_id else f"token '{access_token}'"
342
- logger.debug(f"Retrieve access data for {target}")
343
- # retrieve the data
344
- with self.access_lock:
345
- for item_data in self.access_data:
346
- if (account_id and account_id == item_data.get("reserved-claims").get("sub")) or \
347
- (access_token and access_token == item_data.get("control-data").get("access-token")):
348
- result = item_data
349
- break
350
- if logger:
351
- logger.debug(f"Data is '{result}'")
283
+ # JWT access data not found
284
+ err_msg: str = f"No JWT access data found for '{account_id}'"
285
+ if logger:
286
+ logger.error(err_msg)
287
+ raise RuntimeError(err_msg)
352
288
 
353
289
  return result
354
290
 
355
291
 
356
- def jwt_request_token(errors: list[str],
357
- reference_url: str,
358
- claims: dict[str, Any],
359
- timeout: int = None,
360
- logger: Logger = None) -> dict[str, Any]:
292
+ def _jwt_request_token(errors: list[str],
293
+ reference_url: str,
294
+ claims: dict[str, Any],
295
+ timeout: int = None,
296
+ logger: Logger = None) -> dict[str, Any]:
361
297
  """
362
298
  Obtain and return the JWT token from *reference_url*, along with its duration.
363
299
 
@@ -403,30 +339,3 @@ def jwt_request_token(errors: list[str],
403
339
  errors.append(err_msg)
404
340
 
405
341
  return result
406
-
407
-
408
- def jwt_validate_token(token: str,
409
- key: bytes | str,
410
- algorithm: str,
411
- logger: Logger = None) -> None:
412
- """
413
- Verify if *token* ia a valid JWT token.
414
-
415
- Raise an appropriate exception if validation failed.
416
-
417
- :param token: the token to be validated
418
- :param key: the secret or public key used to create the token (HS or RSA authentication, respectively)
419
- :param algorithm: the algorithm used to to sign the token with
420
- :param logger: optional logger
421
- :raises InvalidTokenError: token is invalid
422
- :raises InvalidKeyError: authentication key is not in the proper format
423
- :raises ExpiredSignatureError: token and refresh period have expired
424
- :raises InvalidSignatureError: signature does not match the one provided as part of the token
425
- """
426
- if logger:
427
- logger.debug(msg=f"Validate JWT token '{token}'")
428
- jwt.decode(jwt=token,
429
- key=key,
430
- algorithms=[algorithm])
431
- if logger:
432
- logger.debug(msg=f"Token '{token}' is valid")
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,39 +1,14 @@
1
- import contextlib
2
- from cryptography.hazmat.primitives import serialization
3
- from cryptography.hazmat.primitives.asymmetric import rsa
4
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
5
- from datetime import datetime
6
- from flask import Request, Response, request, jsonify
1
+ import jwt
2
+ from flask import Request, Response, request
7
3
  from logging import Logger
8
- from pypomes_core import APP_PREFIX, env_get_str, env_get_bytes, env_get_int
9
- from secrets import token_bytes
10
- from typing import Any, Final, Literal
11
-
12
- from .jwt_data import JwtData, jwt_validate_token
13
-
14
- JWT_DEFAULT_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
15
- def_value="HS256")
16
- JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_AGE",
17
- def_value=3600)
18
- JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
19
- def_value=43200)
20
- JWT_HS_SECRET_KEY: Final[bytes] = env_get_bytes(key=f"{APP_PREFIX}_JWT_HS_SECRET_KEY",
21
- def_value=token_bytes(nbytes=32))
22
-
23
- # obtain a RSA private/public key pair
24
- __priv_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
25
- __pub_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
26
- if not __priv_bytes or not __pub_bytes:
27
- __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
28
- key_size=2048)
29
- __priv_bytes = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
30
- format=serialization.PrivateFormat.PKCS8,
31
- encryption_algorithm=serialization.NoEncryption())
32
- __pub_key: RSAPublicKey = __priv_key.public_key()
33
- __pub_bytes = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
34
- format=serialization.PublicFormat.SubjectPublicKeyInfo)
35
- JWT_RSA_PRIVATE_KEY: Final[bytes] = __priv_bytes
36
- JWT_RSA_PUBLIC_KEY: Final[bytes] = __pub_bytes
4
+ from typing import Any, Literal
5
+
6
+ from .jwt_constants import (
7
+ JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
8
+ JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY,
9
+ JWT_DB_ENGINE, JWT_DB_TABLE
10
+ )
11
+ from .jwt_data import JwtData
37
12
 
38
13
  # the JWT data object
39
14
  __jwt_data: JwtData = JwtData()
@@ -58,23 +33,22 @@ def jwt_needed(func: callable) -> callable:
58
33
 
59
34
  def jwt_assert_access(account_id: str) -> bool:
60
35
  """
61
- Determine whether access for *ccount_id* has been established.
36
+ Determine whether access for *account_id* has been established.
62
37
 
63
38
  :param account_id: the account identification
64
39
  :return: *True* if access data exists for *account_id*, *False* otherwise
65
40
  """
66
- return __jwt_data.get_access_data(account_id=account_id) is not None
41
+ return __jwt_data.access_data.get(account_id) is not None
67
42
 
68
43
 
69
44
  def jwt_set_access(account_id: str,
70
45
  reference_url: str,
71
46
  claims: dict[str, Any],
72
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
73
47
  access_max_age: int = JWT_ACCESS_MAX_AGE,
74
48
  refresh_max_age: int = JWT_REFRESH_MAX_AGE,
75
- secret_key: bytes = JWT_HS_SECRET_KEY,
76
- private_key: bytes = JWT_RSA_PRIVATE_KEY,
77
- public_key: bytes = JWT_RSA_PUBLIC_KEY,
49
+ grace_interval: int = None,
50
+ token_audience: str = None,
51
+ token_nonce: str = None,
78
52
  request_timeout: int = None,
79
53
  remote_provider: bool = True,
80
54
  logger: Logger = None) -> None:
@@ -84,12 +58,11 @@ def jwt_set_access(account_id: str,
84
58
  :param account_id: the account identification
85
59
  :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
86
60
  :param claims: the JWT claimset, as key-value pairs
87
- :param algorithm: the authentication type
88
- :param access_max_age: token duration, in seconds
89
- :param refresh_max_age: duration for the refresh operation, in seconds
90
- :param secret_key: secret key for HS authentication
91
- :param private_key: private key for RSA authentication
92
- :param public_key: public key for RSA authentication
61
+ :param access_max_age: access token duration, in seconds
62
+ :param refresh_max_age: refresh token duration, in seconds
63
+ :param grace_interval: optional time to wait for token to be valid, in seconds
64
+ :param token_audience: optional audience the token is intended for
65
+ :param token_nonce: optional value used to associate a client session with a token
93
66
  :param request_timeout: timeout for the requests to the reference URL
94
67
  :param remote_provider: whether the JWT provider is a remote server
95
68
  :param logger: optional logger
@@ -106,52 +79,144 @@ def jwt_set_access(account_id: str,
106
79
  reference_url = reference_url[:pos]
107
80
 
108
81
  # register the JWT service
109
- __jwt_data.add_access_data(account_id=account_id,
110
- reference_url=reference_url,
111
- claims=claims,
112
- algorithm=algorithm,
113
- access_max_age=access_max_age,
114
- refresh_max_age=refresh_max_age,
115
- hs_secret_key=secret_key,
116
- rsa_private_key=private_key,
117
- rsa_public_key=public_key,
118
- request_timeout=request_timeout,
119
- remote_provider=remote_provider,
120
- logger=logger)
82
+ __jwt_data.add_access(account_id=account_id,
83
+ reference_url=reference_url,
84
+ claims=claims,
85
+ access_max_age=access_max_age,
86
+ refresh_max_age=refresh_max_age,
87
+ grace_interval=grace_interval,
88
+ token_audience=token_audience,
89
+ token_nonce=token_nonce,
90
+ request_timeout=request_timeout,
91
+ remote_provider=remote_provider,
92
+ logger=logger)
121
93
 
122
94
 
123
95
  def jwt_remove_access(account_id: str,
124
- logger: Logger = None) -> None:
96
+ logger: Logger = None) -> bool:
125
97
  """
126
98
  Remove from storage the JWT access data for *account_id*.
127
99
 
128
100
  :param account_id: the account identification
129
101
  :param logger: optional logger
102
+ return: *True* if the access data was removed, *False* otherwise
130
103
  """
131
104
  if logger:
132
105
  logger.debug(msg=f"Remove access data for '{account_id}'")
133
106
 
134
- __jwt_data.remove_access_data(account_id=account_id,
135
- logger=logger)
107
+ return __jwt_data.remove_access(account_id=account_id,
108
+ logger=logger)
109
+
110
+
111
+ def jwt_validate_token(errors: list[str] | None,
112
+ token: str,
113
+ nature: Literal["A", "R"] = None,
114
+ logger: Logger = None) -> bool:
115
+ """
116
+ Verify if *token* ia a valid JWT token.
136
117
 
118
+ Raise an appropriate exception if validation failed.
137
119
 
138
- def jwt_get_token_data(errors: list[str],
139
- account_id: str,
140
- superceding_claims: dict[str, Any] = None,
141
- logger: Logger = None) -> dict[str, Any]:
120
+ :param errors: incidental error messages
121
+ :param token: the token to be validated
122
+ :param nature: optionally validate the token's nature ("A": access token, "R": refresh token)
123
+ :param logger: optional logger
124
+ :return: *True* if token is valid, *False* otherwise
142
125
  """
143
- Obtain and return the JWT token data associated with *account_id*.
126
+ if logger:
127
+ logger.debug(msg=f"Validate JWT token '{token}'")
128
+
129
+ err_msg: str | None = None
130
+ try:
131
+ # raises:
132
+ # InvalidTokenError: token is invalid
133
+ # InvalidKeyError: authentication key is not in the proper format
134
+ # ExpiredSignatureError: token and refresh period have expired
135
+ # InvalidSignatureError: signature does not match the one provided as part of the token
136
+ claims: dict[str, Any] = jwt.decode(jwt=token,
137
+ key=JWT_DECODING_KEY,
138
+ algorithms=[JWT_DEFAULT_ALGORITHM])
139
+ if nature and "nat" in claims and nature != claims.get("nat"):
140
+ nat: str = "an access" if nature == "A" else "a refresh"
141
+ err_msg = f"Token is not {nat} token"
142
+ except Exception as e:
143
+ err_msg = str(e)
144
+
145
+ if err_msg:
146
+ if logger:
147
+ logger.error(msg=err_msg)
148
+ if isinstance(errors, list):
149
+ errors.append(err_msg)
150
+ elif logger:
151
+ logger.debug(msg=f"Token '{token}' is valid")
152
+
153
+ return err_msg is None
154
+
155
+
156
+ def jwt_revoke_tokens(errors: list[str] | None,
157
+ account_id: str,
158
+ logger: Logger = None) -> bool:
159
+ """
160
+ Revoke all refresh tokens associated with *account_id*.
161
+
162
+ Revoke operations require access to a database table defined by *JWT_DB_TABLE*.
163
+
164
+ :param errors: incidental error messages
165
+ :param account_id: the account identification
166
+ :param logger: optional logger
167
+ :return: *True* if operation could be performed, *False* otherwise
168
+ """
169
+ # initialize the return variable
170
+ result: bool = False
171
+
172
+ if logger:
173
+ logger.debug(msg=f"Revoking refresh tokens of '{account_id}'")
174
+
175
+ op_errors: list[str] = []
176
+ if JWT_DB_ENGINE:
177
+ from pypomes_db import db_delete
178
+ delete_stmt: str = (f"DELETE FROM {JWT_DB_TABLE} "
179
+ f"WHERE account_id = '{account_id}'")
180
+ db_delete(errors=op_errors,
181
+ delete_stmt=delete_stmt,
182
+ logger=logger)
183
+ else:
184
+ op_errors.append("Database access for token revocation has not been specified")
185
+
186
+ if op_errors:
187
+ if logger:
188
+ logger.error(msg="; ".join(op_errors))
189
+ if isinstance(errors, list):
190
+ errors.extend(op_errors)
191
+ else:
192
+ result = True
193
+
194
+ return result
195
+
196
+
197
+ def jwt_get_tokens(errors: list[str] | None,
198
+ account_id: str,
199
+ account_claims: dict[str, Any] = None,
200
+ refresh_token: str = None,
201
+ logger: Logger = None) -> dict[str, Any]:
202
+ """
203
+ Issue or refresh, and return, the JWT token data associated with *account_id*.
204
+
205
+ If *refresh_token* is provided, its claims are used on issuing the new tokens,
206
+ and claims in *account_claims*, if any, are ignored.
144
207
 
145
208
  Structure of the return data:
146
209
  {
147
210
  "access_token": <jwt-token>,
148
211
  "created_in": <timestamp>,
149
- "expires_in": <seconds-to-expiration>
212
+ "expires_in": <seconds-to-expiration>,
213
+ "refresh_token": <jwt-token>
150
214
  }
151
215
 
152
216
  :param errors: incidental error messages
153
217
  :param account_id: the account identification
154
- :param superceding_claims: if provided, may supercede registered custom claims
218
+ :param account_claims: if provided, may supercede registered custom claims
219
+ :param refresh_token: if provided, defines a token refresh operation
155
220
  :param logger: optional logger
156
221
  :return: the JWT token data, or *None* if error
157
222
  """
@@ -160,23 +225,35 @@ def jwt_get_token_data(errors: list[str],
160
225
 
161
226
  if logger:
162
227
  logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
163
- try:
164
- result = __jwt_data.get_token_data(account_id=account_id,
165
- superceding_claims=superceding_claims,
166
- logger=logger)
167
- if logger:
168
- logger.debug(msg=f"Data is '{result}'")
169
- except Exception as e:
228
+ op_errors: list[str] = []
229
+ if refresh_token:
230
+ account_claims = jwt_get_claims(errors=op_errors,
231
+ token=refresh_token)
232
+ if not op_errors and account_claims.get("nat") != "R":
233
+ op_errors.extend("Invalid parameters")
234
+
235
+ if not op_errors:
236
+ try:
237
+ result = __jwt_data.issue_tokens(account_id=account_id,
238
+ account_claims=account_claims)
239
+ if logger:
240
+ logger.debug(msg=f"Data is '{result}'")
241
+ except Exception as e:
242
+ # token issuing failed
243
+ op_errors.append(str(e))
244
+
245
+ if op_errors:
170
246
  if logger:
171
- logger.error(msg=str(e))
172
- errors.append(str(e))
247
+ logger.error("; ".join(op_errors))
248
+ if isinstance(errors, list):
249
+ errors.extend(op_errors)
173
250
 
174
251
  return result
175
252
 
176
253
 
177
- def jwt_get_token_claims(errors: list[str],
178
- token: str,
179
- logger: Logger = None) -> dict[str, Any]:
254
+ def jwt_get_claims(errors: list[str] | None,
255
+ token: str,
256
+ logger: Logger = None) -> dict[str, Any]:
180
257
  """
181
258
  Obtain and return the claims set of a JWT *token*.
182
259
 
@@ -192,11 +269,19 @@ def jwt_get_token_claims(errors: list[str],
192
269
  logger.debug(msg=f"Retrieve claims for token '{token}'")
193
270
 
194
271
  try:
195
- result = __jwt_data.get_token_claims(token=token)
272
+ claims: dict[str, Any] = jwt.decode(jwt=token,
273
+ options={"verify_signature": False})
274
+ if claims.get("nat") in ["A", "R"]:
275
+ result = jwt.decode(jwt=token,
276
+ key=JWT_DECODING_KEY,
277
+ algorithms=[JWT_DEFAULT_ALGORITHM])
278
+ else:
279
+ result = claims
196
280
  except Exception as e:
197
281
  if logger:
198
282
  logger.error(msg=str(e))
199
- errors.append(str(e))
283
+ if isinstance(errors, list):
284
+ errors.append(str(e))
200
285
 
201
286
  return result
202
287
 
@@ -222,30 +307,16 @@ def jwt_verify_request(request: Request,
222
307
 
223
308
  # was a 'Bearer' authorization obtained ?
224
309
  if auth_header and auth_header.startswith("Bearer "):
225
- # yes, extract and validate the JWT token
310
+ # yes, extract and validate the JWT access token
226
311
  token: str = auth_header.split(" ")[1]
227
312
  if logger:
228
313
  logger.debug(msg=f"Token is '{token}'")
229
- # retrieve the reference access data
230
- access_data: dict[str, Any] = __jwt_data.get_access_data(access_token=token)
231
- if access_data:
232
- control_data: dict[str, Any] = access_data.get("control-data")
233
- if control_data.get("remote-provider"):
234
- # JWT provider is remote
235
- if datetime.now().timestamp() > access_data.get("reserved-claims").get("exp"):
236
- err_msg = "Token has expired"
237
- else:
238
- # JWT was locally provided
239
- try:
240
- jwt_validate_token(token=token,
241
- key=(control_data.get("hs-secret-key") or
242
- control_data.get("rsa-public-key")),
243
- algorithm=control_data.get("algorithm"))
244
- except Exception as e:
245
- # validation failed
246
- err_msg = str(e)
247
- else:
248
- err_msg = "No access data found for token"
314
+ errors: list[str] = []
315
+ jwt_validate_token(errors=errors,
316
+ nature="A",
317
+ token=token)
318
+ if errors:
319
+ err_msg = "; ".join(errors)
249
320
  else:
250
321
  # no 'Bearer' found, report the error
251
322
  err_msg = "Request header has no 'Bearer' data"
@@ -258,101 +329,3 @@ def jwt_verify_request(request: Request,
258
329
  status=401)
259
330
 
260
331
  return result
261
-
262
-
263
- def jwt_claims(token: str = None) -> Response:
264
- """
265
- REST service entry point for retrieving the claims of a JWT token.
266
-
267
- Structure of the return data:
268
- {
269
- "<claim-1>": <value-of-claim-1>,
270
- ...
271
- "<claim-n>": <value-of-claim-n>
272
- }
273
-
274
- :param token: the JWT token
275
- :return: a *Response* containing the requested JWT token claims, or reporting an error
276
- """
277
- # declare the return variable
278
- result: Response
279
-
280
- # retrieve the token
281
- # noinspection PyUnusedLocal
282
- if not token:
283
- token = request.values.get("token")
284
- if not token:
285
- with contextlib.suppress(Exception):
286
- token = request.get_json().get("token")
287
-
288
- # has the token been obtained ?
289
- if token:
290
- # yes, obtain the token data
291
- try:
292
- token_claims: dict[str, Any] = __jwt_data.get_token_claims(token=token)
293
- result = jsonify(token_claims)
294
- except Exception as e:
295
- # claims extraction failed
296
- result = Response(response=str(e),
297
- status=400)
298
- else:
299
- # no, report the problem
300
- result = Response(response="Invalid parameters",
301
- status=400)
302
-
303
- return result
304
-
305
-
306
- def jwt_token(service_params: dict[str, Any] = None) -> Response:
307
- """
308
- REST service entry point for obtaining JWT tokens.
309
-
310
- The requester must send, as parameter *service_params* or in the body of the request:
311
- {
312
- "account-id": "<string>" - required account identification
313
- "<custom-claim-key-1>": "<custom-claim-value-1>", - optional superceding custom claims
314
- ...
315
- "<custom-claim-key-n>": "<custom-claim-value-n>"
316
- }
317
- If provided, the superceding custom claims will be sent to the remote provider, if applicable
318
- (custom claims currently registered for the account may be overridden).
319
-
320
-
321
- Structure of the return data:
322
- {
323
- "access_token": <jwt-token>,
324
- "created_in": <timestamp>,
325
- "expires_in": <seconds-to-expiration>
326
- }
327
-
328
- :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
329
- :return: a *Response* containing the requested JWT token data, or reporting an error
330
- """
331
- # declare the return variable
332
- result: Response
333
-
334
- # retrieve the parameters
335
- # noinspection PyUnusedLocal
336
- params: dict[str, Any] = service_params or {}
337
- if not params:
338
- with contextlib.suppress(Exception):
339
- params = request.get_json()
340
- account_id: str | None = params.pop("account-id", None)
341
-
342
- # has the account been identified ?
343
- if account_id:
344
- # yes, obtain the token data
345
- try:
346
- token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
347
- superceding_claims=params)
348
- result = jsonify(token_data)
349
- except Exception as e:
350
- # token validation failed
351
- result = Response(response=str(e),
352
- status=401)
353
- else:
354
- # no, report the problem
355
- result = Response(response="Invalid parameters",
356
- status=401)
357
-
358
- return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A collection of Python pomes, penyeach (JWT module)
5
5
  Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-JWT
6
6
  Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-JWT/issues
@@ -10,6 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
- Requires-Dist: cryptography>=44.0.1
13
+ Requires-Dist: cryptography>=44.0.2
14
14
  Requires-Dist: pyjwt>=2.10.1
15
15
  Requires-Dist: pypomes-core>=1.8.3
@@ -0,0 +1,8 @@
1
+ pypomes_jwt/__init__.py,sha256=Op7UEyEDniIkvk65ro8JUPHjGCV_q35k0YL_Ql_DjBc,1010
2
+ pypomes_jwt/jwt_constants.py,sha256=MGuIKc9tFsbWjx3wBlNbdibzytbndK9uhL2kVPW7X2A,4086
3
+ pypomes_jwt/jwt_data.py,sha256=e5KX0slUtrXbbyCtbdjycuqdQwVrvE8BK-_-XhVvHIs,16403
4
+ pypomes_jwt/jwt_pomes.py,sha256=uyhwZzI781yGzWbHKfI-aM9dVXHBB28okQzlvfT4D00,12130
5
+ pypomes_jwt-0.7.2.dist-info/METADATA,sha256=WamGD3ya5b3Ut04oVeFX8TzEeTHRZra6cjpmxJVkeBI,599
6
+ pypomes_jwt-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.7.2.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.7.2.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=jHyN7gJR575f8djqu7ZsYYxSuhpcy46S4ALAl7SSmHk,940
2
- pypomes_jwt/jwt_data.py,sha256=U_Bl2F-HAbDOxD-VshE3wD6wXxcWqv6fcaAf1J1pK9Y,20050
3
- pypomes_jwt/jwt_pomes.py,sha256=ehi_mtFn6LE5QBpPHnLWYDTnuwbz5Cg4tovz0KcZW3k,14140
4
- pypomes_jwt-0.7.0.dist-info/METADATA,sha256=k8lWDHA8vjQrqa8bi9tep1MRiM6eopzzM5MSTNjbjA4,599
5
- pypomes_jwt-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.7.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.7.0.dist-info/RECORD,,