pypomes-iam 0.1.7__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.7 → pypomes_iam-0.1.8}/PKG-INFO +1 -1
- {pypomes_iam-0.1.7 → 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.7 → pypomes_iam-0.1.8}/src/pypomes_iam/jusbr_pomes.py +53 -118
- pypomes_iam-0.1.8/src/pypomes_iam/keycloak_pomes.py +340 -0
- pypomes_iam-0.1.7/src/pypomes_iam/keycloak_pomes.py +0 -213
- {pypomes_iam-0.1.7 → pypomes_iam-0.1.8}/.gitignore +0 -0
- {pypomes_iam-0.1.7 → pypomes_iam-0.1.8}/LICENSE +0 -0
- {pypomes_iam-0.1.7 → pypomes_iam-0.1.8}/README.md +0 -0
- {pypomes_iam-0.1.7 → pypomes_iam-0.1.8}/src/pypomes_iam/__init__.py +0 -0
- {pypomes_iam-0.1.7 → pypomes_iam-0.1.8}/src/pypomes_iam/provider_pomes.py +0 -0
- {pypomes_iam-0.1.7 → 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}"
|
|
@@ -5,13 +5,17 @@ import string
|
|
|
5
5
|
import sys
|
|
6
6
|
from cachetools import Cache, FIFOCache, TTLCache
|
|
7
7
|
from datetime import datetime
|
|
8
|
-
from flask import Flask,
|
|
8
|
+
from flask import Flask, Response, redirect, request, jsonify
|
|
9
9
|
from logging import Logger
|
|
10
10
|
from pypomes_core import (
|
|
11
11
|
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str, exc_format
|
|
12
12
|
)
|
|
13
13
|
from typing import Any, Final
|
|
14
14
|
|
|
15
|
+
from .common_pomes import (
|
|
16
|
+
_get_login_timeout, _get_public_key, _get_user_data, _user_logout, _log_init
|
|
17
|
+
)
|
|
18
|
+
|
|
15
19
|
JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
|
|
16
20
|
JUSBR_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_SECRET")
|
|
17
21
|
JUSBR_CLIENT_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JUSBR_CLIENT_TIMEOUT")
|
|
@@ -35,17 +39,19 @@ JUSBR_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_A
|
|
|
35
39
|
# "client-id": <str>,
|
|
36
40
|
# "client-secret": <str>,
|
|
37
41
|
# "client-timeout": <int>,
|
|
38
|
-
# "public_key": <
|
|
42
|
+
# "public_key": <bytes>,
|
|
43
|
+
# "key-lifetime": <int>,
|
|
39
44
|
# "key-expiration": <int>,
|
|
40
|
-
# "
|
|
45
|
+
# "base-url": <str>,
|
|
41
46
|
# "callback-url": <str>,
|
|
42
47
|
# "users": {
|
|
43
48
|
# "<user-id>": {
|
|
44
49
|
# "cache-obj": <Cache>,
|
|
45
50
|
# "oauth-scope": <str>,
|
|
46
51
|
# "access-expiration": <timestamp>,
|
|
52
|
+
# "login-expiration": <int>, <-- transient
|
|
53
|
+
# "login-id": <str>, <-- transient
|
|
47
54
|
# data in <Cache>:
|
|
48
|
-
# "oauth-state": <str>
|
|
49
55
|
# "access-token": <str>
|
|
50
56
|
# "refresh-token": <str>
|
|
51
57
|
# }
|
|
@@ -66,7 +72,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
66
72
|
token_endpoint: str = JUSBR_ENDPOINT_TOKEN,
|
|
67
73
|
login_endpoint: str = JUSBR_ENDPOINT_LOGIN,
|
|
68
74
|
logout_endpoint: str = JUSBR_ENDPOINT_LOGOUT,
|
|
69
|
-
|
|
75
|
+
base_url: str = JUSBR_URL_AUTH_BASE,
|
|
70
76
|
callback_url: str = JUSBR_URL_AUTH_CALLBACK,
|
|
71
77
|
logger: Logger = None) -> None:
|
|
72
78
|
"""
|
|
@@ -83,7 +89,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
83
89
|
:param token_endpoint: endpoint for retrieving the JusBR authentication token
|
|
84
90
|
:param login_endpoint: endpoint for redirecting user to JusBR login page
|
|
85
91
|
:param logout_endpoint: endpoint for terminating user access to JusBR
|
|
86
|
-
:param
|
|
92
|
+
:param base_url: base URL to request the JusBR services
|
|
87
93
|
:param callback_url: URL for JusBR to callback on login
|
|
88
94
|
:param logger: optional logger
|
|
89
95
|
"""
|
|
@@ -97,7 +103,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
97
103
|
"client-id": client_id,
|
|
98
104
|
"client-secret": client_secret,
|
|
99
105
|
"client-timeout": client_timeout,
|
|
100
|
-
"
|
|
106
|
+
"base-url": base_url,
|
|
101
107
|
"callback-url": callback_url,
|
|
102
108
|
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
103
109
|
"key-lifetime": public_key_lifetime,
|
|
@@ -141,27 +147,28 @@ def service_login() -> Response:
|
|
|
141
147
|
|
|
142
148
|
# log the request
|
|
143
149
|
if _logger:
|
|
144
|
-
msg: str =
|
|
150
|
+
msg: str = _log_init(request=request)
|
|
145
151
|
_logger.debug(msg=msg)
|
|
146
152
|
|
|
147
|
-
# retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state'
|
|
153
|
+
# retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state')
|
|
148
154
|
input_params: dict[str, Any] = request.values
|
|
149
155
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
150
156
|
user_id: str = input_params.get("user-id") or input_params.get("login") or oauth_state
|
|
151
|
-
# obtain user data
|
|
152
|
-
user_data: dict[str, Any] =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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)
|
|
156
163
|
safe_cache: Cache
|
|
157
164
|
if timeout:
|
|
158
165
|
safe_cache = TTLCache(maxsize=16,
|
|
159
|
-
ttl=
|
|
166
|
+
ttl=timeout)
|
|
160
167
|
else:
|
|
161
168
|
safe_cache = FIFOCache(maxsize=16)
|
|
162
169
|
safe_cache["oauth-state"] = oauth_state
|
|
163
170
|
user_data["cache-obj"] = safe_cache
|
|
164
|
-
auth_url: str = (f"{_jusbr_registry["
|
|
171
|
+
auth_url: str = (f"{_jusbr_registry["base-url"]}/protocol/openid-connect/auth?response_type=code"
|
|
165
172
|
f"&client_id={_jusbr_registry["client-id"]}"
|
|
166
173
|
f"&redirect_uri={_jusbr_registry["callback-url"]}"
|
|
167
174
|
f"&state={oauth_state}")
|
|
@@ -192,18 +199,17 @@ def service_logout() -> Response:
|
|
|
192
199
|
|
|
193
200
|
# log the request
|
|
194
201
|
if _logger:
|
|
195
|
-
msg: str =
|
|
202
|
+
msg: str = _log_init(request=request)
|
|
196
203
|
_logger.debug(msg=msg)
|
|
197
204
|
|
|
198
|
-
# retrieve user id
|
|
205
|
+
# retrieve the user id
|
|
199
206
|
input_params: dict[str, Any] = request.args
|
|
200
207
|
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
201
208
|
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
_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)
|
|
207
213
|
|
|
208
214
|
result: Response = Response(status=200)
|
|
209
215
|
|
|
@@ -227,7 +233,7 @@ def service_callback() -> Response:
|
|
|
227
233
|
|
|
228
234
|
# log the request
|
|
229
235
|
if _logger:
|
|
230
|
-
msg: str =
|
|
236
|
+
msg: str = _log_init(request=request)
|
|
231
237
|
_logger.debug(msg=msg)
|
|
232
238
|
|
|
233
239
|
# validate the OAuth2 state
|
|
@@ -253,7 +259,7 @@ def service_callback() -> Response:
|
|
|
253
259
|
body_data: dict[str, Any] = {
|
|
254
260
|
"grant_type": "authorization_code",
|
|
255
261
|
"code": code,
|
|
256
|
-
"
|
|
262
|
+
"redirect_uri": _jusbr_registry.get("callback-url"),
|
|
257
263
|
}
|
|
258
264
|
token = __post_jusbr(user_data=user_data,
|
|
259
265
|
body_data=body_data,
|
|
@@ -261,9 +267,12 @@ def service_callback() -> Response:
|
|
|
261
267
|
logger=_logger)
|
|
262
268
|
# retrieve the token's claims
|
|
263
269
|
if not errors:
|
|
270
|
+
public_key: bytes = _get_public_key(registry=_jusbr_registry,
|
|
271
|
+
url=_jusbr_registry["base-url"],
|
|
272
|
+
logger=_logger)
|
|
264
273
|
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
265
|
-
issuer=_jusbr_registry
|
|
266
|
-
public_key=
|
|
274
|
+
issuer=_jusbr_registry["base-url"],
|
|
275
|
+
public_key=public_key,
|
|
267
276
|
errors=errors,
|
|
268
277
|
logger=_logger)
|
|
269
278
|
if not errors:
|
|
@@ -275,7 +284,7 @@ def service_callback() -> Response:
|
|
|
275
284
|
errors.append(f"Token was issued to user '{token_user}'")
|
|
276
285
|
else:
|
|
277
286
|
msg: str = "Unknown OAuth2 code received"
|
|
278
|
-
if
|
|
287
|
+
if _get_login_timeout(registry=_jusbr_registry):
|
|
279
288
|
msg += " - possible operation timeout"
|
|
280
289
|
errors.append(msg)
|
|
281
290
|
|
|
@@ -305,7 +314,7 @@ def service_token() -> Response:
|
|
|
305
314
|
"""
|
|
306
315
|
# log the request
|
|
307
316
|
if _logger:
|
|
308
|
-
msg: str =
|
|
317
|
+
msg: str = _log_init(request=request)
|
|
309
318
|
_logger.debug(msg=msg)
|
|
310
319
|
|
|
311
320
|
# retrieve the token
|
|
@@ -344,8 +353,9 @@ def jusbr_get_token(user_id: str,
|
|
|
344
353
|
# initialize the return variable
|
|
345
354
|
result: str | None = None
|
|
346
355
|
|
|
347
|
-
user_data: dict[str, Any] =
|
|
348
|
-
|
|
356
|
+
user_data: dict[str, Any] = _get_user_data(registry=_jusbr_registry,
|
|
357
|
+
user_id=user_id,
|
|
358
|
+
logger=logger)
|
|
349
359
|
safe_cache: Cache = user_data.get("cache-obj")
|
|
350
360
|
if safe_cache:
|
|
351
361
|
access_expiration: int = user_data.get("access-expiration")
|
|
@@ -389,83 +399,13 @@ def jusbr_set_scope(user_id: str,
|
|
|
389
399
|
global _jusbr_registry
|
|
390
400
|
|
|
391
401
|
# retrieve user data
|
|
392
|
-
user_data: dict[str, Any] =
|
|
393
|
-
|
|
402
|
+
user_data: dict[str, Any] = _get_user_data(registry=_jusbr_registry,
|
|
403
|
+
user_id=user_id,
|
|
404
|
+
logger=logger)
|
|
394
405
|
# set the OAuth2 scope
|
|
395
406
|
user_data["oauth-scope"] = scope
|
|
396
407
|
if logger:
|
|
397
|
-
logger.debug(f"Scope for user '{user_id}' set to '{scope}'")
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def __get_public_key(url: str,
|
|
401
|
-
logger: Logger | None) -> str:
|
|
402
|
-
"""
|
|
403
|
-
Obtain the public key used by JusBR to sign the authentication tokens.
|
|
404
|
-
|
|
405
|
-
:param url: the base URL to request the public key
|
|
406
|
-
:return: the public key, in *PEM* format
|
|
407
|
-
"""
|
|
408
|
-
from pypomes_crypto import crypto_jwk_convert
|
|
409
|
-
global _jusbr_registry
|
|
410
|
-
|
|
411
|
-
# initialize the return variable
|
|
412
|
-
result: str | None = None
|
|
413
|
-
|
|
414
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
415
|
-
if now > _jusbr_registry.get("key-expiration"):
|
|
416
|
-
# obtain a new public key
|
|
417
|
-
url: str = f"{url}/protocol/openid-connect/certs"
|
|
418
|
-
response: requests.Response = requests.get(url=url)
|
|
419
|
-
if response.status_code == 200:
|
|
420
|
-
# request succeeded
|
|
421
|
-
reply: dict[str, Any] = response.json()
|
|
422
|
-
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
423
|
-
fmt="PEM")
|
|
424
|
-
_jusbr_registry["public-key"] = result
|
|
425
|
-
duration: int = _jusbr_registry.get("key-lifetime") or 0
|
|
426
|
-
_jusbr_registry["key-expiration"] = now + duration
|
|
427
|
-
elif logger:
|
|
428
|
-
logger.error(msg=f"GET '{url}': failed, "
|
|
429
|
-
f"status {response.status_code}, reason '{response.reason}'")
|
|
430
|
-
else:
|
|
431
|
-
result = _jusbr_registry.get("public-key")
|
|
432
|
-
|
|
433
|
-
return result
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def __get_login_timeout() -> int | None:
|
|
437
|
-
"""
|
|
438
|
-
Retrieve the timeout currently applicable for the login operation.
|
|
439
|
-
|
|
440
|
-
:return: the current login timeout, or *None* if none has been set.
|
|
441
|
-
"""
|
|
442
|
-
global _jusbr_registry
|
|
443
|
-
|
|
444
|
-
timeout: int = _jusbr_registry.get("client-timeout")
|
|
445
|
-
return timeout if isinstance(timeout, int) and timeout > 0 else None
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
def __get_user_data(user_id: str,
|
|
449
|
-
logger: Logger | None) -> dict[str, Any]:
|
|
450
|
-
"""
|
|
451
|
-
Retrieve the data for *user_id* from the registry.
|
|
452
|
-
|
|
453
|
-
If an entry is not found for *user_id* in the registry, it is created.
|
|
454
|
-
It will remain there until the user is logged out.
|
|
455
|
-
|
|
456
|
-
:param user_id:
|
|
457
|
-
:return: the data for *user_id* in the registry
|
|
458
|
-
"""
|
|
459
|
-
global _jusbr_registry
|
|
460
|
-
|
|
461
|
-
result: dict[str, Any] = _jusbr_registry["users"].get(user_id)
|
|
462
|
-
if not result:
|
|
463
|
-
result = {"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())}
|
|
464
|
-
_jusbr_registry["users"][user_id] = result
|
|
465
|
-
if logger:
|
|
466
|
-
logger.debug(f"Entry for user '{user_id}' added to registry")
|
|
467
|
-
|
|
468
|
-
return result
|
|
408
|
+
logger.debug(msg=f"Scope for user '{user_id}' set to '{scope}'")
|
|
469
409
|
|
|
470
410
|
|
|
471
411
|
def __post_jusbr(user_data: dict[str, Any],
|
|
@@ -475,7 +415,7 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
475
415
|
"""
|
|
476
416
|
Send a POST request to JusBR to obtain the authentication token data, and return the access token.
|
|
477
417
|
|
|
478
|
-
For
|
|
418
|
+
For token exchange, *body_data* will have the attributes
|
|
479
419
|
- "grant_type": "authorization_code"
|
|
480
420
|
- "code": <16-character-random-code>
|
|
481
421
|
- "redirect_uri": <callback-url>
|
|
@@ -506,8 +446,11 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
506
446
|
# obtain the token
|
|
507
447
|
err_msg: str | None = None
|
|
508
448
|
safe_cache: Cache = user_data.get("cache-obj")
|
|
509
|
-
url: str = _jusbr_registry.get("
|
|
449
|
+
url: str = _jusbr_registry.get("base-url") + "/protocol/openid-connect/token"
|
|
510
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)}")
|
|
511
454
|
try:
|
|
512
455
|
# JusBR return on a token request:
|
|
513
456
|
# {
|
|
@@ -520,6 +463,8 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
520
463
|
data=body_data)
|
|
521
464
|
if response.status_code == 200:
|
|
522
465
|
# request succeeded
|
|
466
|
+
if logger:
|
|
467
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
523
468
|
reply: dict[str, Any] = response.json()
|
|
524
469
|
result = reply.get("access_token")
|
|
525
470
|
safe_cache: Cache = FIFOCache(maxsize=1024)
|
|
@@ -528,12 +473,9 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
528
473
|
safe_cache["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
529
474
|
user_data["cache-obj"] = safe_cache
|
|
530
475
|
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
531
|
-
if logger:
|
|
532
|
-
logger.debug(msg=f"POST '{url}': status {response.status_code}")
|
|
533
476
|
else:
|
|
534
477
|
# request resulted in error
|
|
535
|
-
err_msg =
|
|
536
|
-
f"status {response.status_code}, reason '{response.reason}'")
|
|
478
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
537
479
|
if hasattr(response, "content") and response.content:
|
|
538
480
|
err_msg += f", content '{response.content}'"
|
|
539
481
|
if response.status_code == 401 and "refresh_token" in body_data:
|
|
@@ -552,10 +494,3 @@ def __post_jusbr(user_data: dict[str, Any],
|
|
|
552
494
|
logger.error(msg=err_msg)
|
|
553
495
|
|
|
554
496
|
return result
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def __log_init(request: Request) -> str:
|
|
558
|
-
|
|
559
|
-
params: str = json.dumps(obj=request.args,
|
|
560
|
-
ensure_ascii=False)
|
|
561
|
-
return f"Request {request.method}:{request.path}, params {params}"
|
|
@@ -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
|