pypomes-iam 0.2.9__py3-none-any.whl → 0.7.0__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-iam might be problematic. Click here for more details.
- pypomes_iam/__init__.py +20 -8
- pypomes_iam/iam_actions.py +878 -0
- pypomes_iam/iam_common.py +295 -334
- pypomes_iam/iam_pomes.py +135 -192
- pypomes_iam/iam_services.py +394 -0
- pypomes_iam/provider_pomes.py +175 -72
- pypomes_iam/token_pomes.py +59 -7
- {pypomes_iam-0.2.9.dist-info → pypomes_iam-0.7.0.dist-info}/METADATA +1 -2
- pypomes_iam-0.7.0.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -114
- pypomes_iam/keycloak_pomes.py +0 -117
- pypomes_iam-0.2.9.dist-info/RECORD +0 -11
- {pypomes_iam-0.2.9.dist-info → pypomes_iam-0.7.0.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.2.9.dist-info → pypomes_iam-0.7.0.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_common.py
CHANGED
|
@@ -1,427 +1,388 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
2
|
import sys
|
|
6
|
-
from cachetools import Cache
|
|
7
3
|
from datetime import datetime
|
|
8
|
-
from enum import StrEnum
|
|
9
|
-
from flask import Request
|
|
4
|
+
from enum import StrEnum, auto
|
|
10
5
|
from logging import Logger
|
|
11
6
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
12
7
|
from pypomes_crypto import crypto_jwk_convert
|
|
8
|
+
from threading import RLock
|
|
13
9
|
from typing import Any, Final
|
|
14
10
|
|
|
15
11
|
|
|
16
12
|
class IamServer(StrEnum):
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
"""
|
|
14
|
+
Supported IAM servers.
|
|
15
|
+
"""
|
|
16
|
+
JUSBR = auto()
|
|
17
|
+
KEYCLOAK = auto()
|
|
18
|
+
|
|
19
19
|
|
|
20
|
+
class IamParam(StrEnum):
|
|
21
|
+
"""
|
|
22
|
+
Parameters for configuring *IAM* servers.
|
|
23
|
+
"""
|
|
24
|
+
ADMIN_ID = "admin-id"
|
|
25
|
+
ADMIN_SECRET = "admin-secret"
|
|
26
|
+
CLIENT_ID = "client-id"
|
|
27
|
+
CLIENT_REALM = "client-realm"
|
|
28
|
+
CLIENT_SECRET = "client-secret"
|
|
29
|
+
ENDPOINT_CALLBACK = "endpoint-callback"
|
|
30
|
+
ENDPOINT_LOGIN = "endpoint-login"
|
|
31
|
+
ENDPOINT_LOGOUT = "endpoint_logout"
|
|
32
|
+
ENDPOINT_TOKEN = "endpoint-token"
|
|
33
|
+
ENDPOINT_EXCHANGE = "endpoint-exchange"
|
|
34
|
+
LOGIN_TIMEOUT = "login-timeout"
|
|
35
|
+
PK_EXPIRATION = "pk-expiration"
|
|
36
|
+
PK_LIFETIME = "pk-lifetime"
|
|
37
|
+
PUBLIC_KEY = "public-key"
|
|
38
|
+
RECIPIENT_ATTR = "recipient-attr"
|
|
39
|
+
URL_BASE = "url-base"
|
|
40
|
+
USERS = "users"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserParam(StrEnum):
|
|
44
|
+
"""
|
|
45
|
+
Parameters for handling *IAM* users.
|
|
46
|
+
"""
|
|
47
|
+
ACCESS_TOKEN = "access-token"
|
|
48
|
+
REFRESH_TOKEN = "refresh-token"
|
|
49
|
+
ACCESS_EXPIRATION = "access-expiration"
|
|
50
|
+
REFRESH_EXPIRATION = "refresh-expiration"
|
|
51
|
+
# transient attributes
|
|
52
|
+
LOGIN_EXPIRATION = "login-expiration"
|
|
53
|
+
LOGIN_ID = "login-id"
|
|
54
|
+
REDIRECT_URI = "redirect-uri"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# The configuration parameters for the IAM servers are specified dynamically dynamically with *iam_setup()*
|
|
58
|
+
# Specifying configuration parameters with environment variables can be done in two ways:
|
|
59
|
+
#
|
|
60
|
+
# 1. for a single *IAM* server, specify the data set
|
|
61
|
+
# - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed if administrative duties are performed)
|
|
62
|
+
# - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed if administrative duties are performed)
|
|
63
|
+
# - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
|
|
64
|
+
# - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
|
|
65
|
+
# - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
|
|
66
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (required)
|
|
67
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (required)
|
|
68
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (required)
|
|
69
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (required)
|
|
70
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_PROVIDER* (optional, needed if requesting tokens to providers)
|
|
71
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (required)
|
|
72
|
+
# - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
|
|
73
|
+
# - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
|
|
74
|
+
# - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
|
|
75
|
+
# - *<APP_PREFIX>_IAM_URL_BASE* (required)
|
|
76
|
+
#
|
|
77
|
+
# 2. for multiple *IAM* servers, specify the data set above for each server,
|
|
78
|
+
# respectively replacing *IAM* with a name in *IamServer* (currently, *JUSBR* and *KEYCLOAK* are supported).
|
|
79
|
+
#
|
|
80
|
+
# 3. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
|
|
81
|
+
# as they are reserved for internal use
|
|
20
82
|
|
|
21
83
|
# registry structure:
|
|
22
84
|
# { <IamServer>:
|
|
23
85
|
# {
|
|
86
|
+
# "base-url": <str>,
|
|
87
|
+
# "admin-id": <str>,
|
|
88
|
+
# "admin-secret": <str>,
|
|
24
89
|
# "client-id": <str>,
|
|
25
90
|
# "client-secret": <str>,
|
|
91
|
+
# "client-realm": <str,
|
|
26
92
|
# "client-timeout": <int>,
|
|
27
|
-
# "
|
|
93
|
+
# "recipient-attr": <str>,
|
|
94
|
+
# "public-key": <str>,
|
|
28
95
|
# "pk-lifetime": <int>,
|
|
29
96
|
# "pk-expiration": <int>,
|
|
30
|
-
# "
|
|
31
|
-
# "logger": <Logger>,
|
|
32
|
-
# "cache": <FIFOCache>,
|
|
33
|
-
# "redirect-uri": <str> <-- transient
|
|
97
|
+
# "users": {}
|
|
34
98
|
# },
|
|
35
99
|
# ...
|
|
36
100
|
# }
|
|
37
|
-
# data in "
|
|
101
|
+
# data in "users":
|
|
38
102
|
# {
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
103
|
+
# "<user-id>": {
|
|
104
|
+
# "access-token": <str>
|
|
105
|
+
# "refresh-token": <str>
|
|
106
|
+
# "access-expiration": <timestamp>,
|
|
107
|
+
# "refresh-expiration": <timestamp>,
|
|
108
|
+
# # transient attributes
|
|
109
|
+
# "login-expiration": <timestamp>,
|
|
110
|
+
# "login-id": <str>,
|
|
111
|
+
# "redirect-uri": <str>
|
|
112
|
+
# },
|
|
49
113
|
# ...
|
|
50
114
|
# }
|
|
51
|
-
|
|
115
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
|
|
52
116
|
|
|
53
117
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
Build the callback URL for redirecting the request to the IAM's authentication page.
|
|
59
|
-
|
|
60
|
-
:param registry: the registry holding the authentication data
|
|
61
|
-
:param args: the arguments passed when requesting the service
|
|
62
|
-
:param logger: optional logger
|
|
63
|
-
:return: the callback URL, with the appropriate parameters
|
|
64
|
-
"""
|
|
65
|
-
# retrieve user data
|
|
66
|
-
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
67
|
-
|
|
68
|
-
# build the user data
|
|
69
|
-
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
70
|
-
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
71
|
-
user_id=oauth_state,
|
|
72
|
-
logger=logger)
|
|
73
|
-
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
74
|
-
user_data["login-id"] = user_id
|
|
75
|
-
timeout: int = _get_login_timeout(registry=registry)
|
|
76
|
-
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
|
|
77
|
-
redirect_uri: str = args.get("redirect-uri")
|
|
78
|
-
registry["redirect-uri"] = redirect_uri
|
|
79
|
-
|
|
80
|
-
# build the login url
|
|
81
|
-
return {
|
|
82
|
-
"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
83
|
-
f"?response_type=code&scope=openid"
|
|
84
|
-
f"&client_id={registry["client-id"]}"
|
|
85
|
-
f"&redirect_uri={redirect_uri}"
|
|
86
|
-
f"&state={oauth_state}")
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _service_logout(registry: dict[str, Any],
|
|
91
|
-
args: dict[str, Any],
|
|
92
|
-
logger: Logger | None) -> None:
|
|
93
|
-
"""
|
|
94
|
-
Remove all data associating *user_id* from *registry*.
|
|
95
|
-
|
|
96
|
-
:param registry: the registry holding the authentication data
|
|
97
|
-
:param args: the arguments passed when requesting the service
|
|
98
|
-
:param logger: optional logger
|
|
99
|
-
"""
|
|
100
|
-
# remove the user data
|
|
101
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
102
|
-
if user_id:
|
|
103
|
-
cache: Cache = registry["cache"]
|
|
104
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
105
|
-
if user_id in users:
|
|
106
|
-
users.pop(user_id)
|
|
107
|
-
if logger:
|
|
108
|
-
logger.debug(msg=f"User '{user_id}' removed from the registry")
|
|
118
|
+
# the lock protecting the data in '_IAM_SERVERS'
|
|
119
|
+
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
120
|
+
_iam_lock: Final[RLock] = RLock()
|
|
109
121
|
|
|
110
122
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
logger: Logger | None) -> tuple[str, str]:
|
|
123
|
+
def _iam_server_from_endpoint(endpoint: str,
|
|
124
|
+
errors: list[str] | None,
|
|
125
|
+
logger: Logger | None) -> IamServer | None:
|
|
115
126
|
"""
|
|
116
|
-
|
|
127
|
+
Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
|
|
117
128
|
|
|
118
|
-
:param
|
|
119
|
-
:param
|
|
120
|
-
:param errors: incidental errors
|
|
129
|
+
:param endpoint: the service's invocation endpoint
|
|
130
|
+
:param errors: incidental error messages
|
|
121
131
|
:param logger: optional logger
|
|
132
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
122
133
|
"""
|
|
123
|
-
from .token_pomes import token_validate
|
|
124
|
-
|
|
125
134
|
# initialize the return variable
|
|
126
|
-
result:
|
|
127
|
-
|
|
128
|
-
# retrieve the users authentication data
|
|
129
|
-
cache: Cache = registry["cache"]
|
|
130
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
131
|
-
|
|
132
|
-
# validate the OAuth2 state
|
|
133
|
-
oauth_state: str = args.get("state")
|
|
134
|
-
user_data: dict[str, Any] | None = None
|
|
135
|
-
if oauth_state:
|
|
136
|
-
for user, data in users.items():
|
|
137
|
-
if user == oauth_state:
|
|
138
|
-
user_data = data
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
# exchange 'code' for the token
|
|
142
|
-
if user_data:
|
|
143
|
-
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
144
|
-
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
145
|
-
errors.append("Operation timeout")
|
|
146
|
-
else:
|
|
147
|
-
users.pop(oauth_state)
|
|
148
|
-
code: str = args.get("code")
|
|
149
|
-
body_data: dict[str, Any] = {
|
|
150
|
-
"grant_type": "authorization_code",
|
|
151
|
-
"code": code,
|
|
152
|
-
"redirect_uri": registry.get("redirect-uri"),
|
|
153
|
-
}
|
|
154
|
-
token = _post_for_token(registry=registry,
|
|
155
|
-
user_data=user_data,
|
|
156
|
-
body_data=body_data,
|
|
157
|
-
errors=errors,
|
|
158
|
-
logger=logger)
|
|
159
|
-
# retrieve the token's claims
|
|
160
|
-
if not errors:
|
|
161
|
-
public_key: str = _get_public_key(registry=registry,
|
|
162
|
-
logger=logger)
|
|
163
|
-
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
164
|
-
issuer=registry["base-url"],
|
|
165
|
-
public_key=public_key,
|
|
166
|
-
errors=errors,
|
|
167
|
-
logger=logger)
|
|
168
|
-
if not errors:
|
|
169
|
-
token_user: str = token_claims["payload"].get("preferred_username")
|
|
170
|
-
login_id = user_data.pop("login-id", None)
|
|
171
|
-
if not login_id or (login_id == token_user):
|
|
172
|
-
users[token_user] = user_data
|
|
173
|
-
result = (token_user, token)
|
|
174
|
-
else:
|
|
175
|
-
errors.append(f"Token was issued to user '{token_user}'")
|
|
176
|
-
else:
|
|
177
|
-
errors.append("Unknown state received")
|
|
135
|
+
result: IamServer | None = None
|
|
178
136
|
|
|
179
|
-
|
|
180
|
-
|
|
137
|
+
for iam_server in _IAM_SERVERS:
|
|
138
|
+
if endpoint.startswith(iam_server):
|
|
139
|
+
result = iam_server
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if not result:
|
|
143
|
+
msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
|
|
144
|
+
if logger:
|
|
145
|
+
logger.error(msg=msg)
|
|
146
|
+
if isinstance(errors, list):
|
|
147
|
+
errors.append(msg)
|
|
181
148
|
|
|
182
149
|
return result
|
|
183
150
|
|
|
184
151
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
logger: Logger = None) -> str:
|
|
152
|
+
def _iam_server_from_issuer(issuer: str,
|
|
153
|
+
errors: list[str] | None,
|
|
154
|
+
logger: Logger | None) -> IamServer | None:
|
|
189
155
|
"""
|
|
190
|
-
Retrieve the
|
|
156
|
+
Retrieve the registered *IAM* server associated with the token's *issuer*.
|
|
191
157
|
|
|
192
|
-
:param
|
|
193
|
-
:param args: the arguments passed when requesting the service
|
|
158
|
+
:param issuer: the token's issuer
|
|
194
159
|
:param errors: incidental error messages
|
|
195
160
|
:param logger: optional logger
|
|
196
|
-
:return: the
|
|
161
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
197
162
|
"""
|
|
198
163
|
# initialize the return variable
|
|
199
|
-
result:
|
|
164
|
+
result: IamServer | None = None
|
|
200
165
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
210
|
-
if now < access_expiration:
|
|
211
|
-
result = token
|
|
212
|
-
else:
|
|
213
|
-
# access token has expired
|
|
214
|
-
refresh_token: str = user_data["refresh-token"]
|
|
215
|
-
if refresh_token:
|
|
216
|
-
refresh_expiration = user_data["refresh-expiration"]
|
|
217
|
-
if now < refresh_expiration:
|
|
218
|
-
body_data: dict[str, str] = {
|
|
219
|
-
"grant_type": "refresh_token",
|
|
220
|
-
"refresh_token": refresh_token
|
|
221
|
-
}
|
|
222
|
-
result = _post_for_token(registry=registry,
|
|
223
|
-
user_data=user_data,
|
|
224
|
-
body_data=body_data,
|
|
225
|
-
errors=errors,
|
|
226
|
-
logger=logger)
|
|
227
|
-
else:
|
|
228
|
-
# refresh token has expired
|
|
229
|
-
err_msg = "Access and refresh tokens expired"
|
|
230
|
-
else:
|
|
231
|
-
err_msg = "Access token expired, no refresh token available"
|
|
232
|
-
else:
|
|
233
|
-
err_msg = f"User '{user_id}' not authenticated"
|
|
234
|
-
|
|
235
|
-
if err_msg and (logger or isinstance(errors, list)):
|
|
236
|
-
err_msg: str = f"User '{user_id}' not authenticated"
|
|
237
|
-
if isinstance(errors, list):
|
|
238
|
-
errors.append(err_msg)
|
|
166
|
+
for iam_server, registry in _IAM_SERVERS.items():
|
|
167
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
168
|
+
if base_url == issuer:
|
|
169
|
+
result = IamServer(iam_server)
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
if not result:
|
|
173
|
+
msg: str = f"Unable to find a IAM server associated with token issuer '{issuer}'"
|
|
239
174
|
if logger:
|
|
240
|
-
logger.error(msg=
|
|
241
|
-
|
|
175
|
+
logger.error(msg=msg)
|
|
176
|
+
if isinstance(errors, list):
|
|
177
|
+
errors.append(msg)
|
|
242
178
|
|
|
243
179
|
return result
|
|
244
180
|
|
|
245
181
|
|
|
246
|
-
def _get_public_key(
|
|
182
|
+
def _get_public_key(iam_server: IamServer,
|
|
183
|
+
errors: list[str] | None,
|
|
247
184
|
logger: Logger | None) -> str:
|
|
248
185
|
"""
|
|
249
|
-
Obtain the public key used by
|
|
186
|
+
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
187
|
+
|
|
188
|
+
This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
|
|
189
|
+
containing the public keys used for various purposes, as indicated in the attribute *use*:
|
|
190
|
+
- *enc*: the key is intended for encryption
|
|
191
|
+
- *sig*: the key is intended for digital signature
|
|
192
|
+
- *wrap*: the key is intended for key wrapping
|
|
193
|
+
|
|
194
|
+
A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
|
|
195
|
+
{
|
|
196
|
+
"keys": [
|
|
197
|
+
{
|
|
198
|
+
"kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
|
|
199
|
+
"kty": "RSA",
|
|
200
|
+
"alg": "RSA256",
|
|
201
|
+
"use": "sig",
|
|
202
|
+
"n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
|
|
203
|
+
"e": "AQAB",
|
|
204
|
+
"x5c": [
|
|
205
|
+
"MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
|
|
206
|
+
],
|
|
207
|
+
"x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
|
|
208
|
+
"x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
|
|
209
|
+
},
|
|
210
|
+
...
|
|
211
|
+
]
|
|
212
|
+
}
|
|
250
213
|
|
|
251
|
-
|
|
214
|
+
Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
|
|
215
|
+
to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
|
|
252
216
|
|
|
253
|
-
:param
|
|
254
|
-
:
|
|
217
|
+
:param iam_server: the reference registered *IAM* server
|
|
218
|
+
:param errors: incidental error messages
|
|
219
|
+
:param logger: optional logger
|
|
220
|
+
:return: the public key in *PEM* format, or *None* if error
|
|
255
221
|
"""
|
|
256
222
|
# initialize the return variable
|
|
257
223
|
result: str | None = None
|
|
258
224
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
225
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
226
|
+
errors=errors,
|
|
227
|
+
logger=logger)
|
|
228
|
+
if registry:
|
|
229
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
230
|
+
if now > registry[IamParam.PK_EXPIRATION]:
|
|
231
|
+
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
232
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
233
|
+
url: str = f"{base_url}/protocol/openid-connect/certs"
|
|
268
234
|
if logger:
|
|
269
|
-
logger.debug(msg=f"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
235
|
+
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
236
|
+
logger.debug(msg=f"GET {url}")
|
|
237
|
+
try:
|
|
238
|
+
response: requests.Response = requests.get(url=url)
|
|
239
|
+
if response.status_code == 200:
|
|
240
|
+
# request succeeded
|
|
241
|
+
if logger:
|
|
242
|
+
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
243
|
+
# select the appropriate JWK
|
|
244
|
+
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
245
|
+
jwk: dict[str, str] | None = None
|
|
246
|
+
for key in reply["keys"]:
|
|
247
|
+
if key.get("use") == "sig":
|
|
248
|
+
jwk = key
|
|
249
|
+
break
|
|
250
|
+
if jwk:
|
|
251
|
+
# convert from 'JWK' to 'PEM' and save it for further use
|
|
252
|
+
result = crypto_jwk_convert(jwk=jwk,
|
|
253
|
+
fmt="PEM")
|
|
254
|
+
registry[IamParam.PUBLIC_KEY] = result
|
|
255
|
+
lifetime: int = registry[IamParam.PK_LIFETIME] or 0
|
|
256
|
+
registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
|
|
257
|
+
if logger:
|
|
258
|
+
logger.debug("Public key obtained and saved")
|
|
259
|
+
else:
|
|
260
|
+
msg = "Signature public key missing from the token issuer's JWKS"
|
|
261
|
+
if logger:
|
|
262
|
+
logger.error(msg=msg)
|
|
263
|
+
if isinstance(errors, list):
|
|
264
|
+
errors.append(msg)
|
|
265
|
+
elif logger:
|
|
266
|
+
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
267
|
+
if hasattr(response, "content") and response.content:
|
|
268
|
+
msg += f", content {response.content}"
|
|
269
|
+
logger.error(msg=msg)
|
|
270
|
+
if isinstance(errors, list):
|
|
271
|
+
errors.append(msg)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
# the operation raised an exception
|
|
274
|
+
msg = exc_format(exc=e,
|
|
275
|
+
exc_info=sys.exc_info())
|
|
276
|
+
if logger:
|
|
277
|
+
logger.error(msg=msg)
|
|
278
|
+
if isinstance(errors, list):
|
|
279
|
+
errors.append(msg)
|
|
280
|
+
else:
|
|
281
|
+
result = registry[IamParam.PUBLIC_KEY]
|
|
283
282
|
|
|
284
283
|
return result
|
|
285
284
|
|
|
286
285
|
|
|
287
|
-
def _get_login_timeout(
|
|
286
|
+
def _get_login_timeout(iam_server: IamServer,
|
|
287
|
+
errors: list[str] | None,
|
|
288
|
+
logger: Logger) -> int | None:
|
|
288
289
|
"""
|
|
289
|
-
Retrieve
|
|
290
|
+
Retrieve the timeout currently applicable for the login operation.
|
|
290
291
|
|
|
291
|
-
:param
|
|
292
|
-
:
|
|
292
|
+
:param iam_server: the reference registered *IAM* server
|
|
293
|
+
:param errors: incidental error messages
|
|
294
|
+
:param logger: optional logger
|
|
295
|
+
:return: the current login timeout, or *None* if the server is unknown or none has been set.
|
|
293
296
|
"""
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
# initialize the return variable
|
|
298
|
+
result: int | None = None
|
|
299
|
+
|
|
300
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
301
|
+
errors=errors,
|
|
302
|
+
logger=logger)
|
|
303
|
+
if registry:
|
|
304
|
+
timeout: int = registry.get("client-timeout")
|
|
305
|
+
if isinstance(timeout, int) and timeout > 0:
|
|
306
|
+
result = timeout
|
|
307
|
+
|
|
308
|
+
return result
|
|
296
309
|
|
|
297
310
|
|
|
298
|
-
def _get_user_data(
|
|
311
|
+
def _get_user_data(iam_server: IamServer,
|
|
299
312
|
user_id: str,
|
|
300
|
-
|
|
313
|
+
errors: list[str] | None,
|
|
314
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
301
315
|
"""
|
|
302
|
-
Retrieve the data for *user_id* from *registry
|
|
316
|
+
Retrieve the data for *user_id* from *iam_server*'s registry.
|
|
303
317
|
|
|
304
318
|
If an entry is not found for *user_id* in the registry, it is created.
|
|
305
319
|
It will remain there until the user is logged out.
|
|
306
320
|
|
|
307
|
-
:param
|
|
308
|
-
:
|
|
321
|
+
:param iam_server: the reference registered *IAM* server
|
|
322
|
+
:param errors: incidental error messages
|
|
323
|
+
:param logger: optional logger
|
|
324
|
+
:return: the data for *user_id* in *iam_server*'s registry, or *None* if the server is unknown
|
|
309
325
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
# initialize the return variable
|
|
327
|
+
result: dict[str, Any] | None = None
|
|
328
|
+
|
|
329
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
330
|
+
errors=errors,
|
|
331
|
+
logger=logger)
|
|
332
|
+
if isinstance(users, dict):
|
|
333
|
+
result = users.get(user_id)
|
|
334
|
+
if not result:
|
|
335
|
+
result = {
|
|
336
|
+
UserParam.ACCESS_TOKEN: None,
|
|
337
|
+
UserParam.REFRESH_TOKEN: None,
|
|
338
|
+
UserParam.ACCESS_EXPIRATION: int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
339
|
+
UserParam.REFRESH_EXPIRATION: sys.maxsize
|
|
340
|
+
}
|
|
341
|
+
users[user_id] = result
|
|
342
|
+
if logger:
|
|
343
|
+
logger.debug(msg=f"Entry for '{user_id}' added to {iam_server}'s registry")
|
|
344
|
+
elif logger:
|
|
345
|
+
logger.debug(msg=f"Entry for '{user_id}' obtained from {iam_server}'s registry")
|
|
325
346
|
|
|
326
347
|
return result
|
|
327
348
|
|
|
328
349
|
|
|
329
|
-
def
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
errors: list[str] | None,
|
|
333
|
-
logger: Logger | None) -> str | None:
|
|
350
|
+
def _get_iam_registry(iam_server: IamServer,
|
|
351
|
+
errors: list[str] | None,
|
|
352
|
+
logger: Logger | None) -> dict[str, Any]:
|
|
334
353
|
"""
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
- "code": <16-character-random-code>
|
|
340
|
-
- "redirect_uri": <redirect-uri>
|
|
341
|
-
For token refresh, *body_data* will have the attributes
|
|
342
|
-
- "grant_type": "refresh_token"
|
|
343
|
-
- "refresh_token": <current-refresh-token>
|
|
344
|
-
|
|
345
|
-
If the operation is successful, the token data is stored in the registry.
|
|
346
|
-
Otherwise, *errors* will contain the appropriate error message.
|
|
347
|
-
|
|
348
|
-
:param registry: the registry holding the authentication data
|
|
349
|
-
:param user_data: the user's data in the registry
|
|
350
|
-
:param body_data: the data to send in the body of the request
|
|
351
|
-
:param errors: incidental errors
|
|
354
|
+
Retrieve the registry associated with *iam_server*.
|
|
355
|
+
|
|
356
|
+
:param iam_server: the reference registered *IAM* server
|
|
357
|
+
:param errors: incidental error messages
|
|
352
358
|
:param logger: optional logger
|
|
353
|
-
:return: the
|
|
359
|
+
:return: the registry associated with *iam_server*, or *None* if the server is unknown
|
|
354
360
|
"""
|
|
355
|
-
#
|
|
356
|
-
result: str
|
|
361
|
+
# assign the return variable
|
|
362
|
+
result: dict[str, Any] = _IAM_SERVERS.get(iam_server)
|
|
357
363
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
client_secret: str = registry["client-secret"]
|
|
361
|
-
if client_secret:
|
|
362
|
-
body_data["client_secret"] = client_secret
|
|
363
|
-
|
|
364
|
-
# obtain the token
|
|
365
|
-
err_msg: str | None = None
|
|
366
|
-
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
367
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
368
|
-
if logger:
|
|
369
|
-
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
370
|
-
ensure_ascii=False)}")
|
|
371
|
-
try:
|
|
372
|
-
# typical return on a token request:
|
|
373
|
-
# {
|
|
374
|
-
# "token_type": "Bearer",
|
|
375
|
-
# "access_token": <str>,
|
|
376
|
-
# "expires_in": <number-of-seconds>,
|
|
377
|
-
# "refresh_token": <str>,
|
|
378
|
-
# "refesh_expires_in": <number-of-seconds>
|
|
379
|
-
# }
|
|
380
|
-
response: requests.Response = requests.post(url=url,
|
|
381
|
-
data=body_data)
|
|
382
|
-
if response.status_code == 200:
|
|
383
|
-
# request succeeded
|
|
384
|
-
if logger:
|
|
385
|
-
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
386
|
-
reply: dict[str, Any] = response.json()
|
|
387
|
-
result = reply.get("access_token")
|
|
388
|
-
user_data["access-token"] = result
|
|
389
|
-
# on token refresh, keep current refresh token if a new one is not provided
|
|
390
|
-
user_data["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
391
|
-
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
392
|
-
refresh_expiration: int = user_data.get("refresh_expires_in")
|
|
393
|
-
user_data["refresh-expiration"] = (now + refresh_expiration) if refresh_expiration else sys.maxsize
|
|
394
|
-
else:
|
|
395
|
-
# request resulted in error
|
|
396
|
-
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
397
|
-
if hasattr(response, "content") and response.content:
|
|
398
|
-
err_msg += f", content '{response.content}'"
|
|
399
|
-
if response.status_code == 400 and body_data.get("grant_type") == "refresh_token":
|
|
400
|
-
# refresh token is no longer valid
|
|
401
|
-
user_data["refresh-token"] = None
|
|
402
|
-
except Exception as e:
|
|
403
|
-
# the operation raised an exception
|
|
404
|
-
err_msg = exc_format(exc=e,
|
|
405
|
-
exc_info=sys.exc_info())
|
|
406
|
-
err_msg = f"POST '{url}': error '{err_msg}'"
|
|
407
|
-
|
|
408
|
-
if err_msg:
|
|
409
|
-
if isinstance(errors, list):
|
|
410
|
-
errors.append(err_msg)
|
|
364
|
+
if not result:
|
|
365
|
+
msg = f"Unknown IAM server '{iam_server}'"
|
|
411
366
|
if logger:
|
|
412
|
-
logger.error(msg=
|
|
367
|
+
logger.error(msg=msg)
|
|
368
|
+
if isinstance(errors, list):
|
|
369
|
+
errors.append(msg)
|
|
413
370
|
|
|
414
371
|
return result
|
|
415
372
|
|
|
416
373
|
|
|
417
|
-
def
|
|
374
|
+
def _get_iam_users(iam_server: IamServer,
|
|
375
|
+
errors: list[str] | None,
|
|
376
|
+
logger: Logger | None) -> dict[str, dict[str, Any]]:
|
|
418
377
|
"""
|
|
419
|
-
|
|
378
|
+
Retrieve the users data storage in *iam_server*'s registry.
|
|
420
379
|
|
|
421
|
-
:param
|
|
422
|
-
:
|
|
380
|
+
:param iam_server: the reference registered *IAM* server
|
|
381
|
+
:param errors: incidental error messages
|
|
382
|
+
:param logger: optional logger
|
|
383
|
+
:return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
423
384
|
"""
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return
|
|
385
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
386
|
+
errors=errors,
|
|
387
|
+
logger=logger)
|
|
388
|
+
return registry[IamParam.USERS] if registry else None
|