pypomes-iam 0.1.6__tar.gz → 0.1.8__tar.gz
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-0.1.6 → pypomes_iam-0.1.8}/PKG-INFO +1 -1
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/pyproject.toml +1 -1
- pypomes_iam-0.1.8/src/pypomes_iam/common_pomes.py +111 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/src/pypomes_iam/jusbr_pomes.py +93 -112
- pypomes_iam-0.1.8/src/pypomes_iam/keycloak_pomes.py +340 -0
- pypomes_iam-0.1.6/src/pypomes_iam/keycloak_pomes.py +0 -213
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/.gitignore +0 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/LICENSE +0 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/README.md +0 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/src/pypomes_iam/__init__.py +0 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/src/pypomes_iam/provider_pomes.py +0 -0
- {pypomes_iam-0.1.6 → pypomes_iam-0.1.8}/src/pypomes_iam/token_pomes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A collection of Python pomes, penyeach (IAM modules)
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from flask import Request
|
|
5
|
+
from logging import Logger
|
|
6
|
+
from pypomes_core import TZ_LOCAL
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_public_key(registry: dict[str, Any],
|
|
11
|
+
url: str,
|
|
12
|
+
logger: Logger | None) -> bytes:
|
|
13
|
+
"""
|
|
14
|
+
Obtain the public key used by the *IAM* to sign the authentication tokens.
|
|
15
|
+
|
|
16
|
+
The public key is saved in *registry*.
|
|
17
|
+
|
|
18
|
+
:param url: the base URL to request the public key
|
|
19
|
+
:return: the public key, in *DER* format
|
|
20
|
+
"""
|
|
21
|
+
from pypomes_crypto import crypto_jwk_convert
|
|
22
|
+
|
|
23
|
+
# initialize the return variable
|
|
24
|
+
result: bytes | None = None
|
|
25
|
+
|
|
26
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
27
|
+
if now > registry.get("key-expiration"):
|
|
28
|
+
# obtain a new public key
|
|
29
|
+
url: str = f"{url}/protocol/openid-connect/certs"
|
|
30
|
+
if logger:
|
|
31
|
+
logger.debug(msg=f"GET '{url}'")
|
|
32
|
+
response: requests.Response = requests.get(url=url)
|
|
33
|
+
if response.status_code == 200:
|
|
34
|
+
# request succeeded
|
|
35
|
+
if logger:
|
|
36
|
+
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
37
|
+
reply: dict[str, Any] = response.json()
|
|
38
|
+
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
39
|
+
fmt="DER")
|
|
40
|
+
registry["public-key"] = result
|
|
41
|
+
duration: int = registry.get("key-lifetime") or 0
|
|
42
|
+
registry["key-expiration"] = now + duration
|
|
43
|
+
elif logger:
|
|
44
|
+
msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
|
|
45
|
+
if hasattr(response, "content") and response.content:
|
|
46
|
+
msg += f", content '{response.content}'"
|
|
47
|
+
logger.error(msg=msg)
|
|
48
|
+
else:
|
|
49
|
+
result = registry.get("public-key")
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_login_timeout(registry: dict[str, Any]) -> int | None:
|
|
55
|
+
"""
|
|
56
|
+
Retrieve from *registry* the timeout currently applicable for the login operation.
|
|
57
|
+
|
|
58
|
+
:return: the current login timeout, or *None* if none has been set.
|
|
59
|
+
"""
|
|
60
|
+
timeout: int = registry.get("client-timeout")
|
|
61
|
+
return timeout if isinstance(timeout, int) and timeout > 0 else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_user_data(registry: dict[str, Any],
|
|
65
|
+
user_id: str,
|
|
66
|
+
logger: Logger | None) -> dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Retrieve the data for *user_id* from *registry*.
|
|
69
|
+
|
|
70
|
+
If an entry is not found for *user_id* in the registry, it is created.
|
|
71
|
+
It will remain there until the user is logged out.
|
|
72
|
+
|
|
73
|
+
:param user_id:
|
|
74
|
+
:return: the data for *user_id* in the registry
|
|
75
|
+
"""
|
|
76
|
+
result: dict[str, Any] = registry["users"].get(user_id)
|
|
77
|
+
if not result:
|
|
78
|
+
result = {"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())}
|
|
79
|
+
registry["users"][user_id] = result
|
|
80
|
+
if logger:
|
|
81
|
+
logger.debug(msg=f"Entry for user '{user_id}' added to the registry")
|
|
82
|
+
elif logger:
|
|
83
|
+
logger.debug(msg=f"Entry for user '{user_id}' obtained from the registry")
|
|
84
|
+
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _user_logout(registry: dict[str, Any],
|
|
89
|
+
user_id: str,
|
|
90
|
+
logger: Logger | None) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Remove all data associating *user_id* from *registry*.
|
|
93
|
+
"""
|
|
94
|
+
# remove the user data
|
|
95
|
+
if user_id and user_id in registry.get("users"):
|
|
96
|
+
registry["users"].pop(user_id)
|
|
97
|
+
if logger:
|
|
98
|
+
logger.debug(msg=f"User '{user_id}' removed from the registry")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _log_init(request: Request) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Build the messages for logging the request entry.
|
|
104
|
+
|
|
105
|
+
:param request: the Request object
|
|
106
|
+
:return: the log message
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
params: str = json.dumps(obj=request.args,
|
|
110
|
+
ensure_ascii=False)
|
|
111
|
+
return f"Request {request.method}:{request.path}, params {params}"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import requests
|
|
2
3
|
import secrets
|
|
3
4
|
import string
|
|
@@ -11,6 +12,10 @@ from pypomes_core import (
|
|
|
11
12
|
)
|
|
12
13
|
from typing import Any, Final
|
|
13
14
|
|
|
15
|
+
from .common_pomes import (
|
|
16
|
+
_get_login_timeout, _get_public_key, _get_user_data, _user_logout, _log_init
|
|
17
|
+
)
|
|
18
|
+
|
|
14
19
|
JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
|
|
15
20
|
JUSBR_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_SECRET")
|
|
16
21
|
JUSBR_CLIENT_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JUSBR_CLIENT_TIMEOUT")
|
|
@@ -34,17 +39,19 @@ JUSBR_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_A
|
|
|
34
39
|
# "client-id": <str>,
|
|
35
40
|
# "client-secret": <str>,
|
|
36
41
|
# "client-timeout": <int>,
|
|
37
|
-
# "public_key": <
|
|
42
|
+
# "public_key": <bytes>,
|
|
43
|
+
# "key-lifetime": <int>,
|
|
38
44
|
# "key-expiration": <int>,
|
|
39
|
-
# "
|
|
45
|
+
# "base-url": <str>,
|
|
40
46
|
# "callback-url": <str>,
|
|
41
47
|
# "users": {
|
|
42
48
|
# "<user-id>": {
|
|
43
49
|
# "cache-obj": <Cache>,
|
|
44
50
|
# "oauth-scope": <str>,
|
|
45
51
|
# "access-expiration": <timestamp>,
|
|
52
|
+
# "login-expiration": <int>, <-- transient
|
|
53
|
+
# "login-id": <str>, <-- transient
|
|
46
54
|
# data in <Cache>:
|
|
47
|
-
# "oauth-state": <str>
|
|
48
55
|
# "access-token": <str>
|
|
49
56
|
# "refresh-token": <str>
|
|
50
57
|
# }
|
|
@@ -65,7 +72,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
65
72
|
token_endpoint: str = JUSBR_ENDPOINT_TOKEN,
|
|
66
73
|
login_endpoint: str = JUSBR_ENDPOINT_LOGIN,
|
|
67
74
|
logout_endpoint: str = JUSBR_ENDPOINT_LOGOUT,
|
|
68
|
-
|
|
75
|
+
base_url: str = JUSBR_URL_AUTH_BASE,
|
|
69
76
|
callback_url: str = JUSBR_URL_AUTH_CALLBACK,
|
|
70
77
|
logger: Logger = None) -> None:
|
|
71
78
|
"""
|
|
@@ -82,7 +89,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
82
89
|
:param token_endpoint: endpoint for retrieving the JusBR authentication token
|
|
83
90
|
:param login_endpoint: endpoint for redirecting user to JusBR login page
|
|
84
91
|
:param logout_endpoint: endpoint for terminating user access to JusBR
|
|
85
|
-
:param
|
|
92
|
+
:param base_url: base URL to request the JusBR services
|
|
86
93
|
:param callback_url: URL for JusBR to callback on login
|
|
87
94
|
:param logger: optional logger
|
|
88
95
|
"""
|
|
@@ -96,7 +103,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
96
103
|
"client-id": client_id,
|
|
97
104
|
"client-secret": client_secret,
|
|
98
105
|
"client-timeout": client_timeout,
|
|
99
|
-
"
|
|
106
|
+
"base-url": base_url,
|
|
100
107
|
"callback-url": callback_url,
|
|
101
108
|
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
102
109
|
"key-lifetime": public_key_lifetime,
|
|
@@ -138,32 +145,44 @@ def service_login() -> Response:
|
|
|
138
145
|
"""
|
|
139
146
|
global _jusbr_registry
|
|
140
147
|
|
|
141
|
-
#
|
|
148
|
+
# log the request
|
|
149
|
+
if _logger:
|
|
150
|
+
msg: str = _log_init(request=request)
|
|
151
|
+
_logger.debug(msg=msg)
|
|
152
|
+
|
|
153
|
+
# retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state')
|
|
142
154
|
input_params: dict[str, Any] = request.values
|
|
143
155
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
144
156
|
user_id: str = input_params.get("user-id") or input_params.get("login") or oauth_state
|
|
145
|
-
# obtain user data
|
|
146
|
-
user_data: dict[str, Any] =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
# obtain the user data
|
|
158
|
+
user_data: dict[str, Any] = _get_user_data(registry=_jusbr_registry,
|
|
159
|
+
user_id=user_id,
|
|
160
|
+
logger=_logger)
|
|
161
|
+
# build the redirect url
|
|
162
|
+
timeout: int = _get_login_timeout(registry=_jusbr_registry)
|
|
150
163
|
safe_cache: Cache
|
|
151
164
|
if timeout:
|
|
152
165
|
safe_cache = TTLCache(maxsize=16,
|
|
153
|
-
ttl=
|
|
166
|
+
ttl=timeout)
|
|
154
167
|
else:
|
|
155
168
|
safe_cache = FIFOCache(maxsize=16)
|
|
156
169
|
safe_cache["oauth-state"] = oauth_state
|
|
157
170
|
user_data["cache-obj"] = safe_cache
|
|
158
|
-
auth_url: str = (f"{_jusbr_registry["
|
|
171
|
+
auth_url: str = (f"{_jusbr_registry["base-url"]}/protocol/openid-connect/auth?response_type=code"
|
|
159
172
|
f"&client_id={_jusbr_registry["client-id"]}"
|
|
160
173
|
f"&redirect_uri={_jusbr_registry["callback-url"]}"
|
|
161
174
|
f"&state={oauth_state}")
|
|
162
175
|
if user_data.get("oauth-scope"):
|
|
163
176
|
auth_url += f"&scope={user_data.get("oauth-scope")}"
|
|
164
177
|
|
|
165
|
-
# redirect request
|
|
166
|
-
|
|
178
|
+
# redirect the request
|
|
179
|
+
result: Response = redirect(location=auth_url)
|
|
180
|
+
|
|
181
|
+
# log the response
|
|
182
|
+
if _logger:
|
|
183
|
+
_logger.debug(msg=f"Response {result}")
|
|
184
|
+
|
|
185
|
+
return result
|
|
167
186
|
|
|
168
187
|
|
|
169
188
|
# @flask_app.route(rule=<login_endpoint>, # JUSBR_LOGIN_ENDPOINT: /iam/jusbr:logout
|
|
@@ -178,17 +197,27 @@ def service_logout() -> Response:
|
|
|
178
197
|
"""
|
|
179
198
|
global _jusbr_registry
|
|
180
199
|
|
|
181
|
-
#
|
|
200
|
+
# log the request
|
|
201
|
+
if _logger:
|
|
202
|
+
msg: str = _log_init(request=request)
|
|
203
|
+
_logger.debug(msg=msg)
|
|
204
|
+
|
|
205
|
+
# retrieve the user id
|
|
182
206
|
input_params: dict[str, Any] = request.args
|
|
183
207
|
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
184
208
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
_logger.debug(f"User '{user_id}' removed from the registry")
|
|
209
|
+
# logout the user
|
|
210
|
+
_user_logout(registry=_jusbr_registry,
|
|
211
|
+
user_id=user_id,
|
|
212
|
+
logger=_logger)
|
|
190
213
|
|
|
191
|
-
|
|
214
|
+
result: Response = Response(status=200)
|
|
215
|
+
|
|
216
|
+
# log the response
|
|
217
|
+
if _logger:
|
|
218
|
+
_logger.debug(msg=f"Response {result}")
|
|
219
|
+
|
|
220
|
+
return result
|
|
192
221
|
|
|
193
222
|
|
|
194
223
|
# @flask_app.route(rule=<callback_endpoint>, # JUSBR_CALLBACK_ENDPOINT: /iam/jusbr:callback
|
|
@@ -202,6 +231,11 @@ def service_callback() -> Response:
|
|
|
202
231
|
global _jusbr_registry
|
|
203
232
|
from .token_pomes import token_validate
|
|
204
233
|
|
|
234
|
+
# log the request
|
|
235
|
+
if _logger:
|
|
236
|
+
msg: str = _log_init(request=request)
|
|
237
|
+
_logger.debug(msg=msg)
|
|
238
|
+
|
|
205
239
|
# validate the OAuth2 state
|
|
206
240
|
oauth_state: str = request.args.get("state")
|
|
207
241
|
user_id: str | None = None
|
|
@@ -225,7 +259,7 @@ def service_callback() -> Response:
|
|
|
225
259
|
body_data: dict[str, Any] = {
|
|
226
260
|
"grant_type": "authorization_code",
|
|
227
261
|
"code": code,
|
|
228
|
-
"
|
|
262
|
+
"redirect_uri": _jusbr_registry.get("callback-url"),
|
|
229
263
|
}
|
|
230
264
|
token = __post_jusbr(user_data=user_data,
|
|
231
265
|
body_data=body_data,
|
|
@@ -233,9 +267,12 @@ def service_callback() -> Response:
|
|
|
233
267
|
logger=_logger)
|
|
234
268
|
# retrieve the token's claims
|
|
235
269
|
if not errors:
|
|
270
|
+
public_key: bytes = _get_public_key(registry=_jusbr_registry,
|
|
271
|
+
url=_jusbr_registry["base-url"],
|
|
272
|
+
logger=_logger)
|
|
236
273
|
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
237
|
-
issuer=_jusbr_registry
|
|
238
|
-
public_key=
|
|
274
|
+
issuer=_jusbr_registry["base-url"],
|
|
275
|
+
public_key=public_key,
|
|
239
276
|
errors=errors,
|
|
240
277
|
logger=_logger)
|
|
241
278
|
if not errors:
|
|
@@ -247,7 +284,7 @@ def service_callback() -> Response:
|
|
|
247
284
|
errors.append(f"Token was issued to user '{token_user}'")
|
|
248
285
|
else:
|
|
249
286
|
msg: str = "Unknown OAuth2 code received"
|
|
250
|
-
if
|
|
287
|
+
if _get_login_timeout(registry=_jusbr_registry):
|
|
251
288
|
msg += " - possible operation timeout"
|
|
252
289
|
errors.append(msg)
|
|
253
290
|
|
|
@@ -260,6 +297,10 @@ def service_callback() -> Response:
|
|
|
260
297
|
"user_id": user_id,
|
|
261
298
|
"access_token": token})
|
|
262
299
|
|
|
300
|
+
# log the response
|
|
301
|
+
if _logger:
|
|
302
|
+
_logger.debug(msg=f"Response {result}")
|
|
303
|
+
|
|
263
304
|
return result
|
|
264
305
|
|
|
265
306
|
|
|
@@ -271,11 +312,14 @@ def service_token() -> Response:
|
|
|
271
312
|
|
|
272
313
|
:return: the response containing the token, or *UNAUTHORIZED*
|
|
273
314
|
"""
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
# log the request
|
|
316
|
+
if _logger:
|
|
317
|
+
msg: str = _log_init(request=request)
|
|
318
|
+
_logger.debug(msg=msg)
|
|
277
319
|
|
|
278
320
|
# retrieve the token
|
|
321
|
+
input_params: dict[str, Any] = request.args
|
|
322
|
+
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
279
323
|
errors: list[str] = []
|
|
280
324
|
token: str = jusbr_get_token(user_id=user_id,
|
|
281
325
|
logger=_logger)
|
|
@@ -286,6 +330,10 @@ def service_token() -> Response:
|
|
|
286
330
|
result = Response("; ".join(errors))
|
|
287
331
|
result.status_code = 401
|
|
288
332
|
|
|
333
|
+
# log the response
|
|
334
|
+
if _logger:
|
|
335
|
+
_logger.debug(msg=f"Response {result}")
|
|
336
|
+
|
|
289
337
|
return result
|
|
290
338
|
|
|
291
339
|
|
|
@@ -305,8 +353,9 @@ def jusbr_get_token(user_id: str,
|
|
|
305
353
|
# initialize the return variable
|
|
306
354
|
result: str | None = None
|
|
307
355
|
|
|
308
|
-
user_data: dict[str, Any] =
|
|
309
|
-
|
|
356
|
+
user_data: dict[str, Any] = _get_user_data(registry=_jusbr_registry,
|
|
357
|
+
user_id=user_id,
|
|
358
|
+
logger=logger)
|
|
310
359
|
safe_cache: Cache = user_data.get("cache-obj")
|
|
311
360
|
if safe_cache:
|
|
312
361
|
access_expiration: int = user_data.get("access-expiration")
|
|
@@ -350,83 +399,13 @@ def jusbr_set_scope(user_id: str,
|
|
|
350
399
|
global _jusbr_registry
|
|
351
400
|
|
|
352
401
|
# retrieve user data
|
|
353
|
-
user_data: dict[str, Any] =
|
|
354
|
-
|
|
402
|
+
user_data: dict[str, Any] = _get_user_data(registry=_jusbr_registry,
|
|
403
|
+
user_id=user_id,
|
|
404
|
+
logger=logger)
|
|
355
405
|
# set the OAuth2 scope
|
|
356
406
|
user_data["oauth-scope"] = scope
|
|
357
407
|
if logger:
|
|
358
|
-
logger.debug(f"Scope for user '{user_id}' set to '{scope}'")
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def __get_public_key(url: str,
|
|
362
|
-
logger: Logger | None) -> str:
|
|
363
|
-
"""
|
|
364
|
-
Obtain the public key used by JusBR to sign the authentication tokens.
|
|
365
|
-
|
|
366
|
-
:param url: the base URL to request the public key
|
|
367
|
-
:return: the public key, in *PEM* format
|
|
368
|
-
"""
|
|
369
|
-
from pypomes_crypto import crypto_jwk_convert
|
|
370
|
-
global _jusbr_registry
|
|
371
|
-
|
|
372
|
-
# initialize the return variable
|
|
373
|
-
result: str | None = None
|
|
374
|
-
|
|
375
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
376
|
-
if now > _jusbr_registry.get("key-expiration"):
|
|
377
|
-
# obtain a new public key
|
|
378
|
-
url: str = f"{url}/protocol/openid-connect/certs"
|
|
379
|
-
response: requests.Response = requests.get(url=url)
|
|
380
|
-
if response.status_code == 200:
|
|
381
|
-
# request succeeded
|
|
382
|
-
reply: dict[str, Any] = response.json()
|
|
383
|
-
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
384
|
-
fmt="PEM")
|
|
385
|
-
_jusbr_registry["public-key"] = result
|
|
386
|
-
duration: int = _jusbr_registry.get("key-lifetime") or 0
|
|
387
|
-
_jusbr_registry["key-expiration"] = now + duration
|
|
388
|
-
elif logger:
|
|
389
|
-
logger.error(msg=f"GET '{url}': failed, "
|
|
390
|
-
f"status {response.status_code}, reason '{response.reason}'")
|
|
391
|
-
else:
|
|
392
|
-
result = _jusbr_registry.get("public-key")
|
|
393
|
-
|
|
394
|
-
return result
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def __get_login_timeout() -> int | None:
|
|
398
|
-
"""
|
|
399
|
-
Retrieve the timeout currently applicable for the login operation.
|
|
400
|
-
|
|
401
|
-
:return: the current login timeout, or *None* if none has been set.
|
|
402
|
-
"""
|
|
403
|
-
global _jusbr_registry
|
|
404
|
-
|
|
405
|
-
timeout: int = _jusbr_registry.get("client-timeout")
|
|
406
|
-
return timeout if isinstance(timeout, int) and timeout > 0 else None
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def __get_user_data(user_id: str,
|
|
410
|
-
logger: Logger | None) -> dict[str, Any]:
|
|
411
|
-
"""
|
|
412
|
-
Retrieve the data for *user_id* from the registry.
|
|
413
|
-
|
|
414
|
-
If an entry is not found for *user_id* in the registry, it is created.
|
|
415
|
-
It will remain there until the user is logged out.
|
|
416
|
-
|
|
417
|
-
:param user_id:
|
|
418
|
-
:return: the data for *user_id* in the registry
|
|
419
|
-
"""
|
|
420
|
-
global _jusbr_registry
|
|
421
|
-
|
|
422
|
-
result: dict[str, Any] = _jusbr_registry["users"].get(user_id)
|
|
423
|
-
if not result:
|
|
424
|
-
result = {"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())}
|
|
425
|
-
_jusbr_registry["users"][user_id] = result
|
|
426
|
-
if logger:
|
|
427
|
-
logger.debug(f"Entry for user '{user_id}' added to registry")
|
|
428
|
-
|
|
429
|
-
return result
|
|
408
|
+
logger.debug(msg=f"Scope for user '{user_id}' set to '{scope}'")
|
|
430
409
|
|
|
431
410
|
|
|
432
411
|
def __post_jusbr(user_data: dict[str, Any],
|
|
@@ -436,7 +415,7 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
436
415
|
"""
|
|
437
416
|
Send a POST request to JusBR to obtain the authentication token data, and return the access token.
|
|
438
417
|
|
|
439
|
-
For
|
|
418
|
+
For token exchange, *body_data* will have the attributes
|
|
440
419
|
- "grant_type": "authorization_code"
|
|
441
420
|
- "code": <16-character-random-code>
|
|
442
421
|
- "redirect_uri": <callback-url>
|
|
@@ -467,8 +446,11 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
467
446
|
# obtain the token
|
|
468
447
|
err_msg: str | None = None
|
|
469
448
|
safe_cache: Cache = user_data.get("cache-obj")
|
|
470
|
-
url: str = _jusbr_registry.get("
|
|
449
|
+
url: str = _jusbr_registry.get("base-url") + "/protocol/openid-connect/token"
|
|
471
450
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
451
|
+
if logger:
|
|
452
|
+
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
453
|
+
ensure_ascii=False)}")
|
|
472
454
|
try:
|
|
473
455
|
# JusBR return on a token request:
|
|
474
456
|
# {
|
|
@@ -481,6 +463,8 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
481
463
|
data=body_data)
|
|
482
464
|
if response.status_code == 200:
|
|
483
465
|
# request succeeded
|
|
466
|
+
if logger:
|
|
467
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
484
468
|
reply: dict[str, Any] = response.json()
|
|
485
469
|
result = reply.get("access_token")
|
|
486
470
|
safe_cache: Cache = FIFOCache(maxsize=1024)
|
|
@@ -489,12 +473,9 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
489
473
|
safe_cache["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
490
474
|
user_data["cache-obj"] = safe_cache
|
|
491
475
|
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
492
|
-
if logger:
|
|
493
|
-
logger.debug(msg=f"POST '{url}': status {response.status_code}")
|
|
494
476
|
else:
|
|
495
477
|
# request resulted in error
|
|
496
|
-
err_msg =
|
|
497
|
-
f"status {response.status_code}, reason '{response.reason}'")
|
|
478
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
498
479
|
if hasattr(response, "content") and response.content:
|
|
499
480
|
err_msg += f", content '{response.content}'"
|
|
500
481
|
if response.status_code == 401 and "refresh_token" in body_data:
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import string
|
|
3
|
+
# import sys
|
|
4
|
+
from cachetools import FIFOCache
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from flask import Flask, Response, redirect, request, jsonify
|
|
7
|
+
from logging import Logger
|
|
8
|
+
from pypomes_core import (
|
|
9
|
+
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str
|
|
10
|
+
)
|
|
11
|
+
from typing import Any, Final
|
|
12
|
+
|
|
13
|
+
from .common_pomes import (
|
|
14
|
+
_get_login_timeout, _get_public_key, _get_user_data, _user_logout, _log_init
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
18
|
+
KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
|
|
19
|
+
KEYCLOAK_CLIENT_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_TIMEOUT")
|
|
20
|
+
|
|
21
|
+
KEYCLOAK_ENDPOINT_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_CALLBACK",
|
|
22
|
+
def_value="/iam/keycloak:callback")
|
|
23
|
+
KEYCLOAK_ENDPOINT_LOGIN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGIN",
|
|
24
|
+
def_value="/iam/keycloak:login")
|
|
25
|
+
KEYCLOAK_ENDPOINT_LOGOUT: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGOUT",
|
|
26
|
+
def_value="/iam/keycloak:logout")
|
|
27
|
+
KEYCLOAK_ENDPOINT_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_TOKEN",
|
|
28
|
+
def_value="/iam/keycloak:get-token")
|
|
29
|
+
|
|
30
|
+
KEYCLOAK_PUBLIC_KEY_LIFETIME: Final[int] = env_get_int(key=f"{APP_PREFIX}_KEYCLOAK_PUBLIC_KEY_LIFETIME",
|
|
31
|
+
def_value=86400) # 24 hours
|
|
32
|
+
KEYCLOAK_REALM: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_REALM")
|
|
33
|
+
KEYCLOAK_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_BASE")
|
|
34
|
+
KEYCLOAK_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_CALLBACK")
|
|
35
|
+
|
|
36
|
+
# registry structure:
|
|
37
|
+
# {
|
|
38
|
+
# "client-id": <str>,
|
|
39
|
+
# "client-secret": <str>,
|
|
40
|
+
# "client-timeout": <int>,
|
|
41
|
+
# "public_key": <str>,
|
|
42
|
+
# "key-lifetime": <int>,
|
|
43
|
+
# "key-expiration": <int>,
|
|
44
|
+
# "base-url": <str>,
|
|
45
|
+
# "callback-url": <str>,
|
|
46
|
+
# "users": {
|
|
47
|
+
# "<user-id>": {
|
|
48
|
+
# "cache-obj": <FIFOCache>,
|
|
49
|
+
# "access-expiration": <timestamp>,
|
|
50
|
+
# "login-expiration": <int>, <-- transient
|
|
51
|
+
# "login-id": <str>, <-- transient
|
|
52
|
+
# data in <FIFOCache>:
|
|
53
|
+
# "access-token": <str>
|
|
54
|
+
# "refresh-token": <str>
|
|
55
|
+
# }
|
|
56
|
+
# }
|
|
57
|
+
# }
|
|
58
|
+
_keycloak_registry: dict[str, Any] = {}
|
|
59
|
+
|
|
60
|
+
# dafault logger
|
|
61
|
+
_logger: Logger | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def keycloak_setup(flask_app: Flask,
|
|
65
|
+
client_id: str = KEYCLOAK_CLIENT_ID,
|
|
66
|
+
client_secret: str = KEYCLOAK_CLIENT_SECRET,
|
|
67
|
+
client_timeout: int = KEYCLOAK_CLIENT_TIMEOUT,
|
|
68
|
+
public_key_lifetime: int = KEYCLOAK_PUBLIC_KEY_LIFETIME,
|
|
69
|
+
realm: str = KEYCLOAK_REALM,
|
|
70
|
+
callback_endpoint: str = KEYCLOAK_ENDPOINT_CALLBACK,
|
|
71
|
+
token_endpoint: str = KEYCLOAK_ENDPOINT_TOKEN,
|
|
72
|
+
login_endpoint: str = KEYCLOAK_ENDPOINT_LOGIN,
|
|
73
|
+
logout_endpoint: str = KEYCLOAK_ENDPOINT_LOGOUT,
|
|
74
|
+
base_url: str = KEYCLOAK_URL_AUTH_BASE,
|
|
75
|
+
callback_url: str = KEYCLOAK_URL_AUTH_CALLBACK,
|
|
76
|
+
logger: Logger = None) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Configure the Keycloak IAM.
|
|
79
|
+
|
|
80
|
+
This should be invoked only once, before the first access to a Keycloak service.
|
|
81
|
+
|
|
82
|
+
:param flask_app: the Flask application
|
|
83
|
+
:param client_id: the client's identification with JusBR
|
|
84
|
+
:param client_secret: the client's password with JusBR
|
|
85
|
+
:param client_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
86
|
+
:param public_key_lifetime: how long to use Keycloak's public key, before refreshing it (in seconds)
|
|
87
|
+
:param realm: the Keycloak realm
|
|
88
|
+
:param callback_endpoint: endpoint for the callback from JusBR
|
|
89
|
+
:param token_endpoint: endpoint for retrieving the JusBR authentication token
|
|
90
|
+
:param login_endpoint: endpoint for redirecting user to JusBR login page
|
|
91
|
+
:param logout_endpoint: endpoint for terminating user access to JusBR
|
|
92
|
+
:param base_url: base URL to request the JusBR services
|
|
93
|
+
:param callback_url: URL for Keycloak to callback on login
|
|
94
|
+
:param logger: optional logger
|
|
95
|
+
"""
|
|
96
|
+
global _keycloak_registry
|
|
97
|
+
|
|
98
|
+
# establish the logger
|
|
99
|
+
global _logger
|
|
100
|
+
_logger = logger
|
|
101
|
+
|
|
102
|
+
# configure the JusBR registry
|
|
103
|
+
_keycloak_registry = {
|
|
104
|
+
"client-id": client_id,
|
|
105
|
+
"client-secret": client_secret,
|
|
106
|
+
"client-timeout": client_timeout,
|
|
107
|
+
"base-url": f"{base_url}/realms/{realm}",
|
|
108
|
+
"callback-url": callback_url,
|
|
109
|
+
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
110
|
+
"key-lifetime": public_key_lifetime,
|
|
111
|
+
"users": []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# establish the endpoints
|
|
115
|
+
if token_endpoint:
|
|
116
|
+
flask_app.add_url_rule(rule=token_endpoint,
|
|
117
|
+
endpoint="keycloak-token",
|
|
118
|
+
view_func=service_token,
|
|
119
|
+
methods=["GET"])
|
|
120
|
+
if login_endpoint:
|
|
121
|
+
flask_app.add_url_rule(rule=login_endpoint,
|
|
122
|
+
endpoint="keycloak-login",
|
|
123
|
+
view_func=service_login,
|
|
124
|
+
methods=["GET"])
|
|
125
|
+
if logout_endpoint:
|
|
126
|
+
flask_app.add_url_rule(rule=logout_endpoint,
|
|
127
|
+
endpoint="keycloak-logout",
|
|
128
|
+
view_func=service_logout,
|
|
129
|
+
methods=["GET"])
|
|
130
|
+
if callback_endpoint:
|
|
131
|
+
flask_app.add_url_rule(rule=callback_endpoint,
|
|
132
|
+
endpoint="keycloak-callback",
|
|
133
|
+
view_func=service_callback,
|
|
134
|
+
methods=["POST"])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:login
|
|
138
|
+
# methods=["GET"])
|
|
139
|
+
def service_login() -> Response:
|
|
140
|
+
"""
|
|
141
|
+
Entry point for the Keycloak login service.
|
|
142
|
+
|
|
143
|
+
Redirect the request to the Keycloak authentication page, with the appropriate parameters.
|
|
144
|
+
|
|
145
|
+
:return: the response from the redirect operation
|
|
146
|
+
"""
|
|
147
|
+
global _keycloak_registry
|
|
148
|
+
|
|
149
|
+
# log the request
|
|
150
|
+
if _logger:
|
|
151
|
+
msg: str = _log_init(request=request)
|
|
152
|
+
_logger.debug(msg=msg)
|
|
153
|
+
|
|
154
|
+
# build the OAuth2 state, and temporarily use it as 'user_id'
|
|
155
|
+
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
156
|
+
# obtain the user data
|
|
157
|
+
user_data: dict[str, Any] = _get_user_data(registry=_keycloak_registry,
|
|
158
|
+
user_id=oauth_state,
|
|
159
|
+
logger=_logger)
|
|
160
|
+
# build the redirect url
|
|
161
|
+
timeout: int = _get_login_timeout(registry=_keycloak_registry)
|
|
162
|
+
safe_cache: FIFOCache
|
|
163
|
+
if timeout:
|
|
164
|
+
safe_cache = FIFOCache(maxsize=16)
|
|
165
|
+
else:
|
|
166
|
+
safe_cache = FIFOCache(maxsize=16)
|
|
167
|
+
safe_cache["valid"] = True
|
|
168
|
+
user_data["cache-obj"] = safe_cache
|
|
169
|
+
auth_url: str = (
|
|
170
|
+
f"{_keycloak_registry["base-url"]}/protocol/openid-connect/auth"
|
|
171
|
+
f"?client_id={_keycloak_registry["client-id"]}"
|
|
172
|
+
f"&response_type=code"
|
|
173
|
+
f"&scope=openid"
|
|
174
|
+
f"&redirect_uri={_keycloak_registry["callback-url"]}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# redirect the request
|
|
178
|
+
result: Response = redirect(location=auth_url)
|
|
179
|
+
|
|
180
|
+
# log the response
|
|
181
|
+
if _logger:
|
|
182
|
+
_logger.debug(msg=f"Response {result}")
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:logout
|
|
188
|
+
# methods=["GET"])
|
|
189
|
+
def service_logout() -> Response:
|
|
190
|
+
"""
|
|
191
|
+
Entry point for the Keycloak logout service.
|
|
192
|
+
|
|
193
|
+
Remove all data associating the user with Keycloak from the registry.
|
|
194
|
+
|
|
195
|
+
:return: response *OK*
|
|
196
|
+
"""
|
|
197
|
+
global _keycloak_registry
|
|
198
|
+
|
|
199
|
+
# log the request
|
|
200
|
+
if _logger:
|
|
201
|
+
msg: str = _log_init(request=request)
|
|
202
|
+
_logger.debug(msg=msg)
|
|
203
|
+
|
|
204
|
+
# retrieve the user id
|
|
205
|
+
input_params: dict[str, Any] = request.args
|
|
206
|
+
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
207
|
+
|
|
208
|
+
# logout the user
|
|
209
|
+
_user_logout(registry=_keycloak_registry,
|
|
210
|
+
user_id=user_id,
|
|
211
|
+
logger=_logger)
|
|
212
|
+
|
|
213
|
+
result: Response = Response(status=200)
|
|
214
|
+
|
|
215
|
+
# log the response
|
|
216
|
+
if _logger:
|
|
217
|
+
_logger.debug(msg=f"Response {result}")
|
|
218
|
+
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_CALLBACK_ENDPOINT: /iam/keycloak:callback
|
|
223
|
+
# methods=["POST"])
|
|
224
|
+
def service_callback() -> Response:
|
|
225
|
+
"""
|
|
226
|
+
Entry point for the callback from Keycloak on authentication operation.
|
|
227
|
+
|
|
228
|
+
:return: the response containing the token, or *NOT AUTHORIZED*
|
|
229
|
+
"""
|
|
230
|
+
global _keycloak_registry
|
|
231
|
+
from .token_pomes import token_validate
|
|
232
|
+
|
|
233
|
+
# log the request
|
|
234
|
+
if _logger:
|
|
235
|
+
msg: str = _log_init(request=request)
|
|
236
|
+
_logger.debug(msg=msg)
|
|
237
|
+
|
|
238
|
+
# validate the OAuth2 state
|
|
239
|
+
oauth_state: str = request.args.get("state")
|
|
240
|
+
user_id: str | None = None
|
|
241
|
+
user_data: dict[str, Any] | None = None
|
|
242
|
+
if oauth_state:
|
|
243
|
+
for user, data in _keycloak_registry.get("users").items():
|
|
244
|
+
safe_cache: FIFOCache = data.get("cache-obj")
|
|
245
|
+
if user == oauth_state:
|
|
246
|
+
if data.get("valid"):
|
|
247
|
+
user_id = user
|
|
248
|
+
user_data = data
|
|
249
|
+
else:
|
|
250
|
+
msg = "Operation timeout"
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
# exchange 'code' for the token
|
|
254
|
+
token: str | None = None
|
|
255
|
+
errors: list[str] = []
|
|
256
|
+
if user_data:
|
|
257
|
+
code: str = request.args.get("code")
|
|
258
|
+
body_data: dict[str, Any] = {
|
|
259
|
+
"grant_type": "authorization_code",
|
|
260
|
+
"code": code,
|
|
261
|
+
"redirec_url": _keycloak_registry.get("callback-url"),
|
|
262
|
+
}
|
|
263
|
+
# token = __post_jusbr(user_data=user_data,
|
|
264
|
+
# body_data=body_data,
|
|
265
|
+
# errors=errors,
|
|
266
|
+
# logger=_logger)
|
|
267
|
+
# retrieve the token's claims
|
|
268
|
+
if not errors:
|
|
269
|
+
public_key: bytes = _get_public_key(registry=_keycloak_registry,
|
|
270
|
+
url=_keycloak_registry["base-url"],
|
|
271
|
+
logger=_logger)
|
|
272
|
+
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
273
|
+
issuer=_keycloak_registry["base-url"],
|
|
274
|
+
public_key=public_key,
|
|
275
|
+
errors=errors,
|
|
276
|
+
logger=_logger)
|
|
277
|
+
if not errors:
|
|
278
|
+
token_user: str = token_claims["payload"].get("preferred_username")
|
|
279
|
+
if user_id == oauth_state:
|
|
280
|
+
user_id = token_user
|
|
281
|
+
_keycloak_registry["users"][user_id] = _keycloak_registry["users"].pop(oauth_state)
|
|
282
|
+
elif token_user != user_id:
|
|
283
|
+
errors.append(f"Token was issued to user '{token_user}'")
|
|
284
|
+
else:
|
|
285
|
+
msg: str = "Unknown OAuth2 code received"
|
|
286
|
+
if _get_login_timeout(registry=_keycloak_registry):
|
|
287
|
+
msg += " - possible operation timeout"
|
|
288
|
+
errors.append(msg)
|
|
289
|
+
|
|
290
|
+
result: Response
|
|
291
|
+
if errors:
|
|
292
|
+
result = jsonify({"errors": "; ".join(errors)})
|
|
293
|
+
result.status_code = 400
|
|
294
|
+
else:
|
|
295
|
+
result = jsonify({
|
|
296
|
+
"user_id": user_id,
|
|
297
|
+
"access_token": token})
|
|
298
|
+
|
|
299
|
+
# log the response
|
|
300
|
+
if _logger:
|
|
301
|
+
_logger.debug(msg=f"Response {result}")
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
|
|
307
|
+
# methods=["GET"])
|
|
308
|
+
def service_token() -> Response:
|
|
309
|
+
"""
|
|
310
|
+
Entry point for retrieving the Keycloak token.
|
|
311
|
+
|
|
312
|
+
:return: the response containing the token, or *UNAUTHORIZED*
|
|
313
|
+
"""
|
|
314
|
+
# log the request
|
|
315
|
+
if _logger:
|
|
316
|
+
msg: str = _log_init(request=request)
|
|
317
|
+
_logger.debug(msg=msg)
|
|
318
|
+
|
|
319
|
+
# retrieve the user id
|
|
320
|
+
input_params: dict[str, Any] = request.args
|
|
321
|
+
_user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
322
|
+
return Response()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def keycloak_get_token(user_id: str,
|
|
326
|
+
errors: list[str] = None,
|
|
327
|
+
logger: Logger = None) -> str:
|
|
328
|
+
"""
|
|
329
|
+
Retrieve the authentication token for user *user_id*.
|
|
330
|
+
|
|
331
|
+
:param user_id: the user's identification
|
|
332
|
+
:param errors: incidental error messages
|
|
333
|
+
:param logger: optional logger
|
|
334
|
+
:return: the token for *user_id*, or *None* if error
|
|
335
|
+
"""
|
|
336
|
+
global _keycloak_registry
|
|
337
|
+
|
|
338
|
+
# initialize the return variable
|
|
339
|
+
result: str | None = None
|
|
340
|
+
return result
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
# import requests
|
|
2
|
-
# import secrets
|
|
3
|
-
# import string
|
|
4
|
-
# import sys
|
|
5
|
-
# from cachetools import Cache, FIFOCache, TTLCache
|
|
6
|
-
# from datetime import datetime
|
|
7
|
-
from flask import Flask, Response, redirect, request, jsonify
|
|
8
|
-
from logging import Logger
|
|
9
|
-
from pypomes_core import (
|
|
10
|
-
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str, exc_format
|
|
11
|
-
)
|
|
12
|
-
from typing import Any, Final
|
|
13
|
-
|
|
14
|
-
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
15
|
-
KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
|
|
16
|
-
KEYCLOAK_CLIENT_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_TIMEOUT")
|
|
17
|
-
|
|
18
|
-
KEYCLOAK_ENDPOINT_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_CALLBACK",
|
|
19
|
-
def_value="/iam/keycloak:callback")
|
|
20
|
-
KEYCLOAK_ENDPOINT_LOGIN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGIN",
|
|
21
|
-
def_value="/iam/keycloak:login")
|
|
22
|
-
KEYCLOAK_ENDPOINT_LOGOUT: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGOUT",
|
|
23
|
-
def_value="/iam/keycloak:logout")
|
|
24
|
-
KEYCLOAK_ENDPOINT_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_TOKEN",
|
|
25
|
-
def_value="/iam/keycloak:get-token")
|
|
26
|
-
|
|
27
|
-
KEYCLOAK_REALM: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_REALM")
|
|
28
|
-
KEYCLOAK_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_BASE")
|
|
29
|
-
KEYCLOAK_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_CALLBACK")
|
|
30
|
-
|
|
31
|
-
# registry structure:
|
|
32
|
-
# {
|
|
33
|
-
# "client-id": <str>,
|
|
34
|
-
# "client-secret": <str>,
|
|
35
|
-
# "client-timeout": <int>,
|
|
36
|
-
# "realm": <str>,
|
|
37
|
-
# "auth-url": <str>,
|
|
38
|
-
# "callback-url": <str>,
|
|
39
|
-
# "users": {
|
|
40
|
-
# "<user-id>": {
|
|
41
|
-
# "cache-obj": <Cache>,
|
|
42
|
-
# "oauth-scope": <str>,
|
|
43
|
-
# "access-expiration": <timestamp>,
|
|
44
|
-
# data in <Cache>:
|
|
45
|
-
# "oauth-state": <str>
|
|
46
|
-
# "access-token": <str>
|
|
47
|
-
# "refresh-token": <str>
|
|
48
|
-
# }
|
|
49
|
-
# }
|
|
50
|
-
# }
|
|
51
|
-
_keycloak_registry: dict[str, Any] = {
|
|
52
|
-
"client-id": None,
|
|
53
|
-
"client-secret": None,
|
|
54
|
-
"client-timeout": None,
|
|
55
|
-
"realm": None,
|
|
56
|
-
"auth-url": None,
|
|
57
|
-
"callback-url": None,
|
|
58
|
-
"users": {}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# dafault logger
|
|
62
|
-
_logger: Logger | None = None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def keycloak_setup(flask_app: Flask,
|
|
66
|
-
client_id: str = KEYCLOAK_CLIENT_ID,
|
|
67
|
-
client_secret: str = KEYCLOAK_CLIENT_SECRET,
|
|
68
|
-
client_timeout: int = KEYCLOAK_CLIENT_TIMEOUT,
|
|
69
|
-
realm: str = KEYCLOAK_REALM,
|
|
70
|
-
callback_endpoint: str = KEYCLOAK_ENDPOINT_CALLBACK,
|
|
71
|
-
token_endpoint: str = KEYCLOAK_ENDPOINT_TOKEN,
|
|
72
|
-
login_endpoint: str = KEYCLOAK_ENDPOINT_LOGIN,
|
|
73
|
-
logout_endpoint: str = KEYCLOAK_ENDPOINT_LOGOUT,
|
|
74
|
-
auth_url: str = KEYCLOAK_URL_AUTH_BASE,
|
|
75
|
-
callback_url: str = KEYCLOAK_URL_AUTH_CALLBACK,
|
|
76
|
-
logger: Logger = None) -> None:
|
|
77
|
-
"""
|
|
78
|
-
Configure the Keycloak IAM.
|
|
79
|
-
|
|
80
|
-
This should be invoked only once, before the first access to a Keycloak service.
|
|
81
|
-
|
|
82
|
-
:param flask_app: the Flask application
|
|
83
|
-
:param client_id: the client's identification with JusBR
|
|
84
|
-
:param client_secret: the client's password with JusBR
|
|
85
|
-
:param client_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
86
|
-
:param realm: the Keycloak reals
|
|
87
|
-
:param callback_endpoint: endpoint for the callback from JusBR
|
|
88
|
-
:param token_endpoint: endpoint for retrieving the JusBR authentication token
|
|
89
|
-
:param login_endpoint: endpoint for redirecting user to JusBR login page
|
|
90
|
-
:param logout_endpoint: endpoint for terminating user access to JusBR
|
|
91
|
-
:param auth_url: base URL to request the JusBR services
|
|
92
|
-
:param callback_url: URL for Keycloak to callback on login
|
|
93
|
-
:param logger: optional logger
|
|
94
|
-
"""
|
|
95
|
-
global _keycloak_registry
|
|
96
|
-
|
|
97
|
-
# establish the logger
|
|
98
|
-
global _logger
|
|
99
|
-
_logger = logger
|
|
100
|
-
|
|
101
|
-
# configure the JusBR registry
|
|
102
|
-
_keycloak_registry.update({
|
|
103
|
-
"client-id": client_id,
|
|
104
|
-
"client-secret": client_secret,
|
|
105
|
-
"client-timeout": client_timeout,
|
|
106
|
-
"realm": realm,
|
|
107
|
-
"auth-url": auth_url,
|
|
108
|
-
"callback-url": callback_url,
|
|
109
|
-
"users": []
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
# establish the endpoints
|
|
113
|
-
if token_endpoint:
|
|
114
|
-
flask_app.add_url_rule(rule=token_endpoint,
|
|
115
|
-
endpoint="keycloak-token",
|
|
116
|
-
view_func=service_token,
|
|
117
|
-
methods=["GET"])
|
|
118
|
-
if login_endpoint:
|
|
119
|
-
flask_app.add_url_rule(rule=login_endpoint,
|
|
120
|
-
endpoint="keycloak-login",
|
|
121
|
-
view_func=service_login,
|
|
122
|
-
methods=["GET"])
|
|
123
|
-
if logout_endpoint:
|
|
124
|
-
flask_app.add_url_rule(rule=logout_endpoint,
|
|
125
|
-
endpoint="keycloak-logout",
|
|
126
|
-
view_func=service_logout,
|
|
127
|
-
methods=["GET"])
|
|
128
|
-
if callback_endpoint:
|
|
129
|
-
flask_app.add_url_rule(rule=callback_endpoint,
|
|
130
|
-
endpoint="keycloak-callback",
|
|
131
|
-
view_func=service_callback,
|
|
132
|
-
methods=["POST"])
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:login
|
|
136
|
-
# methods=["GET"])
|
|
137
|
-
def service_login() -> Response:
|
|
138
|
-
"""
|
|
139
|
-
Entry point for the Keycloak login service.
|
|
140
|
-
|
|
141
|
-
Redirect the request to the Keycloak authentication page, with the appropriate parameters.
|
|
142
|
-
|
|
143
|
-
:return: the response from the redirect operation
|
|
144
|
-
"""
|
|
145
|
-
global _keycloak_registry
|
|
146
|
-
|
|
147
|
-
# retrieve user id
|
|
148
|
-
input_params: dict[str, Any] = request.args
|
|
149
|
-
_user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
150
|
-
return Response()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:logout
|
|
154
|
-
# methods=["GET"])
|
|
155
|
-
def service_logout() -> Response:
|
|
156
|
-
"""
|
|
157
|
-
Entry point for the JusBR logout service.
|
|
158
|
-
|
|
159
|
-
Remove all data associating the user with JusBR from the registry.
|
|
160
|
-
|
|
161
|
-
:return: response *OK*
|
|
162
|
-
"""
|
|
163
|
-
global _keycloak_registry
|
|
164
|
-
|
|
165
|
-
# retrieve user id
|
|
166
|
-
input_params: dict[str, Any] = request.args
|
|
167
|
-
_user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
168
|
-
return Response()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_CALLBACK_ENDPOINT: /iam/keycloak:callback
|
|
172
|
-
# methods=["POST"])
|
|
173
|
-
def service_callback() -> Response:
|
|
174
|
-
"""
|
|
175
|
-
Entry point for the callback from Keycloak on authentication operation.
|
|
176
|
-
|
|
177
|
-
:return: the response containing the token, or *NOT AUTHORIZED*
|
|
178
|
-
"""
|
|
179
|
-
global _keycloak_registry
|
|
180
|
-
return Response()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
|
|
184
|
-
# methods=["GET"])
|
|
185
|
-
def service_token() -> Response:
|
|
186
|
-
"""
|
|
187
|
-
Entry point for retrieving the Keycloak token.
|
|
188
|
-
|
|
189
|
-
:return: the response containing the token, or *UNAUTHORIZED*
|
|
190
|
-
"""
|
|
191
|
-
# retrieve user id
|
|
192
|
-
input_params: dict[str, Any] = request.args
|
|
193
|
-
_user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
194
|
-
return Response()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def keycloak_get_token(user_id: str,
|
|
198
|
-
errors: list[str] = None,
|
|
199
|
-
logger: Logger = None) -> str:
|
|
200
|
-
"""
|
|
201
|
-
Retrieve the authentication token for user *user_id*.
|
|
202
|
-
|
|
203
|
-
:param user_id: the user's identification
|
|
204
|
-
:param errors: incidental error messages
|
|
205
|
-
:param logger: optional logger
|
|
206
|
-
:return: the token for *user_id*, or *None* if error
|
|
207
|
-
"""
|
|
208
|
-
global _keycloak_registry
|
|
209
|
-
|
|
210
|
-
# initialize the return variable
|
|
211
|
-
result: str | None = None
|
|
212
|
-
return result
|
|
213
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|