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 +14 -12
- pypomes_jwt/jwt_constants.py +52 -0
- pypomes_jwt/jwt_data.py +164 -293
- pypomes_jwt/jwt_pomes.py +155 -121
- {pypomes_jwt-0.7.0.dist-info → pypomes_jwt-0.7.1.dist-info}/METADATA +2 -2
- pypomes_jwt-0.7.1.dist-info/RECORD +8 -0
- pypomes_jwt-0.7.0.dist-info/RECORD +0 -7
- {pypomes_jwt-0.7.0.dist-info → pypomes_jwt-0.7.1.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.7.0.dist-info → pypomes_jwt-0.7.1.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/__init__.py
CHANGED
|
@@ -1,22 +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_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
|
-
#
|
|
14
|
-
"
|
|
15
|
-
|
|
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
|
-
"
|
|
18
|
-
|
|
19
|
-
"
|
|
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
|
|
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
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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]
|
|
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:
|
|
65
|
-
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
94
|
-
:param
|
|
95
|
-
:param
|
|
96
|
-
:param
|
|
97
|
-
:param
|
|
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
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
147
|
+
else:
|
|
148
|
+
logger.warning(f"No JWT data found for '{account_id}'")
|
|
167
149
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 *
|
|
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.
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
88
|
-
:param
|
|
89
|
-
:param
|
|
90
|
-
:param
|
|
91
|
-
:param
|
|
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.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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) ->
|
|
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.
|
|
135
|
-
|
|
107
|
+
return __jwt_data.remove_access(account_id=account_id,
|
|
108
|
+
logger=logger)
|
|
136
109
|
|
|
137
110
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
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:
|
|
142
115
|
"""
|
|
143
|
-
|
|
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
|
|
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.
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
191
|
+
if isinstance(errors, list):
|
|
192
|
+
errors.append(str(e))
|
|
173
193
|
|
|
174
194
|
return result
|
|
175
195
|
|
|
176
196
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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>"
|
|
313
|
-
"
|
|
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
|
-
"<
|
|
330
|
+
"<account-claim-key-n>": "<account-claim-value-n>"
|
|
316
331
|
}
|
|
317
|
-
|
|
318
|
-
|
|
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,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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.
|
|
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
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,,
|
|
File without changes
|
|
File without changes
|