pypomes-iam 0.1.8__py3-none-any.whl → 0.1.9__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 +5 -0
- pypomes_iam/common_pomes.py +331 -19
- pypomes_iam/jusbr_pomes.py +52 -243
- pypomes_iam/keycloak_pomes.py +83 -114
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.1.9.dist-info}/METADATA +2 -2
- pypomes_iam-0.1.9.dist-info/RECORD +10 -0
- pypomes_iam-0.1.8.dist-info/RECORD +0 -10
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.1.9.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.1.9.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from .jusbr_pomes import (
|
|
2
2
|
jusbr_setup, jusbr_get_token, jusbr_set_scope
|
|
3
3
|
)
|
|
4
|
+
from .keycloak_pomes import (
|
|
5
|
+
keycloak_setup, keycloak_get_token, keycloak_set_scope
|
|
6
|
+
)
|
|
4
7
|
from .provider_pomes import (
|
|
5
8
|
provider_register, provider_get_token
|
|
6
9
|
)
|
|
@@ -11,6 +14,8 @@ from .token_pomes import (
|
|
|
11
14
|
__all__ = [
|
|
12
15
|
# jusbr_pomes
|
|
13
16
|
"jusbr_setup", "jusbr_get_token", "jusbr_set_scope",
|
|
17
|
+
# keycloak_pomes
|
|
18
|
+
"keycloak_setup", "keycloak_get_token", "keycloak_set_scope",
|
|
14
19
|
# provider_pomes
|
|
15
20
|
"provider_register", "provider_get_token",
|
|
16
21
|
# token_pomes
|
pypomes_iam/common_pomes.py
CHANGED
|
@@ -1,21 +1,233 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import requests
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import sys
|
|
6
|
+
from cachetools import Cache
|
|
3
7
|
from datetime import datetime
|
|
4
8
|
from flask import Request
|
|
5
9
|
from logging import Logger
|
|
6
|
-
from pypomes_core import TZ_LOCAL
|
|
10
|
+
from pypomes_core import TZ_LOCAL, exc_format
|
|
7
11
|
from typing import Any
|
|
8
12
|
|
|
13
|
+
# registry structure:
|
|
14
|
+
# {
|
|
15
|
+
# "client-id": <str>,
|
|
16
|
+
# "client-secret": <str>,
|
|
17
|
+
# "client-timeout": <int>,
|
|
18
|
+
# "public_key": <str>,
|
|
19
|
+
# "key-lifetime": <int>,
|
|
20
|
+
# "key-expiration": <int>,
|
|
21
|
+
# "base-url": <str>,
|
|
22
|
+
# "callback-url": <str>,
|
|
23
|
+
# "safe-cache": <FIFOCache>
|
|
24
|
+
# }
|
|
25
|
+
# data in "safe-cache":
|
|
26
|
+
# {
|
|
27
|
+
# "users": {
|
|
28
|
+
# "<user-id>": {
|
|
29
|
+
# "access-token": <str>
|
|
30
|
+
# "refresh-token": <str>
|
|
31
|
+
# "access-expiration": <timestamp>,
|
|
32
|
+
# "login-expiration": <timestamp>, <-- transient
|
|
33
|
+
# "login-id": <str>, <-- transient
|
|
34
|
+
# "oauth-scope": <str> <-- optional
|
|
35
|
+
# }
|
|
36
|
+
# }
|
|
37
|
+
# }
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _service_callback(registry: dict[str, Any],
|
|
41
|
+
args: dict[str, Any],
|
|
42
|
+
errors: list[str],
|
|
43
|
+
logger: Logger | None) -> tuple[str, str]:
|
|
44
|
+
"""
|
|
45
|
+
Entry point for the callback from JusBR on authentication operation.
|
|
46
|
+
|
|
47
|
+
:param registry: the registry holding the authentication data
|
|
48
|
+
:param args: the arguments passed when requesting the service
|
|
49
|
+
:param errors: incidental errors
|
|
50
|
+
:param logger: optional logger
|
|
51
|
+
"""
|
|
52
|
+
from .token_pomes import token_validate
|
|
53
|
+
|
|
54
|
+
# initialize the return variable
|
|
55
|
+
result: tuple[str, str] | None = None
|
|
56
|
+
|
|
57
|
+
# retrieve the users authentication data
|
|
58
|
+
cache: Cache = registry["safe-cache"]
|
|
59
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
60
|
+
|
|
61
|
+
# validate the OAuth2 state
|
|
62
|
+
oauth_state: str = args.get("state")
|
|
63
|
+
user_data: dict[str, Any] | None = None
|
|
64
|
+
if oauth_state:
|
|
65
|
+
for user, data in users.items():
|
|
66
|
+
if user == oauth_state:
|
|
67
|
+
user_data = data
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
# exchange 'code' for the token
|
|
71
|
+
if user_data:
|
|
72
|
+
users.pop(oauth_state)
|
|
73
|
+
code: str = args.get("code")
|
|
74
|
+
body_data: dict[str, Any] = {
|
|
75
|
+
"grant_type": "authorization_code",
|
|
76
|
+
"code": code,
|
|
77
|
+
"redirect_uri": registry.get("callback-url"),
|
|
78
|
+
}
|
|
79
|
+
token = _post_for_token(registry=registry,
|
|
80
|
+
user_data=user_data,
|
|
81
|
+
body_data=body_data,
|
|
82
|
+
errors=errors,
|
|
83
|
+
logger=logger)
|
|
84
|
+
# retrieve the token's claims
|
|
85
|
+
if not errors:
|
|
86
|
+
public_key: bytes = _get_public_key(registry=registry,
|
|
87
|
+
logger=logger)
|
|
88
|
+
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
89
|
+
issuer=registry["base-url"],
|
|
90
|
+
public_key=public_key,
|
|
91
|
+
errors=errors,
|
|
92
|
+
logger=logger)
|
|
93
|
+
if not errors:
|
|
94
|
+
token_user: str = token_claims["payload"].get("preferred_username")
|
|
95
|
+
if token_user == oauth_state:
|
|
96
|
+
users[token_user] = user_data
|
|
97
|
+
result = (token_user, token)
|
|
98
|
+
else:
|
|
99
|
+
errors.append(f"Token was issued to user '{token_user}'")
|
|
100
|
+
else:
|
|
101
|
+
msg: str = "Unknown OAuth2 code received"
|
|
102
|
+
if _get_login_timeout(registry=registry):
|
|
103
|
+
msg += " - possible operation timeout"
|
|
104
|
+
errors.append(msg)
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _service_login(registry: dict[str, Any],
|
|
110
|
+
args: dict[str, Any],
|
|
111
|
+
logger: Logger | None) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Build the callback URL for redirecting the request to the IAM's authentication page.
|
|
114
|
+
|
|
115
|
+
:param registry: the registry holding the authentication data
|
|
116
|
+
:param args: the arguments passed when requesting the service
|
|
117
|
+
:param logger: optional logger
|
|
118
|
+
:return: the callback URL, with the appropriate parameters
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# retrieve user data
|
|
122
|
+
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
123
|
+
|
|
124
|
+
# build the user data
|
|
125
|
+
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
126
|
+
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
127
|
+
user_id=oauth_state,
|
|
128
|
+
logger=logger)
|
|
129
|
+
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
130
|
+
user_data["login-id"] = user_id
|
|
131
|
+
timeout: int = _get_login_timeout(registry=registry)
|
|
132
|
+
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
|
|
133
|
+
|
|
134
|
+
# build the redirect url
|
|
135
|
+
result: str = (f"{registry["base-url"]}/protocol/openid-connect/auth?response_type=code"
|
|
136
|
+
f"&client_id={registry["client-id"]}"
|
|
137
|
+
f"&redirect_uri={registry["callback-url"]}"
|
|
138
|
+
f"&state={oauth_state}")
|
|
139
|
+
scope: str = _get_user_scope(registry=registry,
|
|
140
|
+
user_id=user_id)
|
|
141
|
+
if scope:
|
|
142
|
+
user_data["oauth-scope"] = scope
|
|
143
|
+
result += f"&scope={scope}"
|
|
144
|
+
|
|
145
|
+
# logout the user
|
|
146
|
+
_service_logout(registry=registry,
|
|
147
|
+
args=args,
|
|
148
|
+
logger=logger)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _service_logout(registry: dict[str, Any],
|
|
153
|
+
args: dict[str, Any],
|
|
154
|
+
logger: Logger | None) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Remove all data associating *user_id* from *registry*.
|
|
157
|
+
|
|
158
|
+
:param registry: the registry holding the authentication data
|
|
159
|
+
:param args: the arguments passed when requesting the service
|
|
160
|
+
:param logger: optional logger
|
|
161
|
+
"""
|
|
162
|
+
# remove the user data
|
|
163
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
164
|
+
if user_id:
|
|
165
|
+
cache: Cache = registry["safe-cache"]
|
|
166
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
167
|
+
if user_id in users:
|
|
168
|
+
users.pop(user_id)
|
|
169
|
+
if logger:
|
|
170
|
+
logger.debug(msg=f"User '{user_id}' removed from the registry")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _service_token(registry: dict[str, Any],
|
|
174
|
+
args: dict[str, Any],
|
|
175
|
+
errors: list[str] = None,
|
|
176
|
+
logger: Logger = None) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Retrieve the authentication token for user *user_id*.
|
|
179
|
+
|
|
180
|
+
:param registry: the registry holding the authentication data
|
|
181
|
+
:param args: the arguments passed when requesting the service
|
|
182
|
+
:param errors: incidental error messages
|
|
183
|
+
:param logger: optional logger
|
|
184
|
+
:return: the token for *user_id*, or *None* if error
|
|
185
|
+
"""
|
|
186
|
+
# initialize the return variable
|
|
187
|
+
result: str | None = None
|
|
188
|
+
|
|
189
|
+
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
190
|
+
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
191
|
+
user_id=user_id,
|
|
192
|
+
logger=logger)
|
|
193
|
+
token: str = user_data["access-token"]
|
|
194
|
+
if token:
|
|
195
|
+
access_expiration: int = user_data.get("access-expiration")
|
|
196
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
197
|
+
if now < access_expiration:
|
|
198
|
+
result = token
|
|
199
|
+
else:
|
|
200
|
+
# access token has expired
|
|
201
|
+
refresh_token: str = user_data["refresh-token"]
|
|
202
|
+
if refresh_token:
|
|
203
|
+
body_data: dict[str, str] = {
|
|
204
|
+
"grant_type": "refresh_token",
|
|
205
|
+
"refresh_token": refresh_token
|
|
206
|
+
}
|
|
207
|
+
result = _post_for_token(registry=registry,
|
|
208
|
+
user_data=user_data,
|
|
209
|
+
body_data=body_data,
|
|
210
|
+
errors=errors,
|
|
211
|
+
logger=logger)
|
|
212
|
+
|
|
213
|
+
elif logger or isinstance(errors, list):
|
|
214
|
+
err_msg: str = f"User '{user_id}' not authenticated"
|
|
215
|
+
if isinstance(errors, list):
|
|
216
|
+
errors.append(err_msg)
|
|
217
|
+
if logger:
|
|
218
|
+
logger.error(msg=err_msg)
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
9
222
|
|
|
10
223
|
def _get_public_key(registry: dict[str, Any],
|
|
11
|
-
url: str,
|
|
12
224
|
logger: Logger | None) -> bytes:
|
|
13
225
|
"""
|
|
14
226
|
Obtain the public key used by the *IAM* to sign the authentication tokens.
|
|
15
227
|
|
|
16
228
|
The public key is saved in *registry*.
|
|
17
229
|
|
|
18
|
-
:param
|
|
230
|
+
:param registry: the registry holding the authentication data
|
|
19
231
|
:return: the public key, in *DER* format
|
|
20
232
|
"""
|
|
21
233
|
from pypomes_crypto import crypto_jwk_convert
|
|
@@ -24,9 +236,9 @@ def _get_public_key(registry: dict[str, Any],
|
|
|
24
236
|
result: bytes | None = None
|
|
25
237
|
|
|
26
238
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
27
|
-
if now > registry
|
|
239
|
+
if now > registry["key-expiration"]:
|
|
28
240
|
# obtain a new public key
|
|
29
|
-
url: str = f"{url}/protocol/openid-connect/certs"
|
|
241
|
+
url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
|
|
30
242
|
if logger:
|
|
31
243
|
logger.debug(msg=f"GET '{url}'")
|
|
32
244
|
response: requests.Response = requests.get(url=url)
|
|
@@ -38,7 +250,7 @@ def _get_public_key(registry: dict[str, Any],
|
|
|
38
250
|
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
39
251
|
fmt="DER")
|
|
40
252
|
registry["public-key"] = result
|
|
41
|
-
duration: int = registry
|
|
253
|
+
duration: int = registry["key-lifetime"] or 0
|
|
42
254
|
registry["key-expiration"] = now + duration
|
|
43
255
|
elif logger:
|
|
44
256
|
msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
|
|
@@ -46,7 +258,7 @@ def _get_public_key(registry: dict[str, Any],
|
|
|
46
258
|
msg += f", content '{response.content}'"
|
|
47
259
|
logger.error(msg=msg)
|
|
48
260
|
else:
|
|
49
|
-
result = registry
|
|
261
|
+
result = registry["public-key"]
|
|
50
262
|
|
|
51
263
|
return result
|
|
52
264
|
|
|
@@ -55,6 +267,7 @@ def _get_login_timeout(registry: dict[str, Any]) -> int | None:
|
|
|
55
267
|
"""
|
|
56
268
|
Retrieve from *registry* the timeout currently applicable for the login operation.
|
|
57
269
|
|
|
270
|
+
:param registry: the registry holding the authentication data
|
|
58
271
|
:return: the current login timeout, or *None* if none has been set.
|
|
59
272
|
"""
|
|
60
273
|
timeout: int = registry.get("client-timeout")
|
|
@@ -70,13 +283,19 @@ def _get_user_data(registry: dict[str, Any],
|
|
|
70
283
|
If an entry is not found for *user_id* in the registry, it is created.
|
|
71
284
|
It will remain there until the user is logged out.
|
|
72
285
|
|
|
73
|
-
:param
|
|
286
|
+
:param registry: the registry holding the authentication data
|
|
74
287
|
:return: the data for *user_id* in the registry
|
|
75
288
|
"""
|
|
76
|
-
|
|
289
|
+
cache: Cache = registry["safe-cache"]
|
|
290
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
291
|
+
result: dict[str, Any] = users.get(user_id)
|
|
77
292
|
if not result:
|
|
78
|
-
result = {
|
|
79
|
-
|
|
293
|
+
result = {
|
|
294
|
+
"access-token": None,
|
|
295
|
+
"refresh-token": None,
|
|
296
|
+
"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
297
|
+
}
|
|
298
|
+
users[user_id] = result
|
|
80
299
|
if logger:
|
|
81
300
|
logger.debug(msg=f"Entry for user '{user_id}' added to the registry")
|
|
82
301
|
elif logger:
|
|
@@ -85,17 +304,110 @@ def _get_user_data(registry: dict[str, Any],
|
|
|
85
304
|
return result
|
|
86
305
|
|
|
87
306
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
logger: Logger | None) -> None:
|
|
307
|
+
def _get_user_scope(registry: dict[str, Any],
|
|
308
|
+
user_id: str) -> str | None:
|
|
91
309
|
"""
|
|
92
|
-
|
|
310
|
+
Retrieve the OAuth2 scope associated with *user_id*.
|
|
311
|
+
|
|
312
|
+
:param registry: the registry holding the authentication data
|
|
313
|
+
:param user_id:
|
|
314
|
+
:return: the OAuth2 scope associated with *user_id*, or *None* if it does not exist
|
|
93
315
|
"""
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
316
|
+
# initialize the return variable
|
|
317
|
+
result: str | None = None
|
|
318
|
+
|
|
319
|
+
if user_id:
|
|
320
|
+
cache: Cache = registry["safe-cache"]
|
|
321
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
322
|
+
if user_id in users:
|
|
323
|
+
result = users[user_id].get("oauth2-scope")
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _post_for_token(registry: dict[str, Any],
|
|
329
|
+
user_data: dict[str, Any],
|
|
330
|
+
body_data: dict[str, Any],
|
|
331
|
+
errors: list[str] | None,
|
|
332
|
+
logger: Logger | None) -> str | None:
|
|
333
|
+
"""
|
|
334
|
+
Send a POST request to obtain the authentication token data, and return the access token.
|
|
335
|
+
|
|
336
|
+
For token exchange, *body_data* will have the attributes
|
|
337
|
+
- "grant_type": "authorization_code"
|
|
338
|
+
- "code": <16-character-random-code>
|
|
339
|
+
- "redirect_uri": <callback-url>
|
|
340
|
+
For token refresh, *body_data* will have the attributes
|
|
341
|
+
- "grant_type": "refresh_token"
|
|
342
|
+
- "refresh_token": <current-refresh-token>
|
|
343
|
+
|
|
344
|
+
If the operation is successful, the token data is stored in the registry.
|
|
345
|
+
Otherwise, *errors* will contain the appropriate error message.
|
|
346
|
+
|
|
347
|
+
:param registry: the registry holding the authentication data
|
|
348
|
+
:param user_data: the user's data in the registry
|
|
349
|
+
:param body_data: the data to send in the body of the request
|
|
350
|
+
:param errors: incidental errors
|
|
351
|
+
:param logger: optional logger
|
|
352
|
+
:return: the access token obtained, or *None* if error
|
|
353
|
+
"""
|
|
354
|
+
# initialize the return variable
|
|
355
|
+
result: str | None = None
|
|
356
|
+
|
|
357
|
+
# complete the data to send in body of request
|
|
358
|
+
body_data["client_id"] = registry["client-id"]
|
|
359
|
+
client_secret: str = registry["client-secret"]
|
|
360
|
+
if client_secret:
|
|
361
|
+
body_data["client_secret"] = client_secret
|
|
362
|
+
|
|
363
|
+
# obtain the token
|
|
364
|
+
err_msg: str | None = None
|
|
365
|
+
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
366
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
367
|
+
if logger:
|
|
368
|
+
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
369
|
+
ensure_ascii=False)}")
|
|
370
|
+
try:
|
|
371
|
+
# typical return on a token request:
|
|
372
|
+
# {
|
|
373
|
+
# "token_type": "Bearer",
|
|
374
|
+
# "access_token": <str>,
|
|
375
|
+
# "expires_in": <number-of-seconds>,
|
|
376
|
+
# "refresh_token": <str>
|
|
377
|
+
# }
|
|
378
|
+
response: requests.Response = requests.post(url=url,
|
|
379
|
+
data=body_data)
|
|
380
|
+
if response.status_code == 200:
|
|
381
|
+
# request succeeded
|
|
382
|
+
if logger:
|
|
383
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
384
|
+
reply: dict[str, Any] = response.json()
|
|
385
|
+
result = reply.get("access_token")
|
|
386
|
+
user_data["access-token"] = result
|
|
387
|
+
# on token refresh, keep current refresh token if a new one is not provided
|
|
388
|
+
user_data["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
389
|
+
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
390
|
+
else:
|
|
391
|
+
# request resulted in error
|
|
392
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
393
|
+
if hasattr(response, "content") and response.content:
|
|
394
|
+
err_msg += f", content '{response.content}'"
|
|
395
|
+
if response.status_code == 400 and body_data.get("grant_type") == "refresh_token":
|
|
396
|
+
# refresh token is no longer valid
|
|
397
|
+
user_data["refresh-token"] = None
|
|
398
|
+
except Exception as e:
|
|
399
|
+
# the operation raised an exception
|
|
400
|
+
err_msg = exc_format(exc=e,
|
|
401
|
+
exc_info=sys.exc_info())
|
|
402
|
+
err_msg = f"POST '{url}': error '{err_msg}'"
|
|
403
|
+
|
|
404
|
+
if err_msg:
|
|
405
|
+
if isinstance(errors, list):
|
|
406
|
+
errors.append(err_msg)
|
|
97
407
|
if logger:
|
|
98
|
-
logger.
|
|
408
|
+
logger.error(msg=err_msg)
|
|
409
|
+
|
|
410
|
+
return result
|
|
99
411
|
|
|
100
412
|
|
|
101
413
|
def _log_init(request: Request) -> str:
|
pypomes_iam/jusbr_pomes.py
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
|
-
import sys
|
|
6
|
-
from cachetools import Cache, FIFOCache, TTLCache
|
|
1
|
+
from cachetools import FIFOCache
|
|
7
2
|
from datetime import datetime
|
|
8
3
|
from flask import Flask, Response, redirect, request, jsonify
|
|
9
4
|
from logging import Logger
|
|
10
5
|
from pypomes_core import (
|
|
11
|
-
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str
|
|
6
|
+
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str
|
|
12
7
|
)
|
|
13
8
|
from typing import Any, Final
|
|
14
9
|
|
|
15
10
|
from .common_pomes import (
|
|
16
|
-
|
|
11
|
+
_service_login, _service_logout,
|
|
12
|
+
_service_callback, _service_token,
|
|
13
|
+
_get_user_data, _log_init
|
|
17
14
|
)
|
|
18
15
|
|
|
19
16
|
JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
|
|
@@ -39,21 +36,23 @@ JUSBR_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_A
|
|
|
39
36
|
# "client-id": <str>,
|
|
40
37
|
# "client-secret": <str>,
|
|
41
38
|
# "client-timeout": <int>,
|
|
42
|
-
# "public_key": <
|
|
39
|
+
# "public_key": <str>,
|
|
43
40
|
# "key-lifetime": <int>,
|
|
44
41
|
# "key-expiration": <int>,
|
|
45
42
|
# "base-url": <str>,
|
|
46
43
|
# "callback-url": <str>,
|
|
44
|
+
# "cache-obj": <FIFOCache>
|
|
45
|
+
# }
|
|
46
|
+
# data in "cache-obj":
|
|
47
|
+
# {
|
|
47
48
|
# "users": {
|
|
48
49
|
# "<user-id>": {
|
|
49
|
-
# "
|
|
50
|
-
# "
|
|
50
|
+
# "access-token": <str>
|
|
51
|
+
# "refresh-token": <str>
|
|
51
52
|
# "access-expiration": <timestamp>,
|
|
52
|
-
# "login-expiration": <
|
|
53
|
-
# "login-id": <str>,
|
|
54
|
-
#
|
|
55
|
-
# "access-token": <str>
|
|
56
|
-
# "refresh-token": <str>
|
|
53
|
+
# "login-expiration": <timestamp>, <-- transient
|
|
54
|
+
# "login-id": <str>, <-- transient
|
|
55
|
+
# "oauth-scope": <str> <-- optional
|
|
57
56
|
# }
|
|
58
57
|
# }
|
|
59
58
|
# }
|
|
@@ -107,7 +106,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
107
106
|
"callback-url": callback_url,
|
|
108
107
|
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
109
108
|
"key-lifetime": public_key_lifetime,
|
|
110
|
-
"
|
|
109
|
+
"cache-obj": FIFOCache(maxsize=1048576)
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
# establish the endpoints
|
|
@@ -147,34 +146,12 @@ def service_login() -> Response:
|
|
|
147
146
|
|
|
148
147
|
# log the request
|
|
149
148
|
if _logger:
|
|
150
|
-
msg
|
|
151
|
-
_logger.debug(msg=msg)
|
|
152
|
-
|
|
153
|
-
# retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state')
|
|
154
|
-
input_params: dict[str, Any] = request.values
|
|
155
|
-
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
156
|
-
user_id: str = input_params.get("user-id") or input_params.get("login") or oauth_state
|
|
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)
|
|
163
|
-
safe_cache: Cache
|
|
164
|
-
if timeout:
|
|
165
|
-
safe_cache = TTLCache(maxsize=16,
|
|
166
|
-
ttl=timeout)
|
|
167
|
-
else:
|
|
168
|
-
safe_cache = FIFOCache(maxsize=16)
|
|
169
|
-
safe_cache["oauth-state"] = oauth_state
|
|
170
|
-
user_data["cache-obj"] = safe_cache
|
|
171
|
-
auth_url: str = (f"{_jusbr_registry["base-url"]}/protocol/openid-connect/auth?response_type=code"
|
|
172
|
-
f"&client_id={_jusbr_registry["client-id"]}"
|
|
173
|
-
f"&redirect_uri={_jusbr_registry["callback-url"]}"
|
|
174
|
-
f"&state={oauth_state}")
|
|
175
|
-
if user_data.get("oauth-scope"):
|
|
176
|
-
auth_url += f"&scope={user_data.get("oauth-scope")}"
|
|
149
|
+
_logger.debug(msg=_log_init(request=request))
|
|
177
150
|
|
|
151
|
+
# obtain the redirect URL
|
|
152
|
+
auth_url: str = _service_login(registry=_jusbr_registry,
|
|
153
|
+
args=request.args,
|
|
154
|
+
logger=_logger)
|
|
178
155
|
# redirect the request
|
|
179
156
|
result: Response = redirect(location=auth_url)
|
|
180
157
|
|
|
@@ -199,17 +176,12 @@ def service_logout() -> Response:
|
|
|
199
176
|
|
|
200
177
|
# log the request
|
|
201
178
|
if _logger:
|
|
202
|
-
msg
|
|
203
|
-
_logger.debug(msg=msg)
|
|
204
|
-
|
|
205
|
-
# retrieve the user id
|
|
206
|
-
input_params: dict[str, Any] = request.args
|
|
207
|
-
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
179
|
+
_logger.debug(msg=_log_init(request=request))
|
|
208
180
|
|
|
209
181
|
# logout the user
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
182
|
+
_service_logout(registry=_jusbr_registry,
|
|
183
|
+
args=request.args,
|
|
184
|
+
logger=_logger)
|
|
213
185
|
|
|
214
186
|
result: Response = Response(status=200)
|
|
215
187
|
|
|
@@ -226,76 +198,28 @@ def service_callback() -> Response:
|
|
|
226
198
|
"""
|
|
227
199
|
Entry point for the callback from JusBR on authentication operation.
|
|
228
200
|
|
|
229
|
-
:return: the response containing the token, or *
|
|
201
|
+
:return: the response containing the token, or *BAD REQUEST*
|
|
230
202
|
"""
|
|
231
203
|
global _jusbr_registry
|
|
232
|
-
from .token_pomes import token_validate
|
|
233
204
|
|
|
234
205
|
# log the request
|
|
235
206
|
if _logger:
|
|
236
|
-
msg
|
|
237
|
-
_logger.debug(msg=msg)
|
|
238
|
-
|
|
239
|
-
# validate the OAuth2 state
|
|
240
|
-
oauth_state: str = request.args.get("state")
|
|
241
|
-
user_id: str | None = None
|
|
242
|
-
user_data: dict[str, Any] | None = None
|
|
243
|
-
if oauth_state:
|
|
244
|
-
for user, data in _jusbr_registry.get("users").items():
|
|
245
|
-
safe_cache: Cache = data.get("cache-obj")
|
|
246
|
-
if user == oauth_state or \
|
|
247
|
-
(safe_cache and oauth_state == safe_cache.get("oauth-state")):
|
|
248
|
-
user_id = user
|
|
249
|
-
user_data = data
|
|
250
|
-
# 'oauth-state' is to be used only once
|
|
251
|
-
safe_cache["oauth-state"] = None
|
|
252
|
-
break
|
|
253
|
-
|
|
254
|
-
# exchange 'code' for the token
|
|
255
|
-
token: str | None = None
|
|
256
|
-
errors: list[str] = []
|
|
257
|
-
if user_data:
|
|
258
|
-
code: str = request.args.get("code")
|
|
259
|
-
body_data: dict[str, Any] = {
|
|
260
|
-
"grant_type": "authorization_code",
|
|
261
|
-
"code": code,
|
|
262
|
-
"redirect_uri": _jusbr_registry.get("callback-url"),
|
|
263
|
-
}
|
|
264
|
-
token = __post_jusbr(user_data=user_data,
|
|
265
|
-
body_data=body_data,
|
|
266
|
-
errors=errors,
|
|
267
|
-
logger=_logger)
|
|
268
|
-
# retrieve the token's claims
|
|
269
|
-
if not errors:
|
|
270
|
-
public_key: bytes = _get_public_key(registry=_jusbr_registry,
|
|
271
|
-
url=_jusbr_registry["base-url"],
|
|
272
|
-
logger=_logger)
|
|
273
|
-
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
274
|
-
issuer=_jusbr_registry["base-url"],
|
|
275
|
-
public_key=public_key,
|
|
276
|
-
errors=errors,
|
|
277
|
-
logger=_logger)
|
|
278
|
-
if not errors:
|
|
279
|
-
token_user: str = token_claims["payload"].get("preferred_username")
|
|
280
|
-
if user_id == oauth_state:
|
|
281
|
-
user_id = token_user
|
|
282
|
-
_jusbr_registry["users"][user_id] = _jusbr_registry["users"].pop(oauth_state)
|
|
283
|
-
elif token_user != user_id:
|
|
284
|
-
errors.append(f"Token was issued to user '{token_user}'")
|
|
285
|
-
else:
|
|
286
|
-
msg: str = "Unknown OAuth2 code received"
|
|
287
|
-
if _get_login_timeout(registry=_jusbr_registry):
|
|
288
|
-
msg += " - possible operation timeout"
|
|
289
|
-
errors.append(msg)
|
|
207
|
+
_logger.debug(msg=_log_init(request=request))
|
|
290
208
|
|
|
209
|
+
# process the callback operation
|
|
210
|
+
errors: list[str] = []
|
|
211
|
+
token_data: tuple[str, str] = _service_callback(registry=_jusbr_registry,
|
|
212
|
+
args=request.args,
|
|
213
|
+
errors=errors,
|
|
214
|
+
logger=_logger)
|
|
291
215
|
result: Response
|
|
292
216
|
if errors:
|
|
293
217
|
result = jsonify({"errors": "; ".join(errors)})
|
|
294
218
|
result.status_code = 400
|
|
295
219
|
else:
|
|
296
220
|
result = jsonify({
|
|
297
|
-
"user_id":
|
|
298
|
-
"access_token":
|
|
221
|
+
"user_id": token_data[0],
|
|
222
|
+
"access_token": token_data[1]})
|
|
299
223
|
|
|
300
224
|
# log the response
|
|
301
225
|
if _logger:
|
|
@@ -312,17 +236,18 @@ def service_token() -> Response:
|
|
|
312
236
|
|
|
313
237
|
:return: the response containing the token, or *UNAUTHORIZED*
|
|
314
238
|
"""
|
|
239
|
+
global _jusbr_registry
|
|
240
|
+
|
|
315
241
|
# log the request
|
|
316
242
|
if _logger:
|
|
317
|
-
msg
|
|
318
|
-
_logger.debug(msg=msg)
|
|
243
|
+
_logger.debug(msg=_log_init(request=request))
|
|
319
244
|
|
|
320
245
|
# 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")
|
|
323
246
|
errors: list[str] = []
|
|
324
|
-
token: str =
|
|
325
|
-
|
|
247
|
+
token: str = _service_token(registry=_jusbr_registry,
|
|
248
|
+
args=request.args,
|
|
249
|
+
errors=errors,
|
|
250
|
+
logger=_logger)
|
|
326
251
|
result: Response
|
|
327
252
|
if token:
|
|
328
253
|
result = jsonify({"token": token})
|
|
@@ -341,54 +266,26 @@ def jusbr_get_token(user_id: str,
|
|
|
341
266
|
errors: list[str] = None,
|
|
342
267
|
logger: Logger = None) -> str:
|
|
343
268
|
"""
|
|
344
|
-
Retrieve
|
|
269
|
+
Retrieve a JusBR authentication token for *user_id*.
|
|
345
270
|
|
|
346
271
|
:param user_id: the user's identification
|
|
347
|
-
:param errors: incidental
|
|
272
|
+
:param errors: incidental errors
|
|
348
273
|
:param logger: optional logger
|
|
349
|
-
:return: the
|
|
274
|
+
:return: the uthentication tokem
|
|
350
275
|
"""
|
|
351
276
|
global _jusbr_registry
|
|
352
277
|
|
|
353
|
-
#
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
safe_cache: Cache = user_data.get("cache-obj")
|
|
360
|
-
if safe_cache:
|
|
361
|
-
access_expiration: int = user_data.get("access-expiration")
|
|
362
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
363
|
-
if now < access_expiration:
|
|
364
|
-
result = safe_cache.get("access-token")
|
|
365
|
-
else:
|
|
366
|
-
# access token has expired
|
|
367
|
-
safe_cache["access-token"] = None
|
|
368
|
-
refresh_token: str = safe_cache.get("refresh-token")
|
|
369
|
-
if refresh_token:
|
|
370
|
-
body_data: dict[str, str] = {
|
|
371
|
-
"grant_type": "refresh_token",
|
|
372
|
-
"refresh_token": refresh_token
|
|
373
|
-
}
|
|
374
|
-
result = __post_jusbr(user_data=user_data,
|
|
375
|
-
body_data=body_data,
|
|
376
|
-
errors=errors,
|
|
377
|
-
logger=logger)
|
|
378
|
-
|
|
379
|
-
elif logger or isinstance(errors, list):
|
|
380
|
-
err_msg: str = f"User '{user_id}' not authenticated with JusBR"
|
|
381
|
-
if isinstance(errors, list):
|
|
382
|
-
errors.append(err_msg)
|
|
383
|
-
if logger:
|
|
384
|
-
logger.error(msg=err_msg)
|
|
385
|
-
|
|
386
|
-
return result
|
|
278
|
+
# retrieve the token
|
|
279
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
280
|
+
return _service_token(registry=_jusbr_registry,
|
|
281
|
+
args=args,
|
|
282
|
+
errors=errors,
|
|
283
|
+
logger=logger)
|
|
387
284
|
|
|
388
285
|
|
|
389
286
|
def jusbr_set_scope(user_id: str,
|
|
390
287
|
scope: str,
|
|
391
|
-
logger: Logger
|
|
288
|
+
logger: Logger = None) -> None:
|
|
392
289
|
"""
|
|
393
290
|
Set the OAuth2 scope of *user_id* to *scope*.
|
|
394
291
|
|
|
@@ -406,91 +303,3 @@ def jusbr_set_scope(user_id: str,
|
|
|
406
303
|
user_data["oauth-scope"] = scope
|
|
407
304
|
if logger:
|
|
408
305
|
logger.debug(msg=f"Scope for user '{user_id}' set to '{scope}'")
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def __post_jusbr(user_data: dict[str, Any],
|
|
412
|
-
body_data: dict[str, Any],
|
|
413
|
-
errors: list[str] | None,
|
|
414
|
-
logger: Logger | None) -> str | None:
|
|
415
|
-
"""
|
|
416
|
-
Send a POST request to JusBR to obtain the authentication token data, and return the access token.
|
|
417
|
-
|
|
418
|
-
For token exchange, *body_data* will have the attributes
|
|
419
|
-
- "grant_type": "authorization_code"
|
|
420
|
-
- "code": <16-character-random-code>
|
|
421
|
-
- "redirect_uri": <callback-url>
|
|
422
|
-
For token refresh, *body_data* will have the attributes
|
|
423
|
-
- "grant_type": "refresh_token"
|
|
424
|
-
- "refresh_token": <current-refresh-token>
|
|
425
|
-
|
|
426
|
-
If the operation is successful, the token data is stored in the registry.
|
|
427
|
-
Otherwise, *errors* will contain the appropriate error message.
|
|
428
|
-
|
|
429
|
-
:param user_data: the user's data in the registry
|
|
430
|
-
:param body_data: the data to send in the body of the request
|
|
431
|
-
:param errors: incidental errors
|
|
432
|
-
:param logger: optional logger
|
|
433
|
-
:return: the access token obtained, or *None* if error
|
|
434
|
-
"""
|
|
435
|
-
global _jusbr_registry
|
|
436
|
-
|
|
437
|
-
# initialize the return variable
|
|
438
|
-
result: str | None = None
|
|
439
|
-
|
|
440
|
-
# complete the data to send in body of request
|
|
441
|
-
body_data["client_id"] = _jusbr_registry.get("client-id")
|
|
442
|
-
client_secret: str = _jusbr_registry.get("client-secret")
|
|
443
|
-
if client_secret:
|
|
444
|
-
body_data["client_secret"] = client_secret
|
|
445
|
-
|
|
446
|
-
# obtain the token
|
|
447
|
-
err_msg: str | None = None
|
|
448
|
-
safe_cache: Cache = user_data.get("cache-obj")
|
|
449
|
-
url: str = _jusbr_registry.get("base-url") + "/protocol/openid-connect/token"
|
|
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)}")
|
|
454
|
-
try:
|
|
455
|
-
# JusBR return on a token request:
|
|
456
|
-
# {
|
|
457
|
-
# "token_type": "Bearer",
|
|
458
|
-
# "access_token": <str>,
|
|
459
|
-
# "expires_in": <number-of-seconds>,
|
|
460
|
-
# "refresh_token": <str>,
|
|
461
|
-
# }
|
|
462
|
-
response: requests.Response = requests.post(url=url,
|
|
463
|
-
data=body_data)
|
|
464
|
-
if response.status_code == 200:
|
|
465
|
-
# request succeeded
|
|
466
|
-
if logger:
|
|
467
|
-
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
468
|
-
reply: dict[str, Any] = response.json()
|
|
469
|
-
result = reply.get("access_token")
|
|
470
|
-
safe_cache: Cache = FIFOCache(maxsize=1024)
|
|
471
|
-
safe_cache["access-token"] = result
|
|
472
|
-
# on token refresh, keep current refresh token if a new one is not provided
|
|
473
|
-
safe_cache["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
474
|
-
user_data["cache-obj"] = safe_cache
|
|
475
|
-
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
476
|
-
else:
|
|
477
|
-
# request resulted in error
|
|
478
|
-
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
479
|
-
if hasattr(response, "content") and response.content:
|
|
480
|
-
err_msg += f", content '{response.content}'"
|
|
481
|
-
if response.status_code == 401 and "refresh_token" in body_data:
|
|
482
|
-
# refresh token is no longer valid
|
|
483
|
-
safe_cache["refresh-token"] = None
|
|
484
|
-
except Exception as e:
|
|
485
|
-
# the operation raised an exception
|
|
486
|
-
err_msg = exc_format(exc=e,
|
|
487
|
-
exc_info=sys.exc_info())
|
|
488
|
-
err_msg = f"POST '{url}': error '{err_msg}'"
|
|
489
|
-
|
|
490
|
-
if err_msg:
|
|
491
|
-
if isinstance(errors, list):
|
|
492
|
-
errors.append(err_msg)
|
|
493
|
-
if logger:
|
|
494
|
-
logger.error(msg=err_msg)
|
|
495
|
-
|
|
496
|
-
return result
|
pypomes_iam/keycloak_pomes.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import secrets
|
|
2
|
-
import string
|
|
3
|
-
# import sys
|
|
4
1
|
from cachetools import FIFOCache
|
|
5
2
|
from datetime import datetime
|
|
6
3
|
from flask import Flask, Response, redirect, request, jsonify
|
|
@@ -11,7 +8,9 @@ from pypomes_core import (
|
|
|
11
8
|
from typing import Any, Final
|
|
12
9
|
|
|
13
10
|
from .common_pomes import (
|
|
14
|
-
|
|
11
|
+
_service_login, _service_logout,
|
|
12
|
+
_service_callback, _service_token,
|
|
13
|
+
_get_user_data, _log_init
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
@@ -43,15 +42,18 @@ KEYCLOAK_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK
|
|
|
43
42
|
# "key-expiration": <int>,
|
|
44
43
|
# "base-url": <str>,
|
|
45
44
|
# "callback-url": <str>,
|
|
45
|
+
# "safe-cache": <FIFOCache>
|
|
46
|
+
# }
|
|
47
|
+
# data in "safe-cache":
|
|
48
|
+
# {
|
|
46
49
|
# "users": {
|
|
47
50
|
# "<user-id>": {
|
|
48
|
-
# "
|
|
51
|
+
# "access-token": <str>
|
|
52
|
+
# "refresh-token": <str>
|
|
49
53
|
# "access-expiration": <timestamp>,
|
|
50
|
-
# "login-expiration": <
|
|
51
|
-
# "login-id": <str>,
|
|
52
|
-
#
|
|
53
|
-
# "access-token": <str>
|
|
54
|
-
# "refresh-token": <str>
|
|
54
|
+
# "login-expiration": <timestamp>, <-- transient
|
|
55
|
+
# "login-id": <str>, <-- transient
|
|
56
|
+
# "oauth-scope": <str> <-- optional
|
|
55
57
|
# }
|
|
56
58
|
# }
|
|
57
59
|
# }
|
|
@@ -108,7 +110,7 @@ def keycloak_setup(flask_app: Flask,
|
|
|
108
110
|
"callback-url": callback_url,
|
|
109
111
|
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
110
112
|
"key-lifetime": public_key_lifetime,
|
|
111
|
-
"
|
|
113
|
+
"safe-cache": FIFOCache(maxsize=1048576)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
# establish the endpoints
|
|
@@ -148,32 +150,12 @@ def service_login() -> Response:
|
|
|
148
150
|
|
|
149
151
|
# log the request
|
|
150
152
|
if _logger:
|
|
151
|
-
msg
|
|
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
|
-
)
|
|
153
|
+
_logger.debug(msg=_log_init(request=request))
|
|
176
154
|
|
|
155
|
+
# obtain the redirect URL
|
|
156
|
+
auth_url: str = _service_login(registry=_keycloak_registry,
|
|
157
|
+
args=request.args,
|
|
158
|
+
logger=_logger)
|
|
177
159
|
# redirect the request
|
|
178
160
|
result: Response = redirect(location=auth_url)
|
|
179
161
|
|
|
@@ -198,17 +180,12 @@ def service_logout() -> Response:
|
|
|
198
180
|
|
|
199
181
|
# log the request
|
|
200
182
|
if _logger:
|
|
201
|
-
msg
|
|
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")
|
|
183
|
+
_logger.debug(msg=_log_init(request=request))
|
|
207
184
|
|
|
208
185
|
# logout the user
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
186
|
+
_service_logout(registry=_keycloak_registry,
|
|
187
|
+
args=request.args,
|
|
188
|
+
logger=_logger)
|
|
212
189
|
|
|
213
190
|
result: Response = Response(status=200)
|
|
214
191
|
|
|
@@ -228,73 +205,25 @@ def service_callback() -> Response:
|
|
|
228
205
|
:return: the response containing the token, or *NOT AUTHORIZED*
|
|
229
206
|
"""
|
|
230
207
|
global _keycloak_registry
|
|
231
|
-
from .token_pomes import token_validate
|
|
232
208
|
|
|
233
209
|
# log the request
|
|
234
210
|
if _logger:
|
|
235
|
-
msg
|
|
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)
|
|
211
|
+
_logger.debug(msg=_log_init(request=request))
|
|
289
212
|
|
|
213
|
+
# process the callback operation
|
|
214
|
+
errors: list[str] = []
|
|
215
|
+
token_data: tuple[str, str] = _service_callback(registry=_keycloak_registry,
|
|
216
|
+
args=request.args,
|
|
217
|
+
errors=errors,
|
|
218
|
+
logger=_logger)
|
|
290
219
|
result: Response
|
|
291
220
|
if errors:
|
|
292
221
|
result = jsonify({"errors": "; ".join(errors)})
|
|
293
222
|
result.status_code = 400
|
|
294
223
|
else:
|
|
295
224
|
result = jsonify({
|
|
296
|
-
"user_id":
|
|
297
|
-
"access_token":
|
|
225
|
+
"user_id": token_data[0],
|
|
226
|
+
"access_token": token_data[1]})
|
|
298
227
|
|
|
299
228
|
# log the response
|
|
300
229
|
if _logger:
|
|
@@ -311,30 +240,70 @@ def service_token() -> Response:
|
|
|
311
240
|
|
|
312
241
|
:return: the response containing the token, or *UNAUTHORIZED*
|
|
313
242
|
"""
|
|
243
|
+
global _keycloak_registry
|
|
244
|
+
|
|
314
245
|
# log the request
|
|
315
246
|
if _logger:
|
|
316
|
-
msg
|
|
317
|
-
_logger.debug(msg=msg)
|
|
247
|
+
_logger.debug(msg=_log_init(request=request))
|
|
318
248
|
|
|
319
|
-
# retrieve the
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
249
|
+
# retrieve the token
|
|
250
|
+
errors: list[str] = []
|
|
251
|
+
token: str = _service_token(registry=_keycloak_registry,
|
|
252
|
+
args=request.args,
|
|
253
|
+
errors=errors,
|
|
254
|
+
logger=_logger)
|
|
255
|
+
result: Response
|
|
256
|
+
if token:
|
|
257
|
+
result = jsonify({"token": token})
|
|
258
|
+
else:
|
|
259
|
+
result = Response("; ".join(errors))
|
|
260
|
+
result.status_code = 401
|
|
261
|
+
|
|
262
|
+
# log the response
|
|
263
|
+
if _logger:
|
|
264
|
+
_logger.debug(msg=f"Response {result}")
|
|
265
|
+
|
|
266
|
+
return result
|
|
323
267
|
|
|
324
268
|
|
|
325
269
|
def keycloak_get_token(user_id: str,
|
|
326
270
|
errors: list[str] = None,
|
|
327
271
|
logger: Logger = None) -> str:
|
|
328
272
|
"""
|
|
329
|
-
Retrieve
|
|
273
|
+
Retrieve a Keycloak authentication token for *user_id*.
|
|
330
274
|
|
|
331
275
|
:param user_id: the user's identification
|
|
332
|
-
:param errors: incidental
|
|
276
|
+
:param errors: incidental errors
|
|
333
277
|
:param logger: optional logger
|
|
334
|
-
:return: the
|
|
278
|
+
:return: the uthentication tokem
|
|
335
279
|
"""
|
|
336
280
|
global _keycloak_registry
|
|
337
281
|
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
return
|
|
282
|
+
# retrieve the token
|
|
283
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
284
|
+
return _service_token(registry=_keycloak_registry,
|
|
285
|
+
args=args,
|
|
286
|
+
errors=errors,
|
|
287
|
+
logger=logger)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def keycloak_set_scope(user_id: str,
|
|
291
|
+
scope: str,
|
|
292
|
+
logger: Logger | None) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Set the OAuth2 scope of *user_id* to *scope*.
|
|
295
|
+
|
|
296
|
+
:param user_id: the user's identification
|
|
297
|
+
:param scope: the OAuth2 scope to set to the user
|
|
298
|
+
:param logger: optional logger
|
|
299
|
+
"""
|
|
300
|
+
global _keycloak_registry
|
|
301
|
+
|
|
302
|
+
# retrieve user data
|
|
303
|
+
user_data: dict[str, Any] = _get_user_data(registry=_keycloak_registry,
|
|
304
|
+
user_id=user_id,
|
|
305
|
+
logger=logger)
|
|
306
|
+
# set the OAuth2 scope
|
|
307
|
+
user_data["oauth-scope"] = scope
|
|
308
|
+
if logger:
|
|
309
|
+
logger.debug(msg=f"Scope for user '{user_id}' set to '{scope}'")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
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
|
|
@@ -13,6 +13,6 @@ Requires-Python: >=3.12
|
|
|
13
13
|
Requires-Dist: cachetools>=6.2.1
|
|
14
14
|
Requires-Dist: flask>=3.1.2
|
|
15
15
|
Requires-Dist: pyjwt>=2.10.1
|
|
16
|
-
Requires-Dist: pypomes-core>=2.8.
|
|
16
|
+
Requires-Dist: pypomes-core>=2.8.1
|
|
17
17
|
Requires-Dist: pypomes-crypto>=0.4.8
|
|
18
18
|
Requires-Dist: requests>=2.32.5
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=ieysDaKOQc3B50PvChh8DLDG5R3XgbTzX3bU0ekGoUk,760
|
|
2
|
+
pypomes_iam/common_pomes.py,sha256=bLDaoWM5KLccxsNSyiK5UbXRNBgqsQ7TB0Q4Nc72QoI,16415
|
|
3
|
+
pypomes_iam/jusbr_pomes.py,sha256=zpvSfQwteY7aL5noG7ARlLT9yNadfuCMdCZB89yvgI4,10932
|
|
4
|
+
pypomes_iam/keycloak_pomes.py,sha256=2KfAQb_-p8C7cXeKSqjHwMIh1ThncinM8VOLEB_JEng,11381
|
|
5
|
+
pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
|
|
6
|
+
pypomes_iam/token_pomes.py,sha256=McjKB8omCjuicenwvDVPiWYu3-7gQeLg1AzgAVKK32M,4309
|
|
7
|
+
pypomes_iam-0.1.9.dist-info/METADATA,sha256=i0F_RcCVWNIfAjKhOlPl2t8-oGk-6bDfPPqURYPvJJo,694
|
|
8
|
+
pypomes_iam-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
pypomes_iam-0.1.9.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
10
|
+
pypomes_iam-0.1.9.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=lHnqNqW1stQjcM6cr9wf3GGnw5_zGf1HN3zyHGb8PCA,577
|
|
2
|
-
pypomes_iam/common_pomes.py,sha256=kdzyEJX275SmMa_zi6AJaC9gVxlXcOailyantPvNOyQ,3908
|
|
3
|
-
pypomes_iam/jusbr_pomes.py,sha256=kNgAgQAMDdODoNO4XKrSggFwQ7R2ID-LLz7tmT3PXH4,19510
|
|
4
|
-
pypomes_iam/keycloak_pomes.py,sha256=m4jM_4c_McVg74T7JG7j3tbMo9Yxp6IKgt8TuauIp7o,13204
|
|
5
|
-
pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
|
|
6
|
-
pypomes_iam/token_pomes.py,sha256=McjKB8omCjuicenwvDVPiWYu3-7gQeLg1AzgAVKK32M,4309
|
|
7
|
-
pypomes_iam-0.1.8.dist-info/METADATA,sha256=fjisTEC7XbvWn37I-2Jez6vtk7Uo83Q1WCjq172hOok,694
|
|
8
|
-
pypomes_iam-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
-
pypomes_iam-0.1.8.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
10
|
-
pypomes_iam-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|