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 +14 -14
- pypomes_jwt/jwt_constants.py +52 -0
- pypomes_jwt/jwt_data.py +165 -292
- pypomes_jwt/jwt_pomes.py +155 -122
- {pypomes_jwt-0.6.9.dist-info → pypomes_jwt-0.7.1.dist-info}/METADATA +3 -3
- pypomes_jwt-0.7.1.dist-info/RECORD +8 -0
- pypomes_jwt-0.6.9.dist-info/RECORD +0 -7
- {pypomes_jwt-0.6.9.dist-info → pypomes_jwt-0.7.1.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.6.9.dist-info → pypomes_jwt-0.7.1.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/__init__.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
from .
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
#
|
|
15
|
-
"
|
|
16
|
-
|
|
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
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
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
|
|
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
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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]
|
|
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:
|
|
64
|
-
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
93
|
-
:param
|
|
94
|
-
:param
|
|
95
|
-
:param
|
|
96
|
-
:param
|
|
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
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
147
|
+
else:
|
|
148
|
+
logger.warning(f"No JWT data found for '{account_id}'")
|
|
166
149
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 *
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
89
|
-
:param
|
|
90
|
-
:param
|
|
91
|
-
:param
|
|
92
|
-
:param
|
|
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.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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) ->
|
|
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.
|
|
136
|
-
|
|
107
|
+
return __jwt_data.remove_access(account_id=account_id,
|
|
108
|
+
logger=logger)
|
|
137
109
|
|
|
138
110
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger: Logger = None) ->
|
|
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
|
-
|
|
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
|
|
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.
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
191
|
+
if isinstance(errors, list):
|
|
192
|
+
errors.append(str(e))
|
|
174
193
|
|
|
175
194
|
return result
|
|
176
195
|
|
|
177
196
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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>"
|
|
314
|
-
"
|
|
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
|
-
"<
|
|
330
|
+
"<account-claim-key-n>": "<account-claim-value-n>"
|
|
317
331
|
}
|
|
318
|
-
|
|
319
|
-
|
|
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,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
13
|
+
Requires-Dist: cryptography>=44.0.2
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
|
-
Requires-Dist: pypomes-core>=1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|