pypomes-jwt 0.6.9__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,24 +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_ENDPOINT_URL,
6
- JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
7
- JWT_HS_SECRET_KEY, JWT_RSA_PRIVATE_KEY, JWT_RSA_PUBLIC_KEY,
8
- jwt_needed, jwt_verify_request, jwt_claims, jwt_token,
9
- 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,
10
10
  jwt_assert_access, jwt_set_access, jwt_remove_access
11
11
  )
12
12
 
13
13
  __all__ = [
14
- # jwt_data
15
- "jwt_request_token", "jwt_validate_token",
16
- # jwt_pomes
17
- "JWT_ENDPOINT_URL",
14
+ # jwt_constants
15
+ "JWT_DB_ENGINE", "JWT_DB_HOST", "JWT_DB_NAME",
16
+ "JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
18
17
  "JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
19
- "JWT_HS_SECRET_KEY", "JWT_RSA_PRIVATE_KEY", "JWT_RSA_PUBLIC_KEY",
20
- "jwt_needed", "jwt_verify_request", "jwt_claims", "jwt_token",
21
- "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",
22
22
  "jwt_assert_access", "jwt_set_access", "jwt_remove_access"
23
23
  ]
24
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
@@ -1,12 +1,16 @@
1
1
  import jwt
2
2
  import requests
3
+ import string
3
4
  from datetime import datetime, timezone
4
- from jwt.exceptions import InvalidTokenError
5
5
  from logging import Logger
6
6
  from pypomes_core import str_random
7
7
  from requests import Response
8
8
  from threading import Lock
9
- from typing import Any, Literal
9
+ from typing import Any
10
+
11
+ from .jwt_constants import (
12
+ JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY
13
+ )
10
14
 
11
15
 
12
16
  class JwtData:
@@ -15,171 +19,153 @@ class JwtData:
15
19
 
16
20
  Instance variables:
17
21
  - access_lock: lock for safe multi-threading access
18
- - access_data: list with dictionaries holding the JWT token data:
19
- [
20
- {
21
- "control-data": { # control data
22
- "remote-provider": <bool>, # whether the JWT provider is a remote server
23
- "access-token": <jwt-token>, # access token
24
- "algorithm": <string>, # HS256, HS512, RSA256, RSA512
25
- "request-timeout": <int>, # in seconds - defaults to no timeout
26
- "access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
27
- "refresh-exp": <timestamp>, # expiration time for the refresh operation
28
- "hs-secret-key": <bytes>, # HS secret key
29
- "rsa-private-key": <bytes>, # RSA private key
30
- "rsa-public-key": <bytes>, # RSA public key
31
- },
32
- "reserved-claims": { # reserved claims
33
- "exp": <timestamp>, # expiration time
34
- "iat": <timestamp> # issued at
35
- "iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
36
- "jti": <string>, # JWT id
37
- "sub": <string> # subject (the account identification)
38
- # not used:
39
- # "aud": <string> # audience
40
- # "nbt": <timestamp> # not before time
41
- },
42
- "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": {
43
34
  "birthdate": <string>, # subject's birth date
44
35
  "email": <string>, # subject's email
45
36
  "gender": <string>, # subject's gender
46
37
  "name": <string>, # subject's name
47
- "roles": <List[str]> # subject roles
48
- },
49
- "custom-claims": { # custom claims (may be empty)
50
- "<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
51
40
  ...
52
- "<custom-claim-key-n>": "<custom-claim-value-n>"
53
41
  }
54
42
  },
55
43
  ...
56
- ]
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
57
71
  """
58
72
  def __init__(self) -> None:
59
73
  """
60
74
  Initizalize the token access data.
61
75
  """
62
76
  self.access_lock: Lock = Lock()
63
- self.access_data: list[dict[str, dict[str, Any]]] = []
64
-
65
- def add_access_data(self,
66
- account_id: str,
67
- reference_url: str,
68
- claims: dict[str, Any],
69
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
70
- access_max_age: int,
71
- refresh_max_age: int,
72
- hs_secret_key: bytes,
73
- rsa_private_key: bytes,
74
- rsa_public_key: bytes,
75
- request_timeout: int,
76
- remote_provider: bool,
77
- 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:
78
91
  """
79
92
  Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
80
93
 
81
- The parameter *claims* may contain public and custom claims. Currently, the public claims supported
82
- are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else is considered to be custom
83
- claims, and sent to the remote JWT provided, if applicable.
84
-
85
- Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
86
- (typically, an acess-key/hs-secret-key pair), have been previously validated elsewhere.
87
- 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.
88
98
 
89
99
  :param account_id: the account identification
90
100
  :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
91
101
  :param claims: the JWT claimset, as key-value pairs
92
- :param algorithm: the algorithm used to sign the token with
93
- :param access_max_age: token duration (in seconds)
94
- :param refresh_max_age: duration for the refresh operation (in seconds)
95
- :param hs_secret_key: secret key for HS authentication
96
- :param rsa_private_key: private key for RSA authentication
97
- :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
98
107
  :param request_timeout: timeout for the requests to the reference URL
99
108
  :param remote_provider: whether the JWT provider is a remote server
100
109
  :param logger: optional logger
101
110
  """
102
- # Do the access data already exist ?
103
- if not self.get_access_data(account_id=account_id):
104
- # no, build control data
105
- control_data: dict[str, Any] = {
106
- "algorithm": algorithm,
107
- "access-max-age": access_max_age,
108
- "request-timeout": request_timeout,
109
- "remote-provider": remote_provider,
110
- "refresh-exp": int(datetime.now(tz=timezone.utc).timestamp()) + refresh_max_age
111
- }
112
- if algorithm in ["HS256", "HS512"]:
113
- control_data["hs-secret-key"] = hs_secret_key
114
- else:
115
- control_data["rsa-private-key"] = rsa_private_key
116
- control_data["rsa-public-key"] = rsa_public_key
117
-
118
- # build claims
119
- reserved_claims: dict[str, Any] = {
120
- "sub": account_id,
121
- "iss": reference_url,
122
- "exp": 0,
123
- "iat": 0,
124
- "jti": "<jwt-id>",
125
- }
126
- custom_claims: dict[str, Any] = {}
127
- public_claims: dict[str, Any] = {}
128
- for key, value in claims.items():
129
- if key in ["birthdate", "email", "gender", "name", "roles"]:
130
- public_claims[key] = value
131
- else:
132
- custom_claims[key] = value
133
- # store access data
134
- item_data = {
135
- "control-data": control_data,
136
- "reserved-claims": reserved_claims,
137
- "public-claims": public_claims,
138
- "custom-claims": custom_claims
139
- }
140
- with self.access_lock:
141
- self.access_data.append(item_data)
142
- if logger:
143
- logger.debug(f"JWT data added for '{account_id}': {item_data}")
144
- elif logger:
145
- logger.warning(f"JWT data already exists for '{account_id}'")
146
-
147
- def remove_access_data(self,
148
- account_id: str,
149
- 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:
150
133
  """
151
134
  Remove from storage the access data for *account_id*.
152
135
 
153
136
  :param account_id: the account identification
154
137
  :param logger: optional logger
138
+ return: *True* if the access data was removed, *False* otherwise
155
139
  """
156
- # obtain the access data item in storage
157
- item_data: dict[str, dict[str, Any]] = self.get_access_data(account_id=account_id,
158
- logger=logger)
159
- if item_data:
160
- with self.access_lock:
161
- self.access_data.remove(item_data)
162
- 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:
163
146
  logger.debug(f"Removed JWT data for '{account_id}'")
164
- elif logger:
165
- logger.warning(f"No JWT data found for '{account_id}'")
147
+ else:
148
+ logger.warning(f"No JWT data found for '{account_id}'")
166
149
 
167
- def get_token_data(self,
168
- account_id: str,
169
- superceding_claims: dict[str, Any] = None,
170
- 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]:
171
156
  """
172
- 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*.
173
158
 
174
159
  Structure of the return data:
175
160
  {
176
161
  "access_token": <jwt-token>,
177
162
  "created_in": <timestamp>,
178
- "expires_in": <seconds-to-expiration>
163
+ "expires_in": <seconds-to-expiration>,
164
+ "refresh_token": <jwt-token>
179
165
  }
180
166
 
181
167
  :param account_id: the account identification
182
- :param superceding_claims: if provided, may supercede registered custom claims
168
+ :param account_claims: if provided, may supercede registered account-related claims
183
169
  :param logger: optional logger
184
170
  :return: the JWT token data, or *None* if error
185
171
  :raises InvalidTokenError: token is invalid
@@ -195,28 +181,27 @@ class JwtData:
195
181
  :raises RuntimeError: access data not found for the given *account_id*, or
196
182
  the remote JWT provider failed to return a token
197
183
  """
198
- # declare the return variable
199
- 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)
200
190
 
201
- # obtain the item in storage
202
- access_data: dict[str, Any] = self.get_access_data(account_id=account_id,
203
- logger=logger)
204
- # was the JWT data obtained ?
205
- if access_data:
206
- # yes, proceed
207
- control_data: dict[str, Any] = access_data.get("control-data")
208
- reserved_claims: dict[str, Any] = access_data.get("reserved-claims")
209
- custom_claims: dict[str, Any] = access_data.get("custom-claims")
210
- if superceding_claims:
211
- custom_claims = custom_claims.copy()
212
- 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")
213
202
 
214
- # obtain a new token, if the current token has expired
215
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
216
- if just_now > reserved_claims.get("exp"):
217
- token_jti: str = str_random(size=16)
218
203
  # where is the JWT service provider ?
219
- if control_data.get("remote-provider"):
204
+ if account_data.get("remote-provider"):
220
205
  # JWT service is being provided by a remote server
221
206
  errors: list[str] = []
222
207
  # Structure of the return data:
@@ -224,138 +209,53 @@ class JwtData:
224
209
  # "access_token": <jwt-token>,
225
210
  # "created_in": <timestamp>,
226
211
  # "expires_in": <seconds-to-expiration>,
212
+ # "refresh_token": <jwt-token>
227
213
  # ...
228
214
  # }
229
- reply: dict[str, Any] = jwt_request_token(errors=errors,
230
- reference_url=reserved_claims.get("iss"),
231
- claims=custom_claims,
232
- timeout=control_data.get("request-timeout"),
233
- logger=logger)
234
- if reply:
235
- with self.access_lock:
236
- control_data["access-token"] = reply.get("access_token")
237
- reserved_claims["jti"] = token_jti
238
- reserved_claims["iat"] = reply.get("created_in")
239
- reserved_claims["exp"] = reply.get("created_in") + reply.get("expires_in")
240
- 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:
241
221
  raise RuntimeError(" - ".join(errors))
242
222
  else:
243
223
  # JWT service is being provided locally
244
- token_iat: int = just_now
245
- token_exp: int = just_now + control_data.get("access-max-age")
246
- claims: dict[str, Any] = access_data.get("public-claims").copy()
247
- claims.update(reserved_claims)
248
- claims.update(custom_claims)
249
- claims["jti"] = token_jti
250
- claims["iat"] = token_iat
251
- 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"
252
228
  # may raise an exception
253
- token: str = jwt.encode(payload=claims,
254
- key=(control_data.get("hs-secret-key") or
255
- control_data.get("rsa-private-key")),
256
- algorithm=control_data.get("algorithm"))
257
- with self.access_lock:
258
- reserved_claims["jti"] = token_jti
259
- reserved_claims["iat"] = token_iat
260
- reserved_claims["exp"] = token_exp
261
- control_data["access-token"] = token
262
-
263
- # return the token
264
- result = {
265
- "access_token": control_data.get("access-token"),
266
- "created_in": reserved_claims.get("iat"),
267
- "expires_in": reserved_claims.get("exp") - reserved_claims.get("iat")
268
- }
269
- else:
270
- # JWT access data not found
271
- err_msg: str = f"No JWT access data found for '{account_id}'"
272
- if logger:
273
- logger.error(err_msg)
274
- raise RuntimeError(err_msg)
275
-
276
- return result
277
-
278
- def get_token_claims(self,
279
- token: str,
280
- logger: Logger = None) -> dict[str, Any]:
281
- """
282
- Obtain and return the claims of a JWT *token*.
283
-
284
- :param token: the token to be inspected for claims
285
- :param logger: optional logger
286
- :return: the token's claimset, or *None* if error
287
- :raises InvalidTokenError: token is not valid
288
- :raises ExpiredSignatureError: token has expired
289
- :raises InvalidAlgorithmError: the specified algorithm is not recognized
290
- """
291
- # declare the return variable
292
- result: dict[str, Any]
293
-
294
- if logger:
295
- logger.debug(msg=f"Retrieve claims for JWT token '{token}'")
296
-
297
- access_data: dict[str, Any] = self.get_access_data(access_token=token,
298
- logger=logger)
299
- if access_data:
300
- control_data: dict[str, Any] = access_data.get("control-data")
301
- if control_data.get("remote-provider"):
302
- # provider is remote
303
- 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
+ }
304
244
  else:
305
- # may raise an exception
306
- result = jwt.decode(jwt=token,
307
- key=(control_data.get("hs-secret-key") or
308
- control_data.get("rsa-public-key")),
309
- algorithms=[control_data.get("algorithm")])
310
- else:
311
- raise InvalidTokenError("JWT token is not valid")
312
-
313
- if logger:
314
- logger.debug(f"Retrieved claims for JWT token '{token}': {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)
315
250
 
316
251
  return result
317
252
 
318
- def get_access_data(self,
319
- account_id: str = None,
320
- access_token: str = None,
321
- logger: Logger = None) -> dict[str, dict[str, Any]]:
322
- # noinspection HttpUrlsUsage
323
- """
324
- Retrieve and return the access data in storage for *account_id*, or optionally, for *access_token*.
325
-
326
- Either *account_id* or *access_token* must be provided, the former having precedence over the later.
327
- Note that, whereas *account_id* uniquely identifies an access dataset, *access_token* might not,
328
- and thus, the first dataset associated with it would be returned.
329
-
330
- :param account_id: the account identification
331
- :param access_token: the access token
332
- :param logger: optional logger
333
- :return: the corresponding item in storage, or *None* if not found
334
- """
335
- # initialize the return variable
336
- result: dict[str, dict[str, Any]] | None = None
337
-
338
- if logger:
339
- target: str = f"account id '{account_id}'" if account_id else f"token '{access_token}'"
340
- logger.debug(f"Retrieve access data for {target}")
341
- # retrieve the data
342
- with self.access_lock:
343
- for item_data in self.access_data:
344
- if (account_id and account_id == item_data.get("reserved-claims").get("sub")) or \
345
- (access_token and access_token == item_data.get("control-data").get("access-token")):
346
- result = item_data
347
- break
348
- if logger:
349
- logger.debug(f"Data is '{result}'")
350
253
 
351
- return result
352
-
353
-
354
- def jwt_request_token(errors: list[str],
355
- reference_url: str,
356
- claims: dict[str, Any],
357
- timeout: int = None,
358
- 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]:
359
259
  """
360
260
  Obtain and return the JWT token from *reference_url*, along with its duration.
361
261
 
@@ -401,30 +301,3 @@ def jwt_request_token(errors: list[str],
401
301
  errors.append(err_msg)
402
302
 
403
303
  return result
404
-
405
-
406
- def jwt_validate_token(token: str,
407
- key: bytes | str,
408
- algorithm: str,
409
- logger: Logger = None) -> None:
410
- """
411
- Verify if *token* ia a valid JWT token.
412
-
413
- Raise an appropriate exception if validation failed.
414
-
415
- :param token: the token to be validated
416
- :param key: the secret or public key used to create the token (HS or RSA authentication, respectively)
417
- :param algorithm: the algorithm used to to sign the token with
418
- :param logger: optional logger
419
- :raises InvalidTokenError: token is invalid
420
- :raises InvalidKeyError: authentication key is not in the proper format
421
- :raises ExpiredSignatureError: token and refresh period have expired
422
- :raises InvalidSignatureError: signature does not match the one provided as part of the token
423
- """
424
- if logger:
425
- logger.debug(msg=f"Validate JWT token '{token}'")
426
- jwt.decode(jwt=token,
427
- key=key,
428
- algorithms=[algorithm])
429
- if logger:
430
- logger.debug(msg=f"Token '{token}' is valid")
pypomes_jwt/jwt_pomes.py CHANGED
@@ -1,40 +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
- JWT_ENDPOINT_URL: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_ENDPOINT_URL")
23
-
24
- # obtain a RSA private/public key pair
25
- __priv_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
26
- __pub_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
27
- if not __priv_bytes or not __pub_bytes:
28
- __priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
29
- key_size=2048)
30
- __priv_bytes = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
31
- format=serialization.PrivateFormat.PKCS8,
32
- encryption_algorithm=serialization.NoEncryption())
33
- __pub_key: RSAPublicKey = __priv_key.public_key()
34
- __pub_bytes = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
35
- format=serialization.PublicFormat.SubjectPublicKeyInfo)
36
- JWT_RSA_PRIVATE_KEY: Final[bytes] = __priv_bytes
37
- 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
38
12
 
39
13
  # the JWT data object
40
14
  __jwt_data: JwtData = JwtData()
@@ -59,23 +33,22 @@ def jwt_needed(func: callable) -> callable:
59
33
 
60
34
  def jwt_assert_access(account_id: str) -> bool:
61
35
  """
62
- Determine whether access for *ccount_id* has been established.
36
+ Determine whether access for *account_id* has been established.
63
37
 
64
38
  :param account_id: the account identification
65
39
  :return: *True* if access data exists for *account_id*, *False* otherwise
66
40
  """
67
- 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
68
42
 
69
43
 
70
44
  def jwt_set_access(account_id: str,
71
45
  reference_url: str,
72
46
  claims: dict[str, Any],
73
- algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
74
47
  access_max_age: int = JWT_ACCESS_MAX_AGE,
75
48
  refresh_max_age: int = JWT_REFRESH_MAX_AGE,
76
- secret_key: bytes = JWT_HS_SECRET_KEY,
77
- private_key: bytes = JWT_RSA_PRIVATE_KEY,
78
- public_key: bytes = JWT_RSA_PUBLIC_KEY,
49
+ grace_interval: int = None,
50
+ token_audience: str = None,
51
+ token_nonce: str = None,
79
52
  request_timeout: int = None,
80
53
  remote_provider: bool = True,
81
54
  logger: Logger = None) -> None:
@@ -85,12 +58,11 @@ def jwt_set_access(account_id: str,
85
58
  :param account_id: the account identification
86
59
  :param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
87
60
  :param claims: the JWT claimset, as key-value pairs
88
- :param algorithm: the authentication type
89
- :param access_max_age: token duration, in seconds
90
- :param refresh_max_age: duration for the refresh operation, in seconds
91
- :param secret_key: secret key for HS authentication
92
- :param private_key: private key for RSA authentication
93
- :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
94
66
  :param request_timeout: timeout for the requests to the reference URL
95
67
  :param remote_provider: whether the JWT provider is a remote server
96
68
  :param logger: optional logger
@@ -107,52 +79,98 @@ def jwt_set_access(account_id: str,
107
79
  reference_url = reference_url[:pos]
108
80
 
109
81
  # register the JWT service
110
- __jwt_data.add_access_data(account_id=account_id,
111
- reference_url=reference_url,
112
- claims=claims,
113
- algorithm=algorithm,
114
- access_max_age=access_max_age,
115
- refresh_max_age=refresh_max_age,
116
- hs_secret_key=secret_key,
117
- rsa_private_key=private_key,
118
- rsa_public_key=public_key,
119
- request_timeout=request_timeout,
120
- remote_provider=remote_provider,
121
- 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)
122
93
 
123
94
 
124
95
  def jwt_remove_access(account_id: str,
125
- logger: Logger = None) -> None:
96
+ logger: Logger = None) -> bool:
126
97
  """
127
98
  Remove from storage the JWT access data for *account_id*.
128
99
 
129
100
  :param account_id: the account identification
130
101
  :param logger: optional logger
102
+ return: *True* if the access data was removed, *False* otherwise
131
103
  """
132
104
  if logger:
133
105
  logger.debug(msg=f"Remove access data for '{account_id}'")
134
106
 
135
- __jwt_data.remove_access_data(account_id=account_id,
136
- logger=logger)
107
+ return __jwt_data.remove_access(account_id=account_id,
108
+ logger=logger)
137
109
 
138
110
 
139
- def jwt_get_token_data(errors: list[str],
140
- account_id: str,
141
- superceding_claims: dict[str, Any] = None,
142
- 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:
143
115
  """
144
- 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*.
145
162
 
146
163
  Structure of the return data:
147
164
  {
148
165
  "access_token": <jwt-token>,
149
166
  "created_in": <timestamp>,
150
- "expires_in": <seconds-to-expiration>
167
+ "expires_in": <seconds-to-expiration>,
168
+ "refresh_token": <jwt-token>
151
169
  }
152
170
 
153
171
  :param errors: incidental error messages
154
172
  :param account_id: the account identification
155
- :param superceding_claims: if provided, may supercede registered custom claims
173
+ :param account_claims: if provided, may supercede registered custom claims
156
174
  :param logger: optional logger
157
175
  :return: the JWT token data, or *None* if error
158
176
  """
@@ -162,22 +180,23 @@ def jwt_get_token_data(errors: list[str],
162
180
  if logger:
163
181
  logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
164
182
  try:
165
- result = __jwt_data.get_token_data(account_id=account_id,
166
- superceding_claims=superceding_claims,
167
- logger=logger)
183
+ result = __jwt_data.issue_tokens(account_id=account_id,
184
+ account_claims=account_claims,
185
+ logger=logger)
168
186
  if logger:
169
187
  logger.debug(msg=f"Data is '{result}'")
170
188
  except Exception as e:
171
189
  if logger:
172
190
  logger.error(msg=str(e))
173
- errors.append(str(e))
191
+ if isinstance(errors, list):
192
+ errors.append(str(e))
174
193
 
175
194
  return result
176
195
 
177
196
 
178
- def jwt_get_token_claims(errors: list[str],
179
- token: str,
180
- 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]:
181
200
  """
182
201
  Obtain and return the claims set of a JWT *token*.
183
202
 
@@ -193,11 +212,19 @@ def jwt_get_token_claims(errors: list[str],
193
212
  logger.debug(msg=f"Retrieve claims for token '{token}'")
194
213
 
195
214
  try:
196
- 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
197
223
  except Exception as e:
198
224
  if logger:
199
225
  logger.error(msg=str(e))
200
- errors.append(str(e))
226
+ if isinstance(errors, list):
227
+ errors.append(str(e))
201
228
 
202
229
  return result
203
230
 
@@ -227,26 +254,11 @@ def jwt_verify_request(request: Request,
227
254
  token: str = auth_header.split(" ")[1]
228
255
  if logger:
229
256
  logger.debug(msg=f"Token is '{token}'")
230
- # retrieve the reference access data
231
- access_data: dict[str, Any] = __jwt_data.get_access_data(access_token=token)
232
- if access_data:
233
- control_data: dict[str, Any] = access_data.get("control-data")
234
- if control_data.get("remote-provider"):
235
- # JWT provider is remote
236
- if datetime.now().timestamp() > access_data.get("reserved-claims").get("exp"):
237
- err_msg = "Token has expired"
238
- else:
239
- # JWT was locally provided
240
- try:
241
- jwt_validate_token(token=token,
242
- key=(control_data.get("hs-secret-key") or
243
- control_data.get("rsa-public-key")),
244
- algorithm=control_data.get("algorithm"))
245
- except Exception as e:
246
- # validation failed
247
- err_msg = str(e)
248
- else:
249
- 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)
250
262
  else:
251
263
  # no 'Bearer' found, report the error
252
264
  err_msg = "Request header has no 'Bearer' data"
@@ -289,13 +301,14 @@ def jwt_claims(token: str = None) -> Response:
289
301
  # has the token been obtained ?
290
302
  if token:
291
303
  # yes, obtain the token data
292
- try:
293
- token_claims: dict[str, Any] = __jwt_data.get_token_claims(token=token)
294
- result = jsonify(token_claims)
295
- except Exception as e:
296
- # claims extraction failed
297
- 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,
298
309
  status=400)
310
+ else:
311
+ result = jsonify(token_claims)
299
312
  else:
300
313
  # no, report the problem
301
314
  result = Response(response="Invalid parameters",
@@ -304,26 +317,28 @@ def jwt_claims(token: str = None) -> Response:
304
317
  return result
305
318
 
306
319
 
307
- def jwt_token(service_params: dict[str, Any] = None) -> Response:
320
+ def jwt_tokens(service_params: dict[str, Any] = None) -> Response:
308
321
  """
309
- REST service entry point for obtaining JWT tokens.
322
+ REST service entry point for obtaining or refreshing JWT tokens.
310
323
 
311
324
  The requester must send, as parameter *service_params* or in the body of the request:
312
325
  {
313
- "account-id": "<string>" - required account identification
314
- "<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
315
329
  ...
316
- "<custom-claim-key-n>": "<custom-claim-value-n>"
330
+ "<account-claim-key-n>": "<account-claim-value-n>"
317
331
  }
318
- If provided, the superceding custom claims will be sent to the remote provider, if applicable
319
- (custom claims currently registered for the account may be overridden).
320
-
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).
321
335
 
322
336
  Structure of the return data:
323
337
  {
324
338
  "access_token": <jwt-token>,
325
339
  "created_in": <timestamp>,
326
- "expires_in": <seconds-to-expiration>
340
+ "expires_in": <seconds-to-expiration>,
341
+ "refresh_token": <jwt-token>
327
342
  }
328
343
 
329
344
  :param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
@@ -339,21 +354,39 @@ def jwt_token(service_params: dict[str, Any] = None) -> Response:
339
354
  with contextlib.suppress(Exception):
340
355
  params = request.get_json()
341
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
342
360
 
343
361
  # has the account been identified ?
344
362
  if account_id:
345
- # yes, obtain the token data
346
- try:
347
- token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
348
- superceding_claims=params)
349
- result = jsonify(token_data)
350
- except Exception as e:
351
- # token validation failed
352
- result = Response(response=str(e),
353
- 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)
354
382
  else:
355
383
  # no, report the problem
356
- result = Response(response="Invalid parameters",
384
+ err_msg = "Invalid parameters"
385
+
386
+ if err_msg:
387
+ result = Response(response=err_msg,
357
388
  status=401)
389
+ else:
390
+ result = jsonify(token_data)
358
391
 
359
392
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.6.9
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
- Requires-Dist: pypomes-core>=1.7.9
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=CwpFtZEi1Yo1i4ulyR65yvo_fJpSRMJsXwzV75E3K0A,988
2
- pypomes_jwt/jwt_data.py,sha256=o89rMzaUp3-GpX3zei2UkFzYacyjmMvi2QPq1vBaNGY,19945
3
- pypomes_jwt/jwt_pomes.py,sha256=rhYoqD57yXayJoePdjbB3RfPAIg23o6YQKkdnD4tz0c,14222
4
- pypomes_jwt-0.6.9.dist-info/METADATA,sha256=penqUj2BH3vbm9Br2kOv15Gcj5fkBj6Jx6tug_bbYxY,599
5
- pypomes_jwt-0.6.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- pypomes_jwt-0.6.9.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
7
- pypomes_jwt-0.6.9.dist-info/RECORD,,