pypomes-jwt 0.7.0__py3-none-any.whl → 0.7.1__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,22 +1,24 @@
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,
8
+ jwt_needed, jwt_verify_request, jwt_claims, jwt_tokens,
9
+ jwt_get_tokens, jwt_get_claims, jwt_validate_token,
9
10
  jwt_assert_access, jwt_set_access, jwt_remove_access
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",
18
+ "JWT_ENCODING_KEY", "JWT_DECODING_KEY",
19
+ # jwt_pomes
20
+ "jwt_needed", "jwt_verify_request", "jwt_claims", "jwt_tokens",
21
+ "jwt_get_tokens", "jwt_get_claims", "jwt_validate_token",
20
22
  "jwt_assert_access", "jwt_set_access", "jwt_remove_access"
21
23
  ]
22
24
 
@@ -0,0 +1,52 @@
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_ENGINE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
13
+ JWT_DB_HOST: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_HOST")
14
+ JWT_DB_NAME: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_NAME")
15
+ JWT_DB_PORT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_DB_PORT")
16
+ JWT_DB_USER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_USER")
17
+ JWT_DB_PWD: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_PWD")
18
+
19
+ JWT_ROTATE_TOKENS: Final[bool] = False \
20
+ if JWT_DB_ENGINE is None else env_get_bool(key=f"{APP_PREFIX}_JWT_ROTATE_TOKENS",
21
+ def_value=False)
22
+
23
+ # one of HS256, HS512, RSA256, RSA512
24
+ JWT_DEFAULT_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
25
+ def_value="HS256")
26
+ # recommended: between 5 min and 1 hour (set to 5 min)
27
+ JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_AGE",
28
+ def_value=300)
29
+ # recommended: at least 2 hours (set to 24 hours)
30
+ JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
31
+ def_value=86400)
32
+
33
+ # recommended: allow the encode and decode keys to be generated anew when app starts
34
+ __encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODE_KEY")
35
+ __decoding_key: bytes
36
+ if JWT_DEFAULT_ALGORITHM in ["HS256", "HS512"]:
37
+ if not __encoding_key:
38
+ __encoding_key = token_bytes(nbytes=32)
39
+ __decoding_key = __encoding_key
40
+ else:
41
+ __decoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_DECODE_KEY")
42
+ if not __encoding_key or not __decoding_key:
43
+ __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
44
+ key_size=2048)
45
+ __encoding_key = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
46
+ format=serialization.PrivateFormat.PKCS8,
47
+ encryption_algorithm=serialization.NoEncryption())
48
+ __pub_key: RSAPublicKey = __priv_key.public_key()
49
+ __decoding_key = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
50
+ format=serialization.PublicFormat.SubjectPublicKeyInfo)
51
+ JWT_ENCODING_KEY: Final[bytes] = __encoding_key
52
+ JWT_DECODING_KEY: Final[bytes] = __decoding_key
pypomes_jwt/jwt_data.py CHANGED
@@ -2,12 +2,15 @@ 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
+ )
11
14
 
12
15
 
13
16
  class JwtData:
@@ -16,171 +19,153 @@ class JwtData:
16
19
 
17
20
  Instance variables:
18
21
  - 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)
22
+ - access_data: list with dictionaries holding the JWT token data, organized by account ids:
23
+ {
24
+ <account-id>: {
25
+ "reference-url": # the reference URL
26
+ "remote-provider": <bool>, # whether the JWT provider is a remote server
27
+ "request-timeout": <int>, # in seconds - defaults to no timeout
28
+ "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
29
+ "refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
30
+ "grace-interval": <int> # time to wait for token to be valid, in seconds
31
+ "token-audience": <string> # the audience the token is intended for
32
+ "token_nonce": <string> # value used to associate a client session with a token
33
+ "claims": {
44
34
  "birthdate": <string>, # subject's birth date
45
35
  "email": <string>, # subject's email
46
36
  "gender": <string>, # subject's gender
47
37
  "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>",
38
+ "roles": <List[str]>, # subject roles
39
+ "nonce": <string>, # value used to associate a Client session with a token
52
40
  ...
53
- "<custom-claim-key-n>": "<custom-claim-value-n>"
54
41
  }
55
42
  },
56
43
  ...
57
- ]
44
+ }
45
+
46
+ JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between
47
+ two parties. It is fully described in the RFC 7519, issued by the Internet Engineering Task Force
48
+ (see https://www.rfc-editor.org/rfc/rfc7519.html).
49
+ In this context, claims are pieces of information a token bears, and herein are loosely classified
50
+ as token-related and account-related. All times are UTC.
51
+
52
+ Token-related claims are mostly required claims, and convey information about the token itself:
53
+ "exp": <timestamp> # expiration time
54
+ "iat": <timestamp> # issued at
55
+ "iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
56
+ "jti": <string> # JWT id
57
+ "sub": <string> # subject (the account identification)
58
+ "nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
59
+ # optional:
60
+ "aud": <string> # token audience
61
+ "nbt": <timestamp> # not before time
62
+
63
+ Account-related claims are optional claims, and convey information about the registered account they belong to.
64
+ Alhough they can be freely specified, these are some of the most commonly used claims:
65
+ "birthdate": <string> # subject's birth date
66
+ "email": <string> # subject's email
67
+ "gender": <string> # subject's gender
68
+ "name": <string> # subject's name
69
+ "roles": <List[str]> # subject roles
70
+ "nonce": <string> # value used to associate a client session with a token
58
71
  """
59
72
  def __init__(self) -> None:
60
73
  """
61
74
  Initizalize the token access data.
62
75
  """
63
76
  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:
77
+ self.access_data: dict[str, Any] = {}
78
+
79
+ def add_access(self,
80
+ account_id: str,
81
+ reference_url: str,
82
+ claims: dict[str, Any],
83
+ access_max_age: int,
84
+ refresh_max_age: int,
85
+ grace_interval: int,
86
+ token_audience: str,
87
+ token_nonce: str,
88
+ request_timeout: int,
89
+ remote_provider: bool,
90
+ logger: Logger = None) -> None:
79
91
  """
80
92
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
81
93
 
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.
94
+ The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
95
+ at a minimum, "birthdate", "email", "gender", "name", and "roles".
96
+ If the token provider is local, then the token-related claims are created at token issuing time.
97
+ If the token provider is remote, all claims are sent to it at token request time.
89
98
 
90
99
  :param account_id: the account identification
91
100
  :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
92
101
  :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
102
+ :param access_max_age: access token duration, in seconds
103
+ :param refresh_max_age: refresh token duration, in seconds
104
+ :param grace_interval: time to wait for token to be valid, in seconds
105
+ :param token_audience: the audience the token is intended for
106
+ :param token_nonce: optional value used to associate a client session with a token
99
107
  :param request_timeout: timeout for the requests to the reference URL
100
108
  :param remote_provider: whether the JWT provider is a remote server
101
109
  :param logger: optional logger
102
110
  """
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:
111
+ # build and store the access data for the account
112
+ with self.access_lock:
113
+ if account_id not in self.access_data:
114
+ self.access_data[account_id] = {
115
+ "reference_url": reference_url,
116
+ "access-max-age": access_max_age,
117
+ "refresh-max-age": refresh_max_age,
118
+ "grace-interval": grace_interval,
119
+ "token-audience": token_audience,
120
+ "token-nonce": token_nonce,
121
+ "request-timeout": request_timeout,
122
+ "remote-provider": remote_provider,
123
+ "claims": claims or {}
124
+ }
125
+ if logger:
126
+ logger.debug(f"JWT data added for '{account_id}'")
127
+ elif logger:
128
+ logger.warning(f"JWT data already exists for '{account_id}'")
129
+
130
+ def remove_access(self,
131
+ account_id: str,
132
+ logger: Logger) -> bool:
151
133
  """
152
134
  Remove from storage the access data for *account_id*.
153
135
 
154
136
  :param account_id: the account identification
155
137
  :param logger: optional logger
138
+ return: *True* if the access data was removed, *False* otherwise
156
139
  """
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:
140
+ account_data: dict[str, Any] | None
141
+ with self.access_lock:
142
+ account_data = self.access_data.pop(account_id, None)
143
+
144
+ if logger:
145
+ if account_data:
164
146
  logger.debug(f"Removed JWT data for '{account_id}'")
165
- elif logger:
166
- logger.warning(f"No JWT data found for '{account_id}'")
147
+ else:
148
+ logger.warning(f"No JWT data found for '{account_id}'")
167
149
 
168
- def get_token_data(self,
169
- account_id: str,
170
- superceding_claims: dict[str, Any] = None,
171
- logger: Logger = None) -> dict[str, Any]:
150
+ return account_data is not None
151
+
152
+ def issue_tokens(self,
153
+ account_id: str,
154
+ account_claims: dict[str, Any] = None,
155
+ logger: Logger = None) -> dict[str, Any]:
172
156
  """
173
- Obtain and return the JWT token for *account_id*, along with its duration.
157
+ Issue and return the JWT access and refresh tokens for *account_id*.
174
158
 
175
159
  Structure of the return data:
176
160
  {
177
161
  "access_token": <jwt-token>,
178
162
  "created_in": <timestamp>,
179
- "expires_in": <seconds-to-expiration>
163
+ "expires_in": <seconds-to-expiration>,
164
+ "refresh_token": <jwt-token>
180
165
  }
181
166
 
182
167
  :param account_id: the account identification
183
- :param superceding_claims: if provided, may supercede registered custom claims
168
+ :param account_claims: if provided, may supercede registered account-related claims
184
169
  :param logger: optional logger
185
170
  :return: the JWT token data, or *None* if error
186
171
  :raises InvalidTokenError: token is invalid
@@ -196,29 +181,27 @@ class JwtData:
196
181
  :raises RuntimeError: access data not found for the given *account_id*, or
197
182
  the remote JWT provider failed to return a token
198
183
  """
199
- # declare the return variable
200
- result: dict[str, Any]
184
+ # initialize the return variable
185
+ result: dict[str, Any] | None = None
186
+
187
+ # process the data in storage
188
+ with (self.access_lock):
189
+ account_data: dict[str, Any] = self.access_data.get(account_id)
201
190
 
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)
191
+ # was the JWT data obtained ?
192
+ if account_data:
193
+ # yes, proceed
194
+ current_claims: dict[str, Any] = account_data.get("claims").copy()
195
+ if account_claims:
196
+ current_claims.update(current_claims)
197
+
198
+ # obtain new tokens
199
+ current_claims["jti"] = str_random(size=32,
200
+ chars=string.ascii_letters + string.digits)
201
+ current_claims["iss"] = account_data.get("reference-url")
214
202
 
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)
220
203
  # where is the JWT service provider ?
221
- if control_data.get("remote-provider"):
204
+ if account_data.get("remote-provider"):
222
205
  # JWT service is being provided by a remote server
223
206
  errors: list[str] = []
224
207
  # Structure of the return data:
@@ -226,138 +209,53 @@ class JwtData:
226
209
  # "access_token": <jwt-token>,
227
210
  # "created_in": <timestamp>,
228
211
  # "expires_in": <seconds-to-expiration>,
212
+ # "refresh_token": <jwt-token>
229
213
  # ...
230
214
  # }
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:
215
+ result = _jwt_request_token(errors=errors,
216
+ reference_url=current_claims.get("iss"),
217
+ claims=current_claims,
218
+ timeout=account_data.get("request-timeout"),
219
+ logger=logger)
220
+ if errors:
243
221
  raise RuntimeError(" - ".join(errors))
244
222
  else:
245
223
  # 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
224
+ just_now: float = datetime.now(tz=timezone.utc).timestamp()
225
+ current_claims["iat"] = just_now
226
+ current_claims["exp"] = just_now + account_data.get("access-max-age")
227
+ current_claims["nat"] = "R"
254
228
  # 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")
229
+ refresh_token: str = jwt.encode(payload=current_claims,
230
+ key=JWT_ENCODING_KEY,
231
+ algorithm=JWT_DEFAULT_ALGORITHM)
232
+ current_claims["nat"] = "A"
233
+ # may raise an exception
234
+ access_token: str = jwt.encode(payload=current_claims,
235
+ key=JWT_ENCODING_KEY,
236
+ algorithm=JWT_DEFAULT_ALGORITHM)
237
+ # return the token data
238
+ result = {
239
+ "access_token": access_token,
240
+ "created_in": account_claims.get("iat"),
241
+ "expires_in": account_claims.get("exp"),
242
+ "refresh_token": refresh_token
243
+ }
306
244
  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}'")
245
+ # JWT access data not found
246
+ err_msg: str = f"No JWT access data found for '{account_id}'"
247
+ if logger:
248
+ logger.error(err_msg)
249
+ raise RuntimeError(err_msg)
352
250
 
353
251
  return result
354
252
 
355
253
 
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]:
254
+ def _jwt_request_token(errors: list[str],
255
+ reference_url: str,
256
+ claims: dict[str, Any],
257
+ timeout: int = None,
258
+ logger: Logger = None) -> dict[str, Any]:
361
259
  """
362
260
  Obtain and return the JWT token from *reference_url*, along with its duration.
363
261
 
@@ -403,30 +301,3 @@ def jwt_request_token(errors: list[str],
403
301
  errors.append(err_msg)
404
302
 
405
303
  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
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
2
+ import jwt
6
3
  from flask import Request, Response, request, jsonify
7
4
  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
5
+ from typing import Any, Literal
6
+
7
+ from .jwt_constants import (
8
+ JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
9
+ JWT_DEFAULT_ALGORITHM, JWT_DECODING_KEY
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,98 @@ 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)
136
109
 
137
110
 
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]:
111
+ def jwt_validate_token(errors: list[str] | None,
112
+ token: str,
113
+ nature: Literal["A", "R"] = None,
114
+ logger: Logger = None) -> bool:
142
115
  """
143
- Obtain and return the JWT token data associated with *account_id*.
116
+ Verify if *token* ia a valid JWT token.
117
+
118
+ Raise an appropriate exception if validation failed.
119
+
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
125
+ """
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_get_tokens(errors: list[str] | None,
157
+ account_id: str,
158
+ account_claims: dict[str, Any] = None,
159
+ logger: Logger = None) -> dict[str, Any]:
160
+ """
161
+ Issue and return the JWT token data associated with *account_id*.
144
162
 
145
163
  Structure of the return data:
146
164
  {
147
165
  "access_token": <jwt-token>,
148
166
  "created_in": <timestamp>,
149
- "expires_in": <seconds-to-expiration>
167
+ "expires_in": <seconds-to-expiration>,
168
+ "refresh_token": <jwt-token>
150
169
  }
151
170
 
152
171
  :param errors: incidental error messages
153
172
  :param account_id: the account identification
154
- :param superceding_claims: if provided, may supercede registered custom claims
173
+ :param account_claims: if provided, may supercede registered custom claims
155
174
  :param logger: optional logger
156
175
  :return: the JWT token data, or *None* if error
157
176
  """
@@ -161,22 +180,23 @@ def jwt_get_token_data(errors: list[str],
161
180
  if logger:
162
181
  logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
163
182
  try:
164
- result = __jwt_data.get_token_data(account_id=account_id,
165
- superceding_claims=superceding_claims,
166
- logger=logger)
183
+ result = __jwt_data.issue_tokens(account_id=account_id,
184
+ account_claims=account_claims,
185
+ logger=logger)
167
186
  if logger:
168
187
  logger.debug(msg=f"Data is '{result}'")
169
188
  except Exception as e:
170
189
  if logger:
171
190
  logger.error(msg=str(e))
172
- errors.append(str(e))
191
+ if isinstance(errors, list):
192
+ errors.append(str(e))
173
193
 
174
194
  return result
175
195
 
176
196
 
177
- def jwt_get_token_claims(errors: list[str],
178
- token: str,
179
- logger: Logger = None) -> dict[str, Any]:
197
+ def jwt_get_claims(errors: list[str] | None,
198
+ token: str,
199
+ logger: Logger = None) -> dict[str, Any]:
180
200
  """
181
201
  Obtain and return the claims set of a JWT *token*.
182
202
 
@@ -192,11 +212,19 @@ def jwt_get_token_claims(errors: list[str],
192
212
  logger.debug(msg=f"Retrieve claims for token '{token}'")
193
213
 
194
214
  try:
195
- result = __jwt_data.get_token_claims(token=token)
215
+ reply: dict[str, Any] = jwt.decode(jwt=token,
216
+ options={"verify_signature": False})
217
+ if reply.get("nat") in ["A", "R"]:
218
+ result = jwt.decode(jwt=token,
219
+ key=JWT_DECODING_KEY,
220
+ algorithms=[JWT_DEFAULT_ALGORITHM])
221
+ else:
222
+ result = reply
196
223
  except Exception as e:
197
224
  if logger:
198
225
  logger.error(msg=str(e))
199
- errors.append(str(e))
226
+ if isinstance(errors, list):
227
+ errors.append(str(e))
200
228
 
201
229
  return result
202
230
 
@@ -226,26 +254,11 @@ def jwt_verify_request(request: Request,
226
254
  token: str = auth_header.split(" ")[1]
227
255
  if logger:
228
256
  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"
257
+ errors: list[str] = []
258
+ jwt_validate_token(errors=errors,
259
+ token=token)
260
+ if errors:
261
+ err_msg = "; ".join(errors)
249
262
  else:
250
263
  # no 'Bearer' found, report the error
251
264
  err_msg = "Request header has no 'Bearer' data"
@@ -288,13 +301,14 @@ def jwt_claims(token: str = None) -> Response:
288
301
  # has the token been obtained ?
289
302
  if token:
290
303
  # 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),
304
+ errors: list[str] = []
305
+ token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
306
+ token=token)
307
+ if errors:
308
+ result = Response(response=errors,
297
309
  status=400)
310
+ else:
311
+ result = jsonify(token_claims)
298
312
  else:
299
313
  # no, report the problem
300
314
  result = Response(response="Invalid parameters",
@@ -303,26 +317,28 @@ def jwt_claims(token: str = None) -> Response:
303
317
  return result
304
318
 
305
319
 
306
- def jwt_token(service_params: dict[str, Any] = None) -> Response:
320
+ def jwt_tokens(service_params: dict[str, Any] = None) -> Response:
307
321
  """
308
- REST service entry point for obtaining JWT tokens.
322
+ REST service entry point for obtaining or refreshing JWT tokens.
309
323
 
310
324
  The requester must send, as parameter *service_params* or in the body of the request:
311
325
  {
312
- "account-id": "<string>" - required account identification
313
- "<custom-claim-key-1>": "<custom-claim-value-1>", - optional superceding custom claims
326
+ "account-id": "<string>" - required account identification
327
+ "refresh_token": <string> - if refresh is being requested
328
+ "<account-claim-key-1>": "<account-claim-value-1>", - optional superceding account claims
314
329
  ...
315
- "<custom-claim-key-n>": "<custom-claim-value-n>"
330
+ "<account-claim-key-n>": "<account-claim-value-n>"
316
331
  }
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
-
332
+ if provided, the refresh token will cause a token refresh operation to be carried out.
333
+ Otherwise, a regular token issue operation is carried out, with the optional superceding
334
+ account claims being used (claims currently registered for the account may be overridden).
320
335
 
321
336
  Structure of the return data:
322
337
  {
323
338
  "access_token": <jwt-token>,
324
339
  "created_in": <timestamp>,
325
- "expires_in": <seconds-to-expiration>
340
+ "expires_in": <seconds-to-expiration>,
341
+ "refresh_token": <jwt-token>
326
342
  }
327
343
 
328
344
  :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
@@ -338,21 +354,39 @@ def jwt_token(service_params: dict[str, Any] = None) -> Response:
338
354
  with contextlib.suppress(Exception):
339
355
  params = request.get_json()
340
356
  account_id: str | None = params.pop("account-id", None)
357
+ refresh_token: str | None = params.pop("refresh-token", None)
358
+ err_msg: str | None = None
359
+ token_data: dict[str, Any] | None = None
341
360
 
342
361
  # has the account been identified ?
343
362
  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)
363
+ # yes, proceed
364
+ if refresh_token:
365
+ errors: list[str] = []
366
+ claims: dict[str, Any] = jwt_get_claims(errors=errors,
367
+ token=refresh_token)
368
+ if errors:
369
+ err_msg = "; ".join(errors)
370
+ elif claims.get("nat") != "R":
371
+ err_msg = "Invalid parameters"
372
+ else:
373
+ params = claims
374
+
375
+ if not err_msg:
376
+ try:
377
+ token_data = __jwt_data.issue_tokens(account_id=account_id,
378
+ account_claims=params)
379
+ except Exception as e:
380
+ # token issuing failed
381
+ err_msg = str(e)
353
382
  else:
354
383
  # no, report the problem
355
- result = Response(response="Invalid parameters",
384
+ err_msg = "Invalid parameters"
385
+
386
+ if err_msg:
387
+ result = Response(response=err_msg,
356
388
  status=401)
389
+ else:
390
+ result = jsonify(token_data)
357
391
 
358
392
  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.1
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=4dBUGZTw1c-TeZukYjwAM7VWFzoFBSEyAN26jgjQRtM,1022
2
+ pypomes_jwt/jwt_constants.py,sha256=k1PFqBF7KI2Ie8ErOW1zw9IWTgqjkxvU4m2eKGr_1EA,2927
3
+ pypomes_jwt/jwt_data.py,sha256=npKZqHZbftvvOGCRAl-2yovUbqCQW0FvcQvyuYGXA_U,14189
4
+ pypomes_jwt/jwt_pomes.py,sha256=w2sgJUF4CZibBCxU_-PZvrQyMm1saP0FBnf1yCCUSak,14247
5
+ pypomes_jwt-0.7.1.dist-info/METADATA,sha256=S092HRvlJYZVEgHbzJyZ3o1bAqlIqUwEri37srho9jA,599
6
+ pypomes_jwt-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.7.1.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.7.1.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,,