pypomes-iam 0.7.6__py3-none-any.whl → 0.8.5__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.
- pypomes_iam/__init__.py +20 -12
- pypomes_iam/iam_actions.py +188 -66
- pypomes_iam/iam_common.py +71 -29
- pypomes_iam/iam_pomes.py +122 -99
- pypomes_iam/iam_services.py +326 -121
- pypomes_iam/provider_pomes.py +210 -39
- pypomes_iam/token_pomes.py +27 -0
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/METADATA +2 -2
- pypomes_iam-0.8.5.dist-info/RECORD +11 -0
- pypomes_iam-0.7.6.dist-info/RECORD +0 -11
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_services.py
CHANGED
|
@@ -9,8 +9,8 @@ from .iam_common import (
|
|
|
9
9
|
_iam_server_from_endpoint, _iam_server_from_issuer
|
|
10
10
|
)
|
|
11
11
|
from .iam_actions import (
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
iam_login, iam_logout, iam_callback,
|
|
13
|
+
iam_exchange, iam_get_token, iam_userinfo
|
|
14
14
|
)
|
|
15
15
|
from .token_pomes import token_get_claims, token_validate
|
|
16
16
|
|
|
@@ -23,7 +23,10 @@ def jwt_required(func: callable) -> callable:
|
|
|
23
23
|
"""
|
|
24
24
|
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
25
25
|
|
|
26
|
+
The decorated function must be a registered endpoint to a *Flask* application.
|
|
27
|
+
|
|
26
28
|
:param func: the function being decorated
|
|
29
|
+
:return: the return from the call to *func*, or a *Response NOT AUTHORIZED* if the authentication failed
|
|
27
30
|
"""
|
|
28
31
|
# ruff: noqa: ANN003 - Missing type annotation for *{name}
|
|
29
32
|
def wrapper(*args, **kwargs) -> Response:
|
|
@@ -44,22 +47,20 @@ def __request_validate(request: Request) -> Response:
|
|
|
44
47
|
Because this code has a high usage frequency, only authentication failures are logged.
|
|
45
48
|
|
|
46
49
|
:param request: the *request* to be verified
|
|
47
|
-
:return: *None* if the *request* is valid, otherwise a *Response
|
|
50
|
+
:return: *None* if the *request* is valid, otherwise a *Response NOT AUTHORIZED*
|
|
48
51
|
"""
|
|
49
52
|
# initialize the return variable
|
|
50
53
|
result: Response | None = None
|
|
51
54
|
|
|
52
|
-
# retrieve the authorization from the request header
|
|
53
|
-
auth_header: str = request.headers.get("Authorization")
|
|
54
|
-
|
|
55
55
|
# validate the authorization token
|
|
56
56
|
bad_token: bool = True
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
token: str = __get_bearer_token(request=request)
|
|
58
|
+
if token:
|
|
59
|
+
# extract token claims
|
|
60
60
|
claims: dict[str, Any] = token_get_claims(token=token)
|
|
61
61
|
if claims:
|
|
62
62
|
issuer: str = claims["payload"].get("iss")
|
|
63
|
+
public_key: str | None = None
|
|
63
64
|
recipient_attr: str | None = None
|
|
64
65
|
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
65
66
|
with _iam_lock:
|
|
@@ -74,17 +75,17 @@ def __request_validate(request: Request) -> Response:
|
|
|
74
75
|
logger=__IAM_LOGGER)
|
|
75
76
|
if registry:
|
|
76
77
|
recipient_attr = registry[IamParam.RECIPIENT_ATTR]
|
|
77
|
-
public_key
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
public_key = _get_public_key(iam_server=iam_server,
|
|
79
|
+
errors=None,
|
|
80
|
+
logger=__IAM_LOGGER)
|
|
80
81
|
# validate the token (log errors, only)
|
|
81
82
|
errors: list[str] = []
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
if token_validate(token=token,
|
|
84
|
+
issuer=issuer,
|
|
85
|
+
recipient_id=recipient_id,
|
|
86
|
+
recipient_attr=recipient_attr,
|
|
87
|
+
public_key=public_key,
|
|
88
|
+
errors=errors):
|
|
88
89
|
# token is valid
|
|
89
90
|
bad_token = False
|
|
90
91
|
elif __IAM_LOGGER:
|
|
@@ -99,7 +100,27 @@ def __request_validate(request: Request) -> Response:
|
|
|
99
100
|
return result
|
|
100
101
|
|
|
101
102
|
|
|
102
|
-
def
|
|
103
|
+
def __get_bearer_token(request: Request) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Retrieve the bearer token sent in the header of *request*.
|
|
106
|
+
|
|
107
|
+
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
108
|
+
|
|
109
|
+
:param request: the *request* to retrieve the token from
|
|
110
|
+
:return: the bearer token, or *None* if not found
|
|
111
|
+
"""
|
|
112
|
+
# initialize the return variable
|
|
113
|
+
result: str | None = None
|
|
114
|
+
|
|
115
|
+
# retrieve the authorization from the request header
|
|
116
|
+
auth_header: str = request.headers.get("Authorization")
|
|
117
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
118
|
+
result: str = auth_header.split(" ")[1]
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def iam_setup_logger(logger: Logger) -> None:
|
|
103
124
|
"""
|
|
104
125
|
Register the logger for HTTP services.
|
|
105
126
|
|
|
@@ -109,13 +130,66 @@ def logger_register(logger: Logger) -> None:
|
|
|
109
130
|
__IAM_LOGGER = logger
|
|
110
131
|
|
|
111
132
|
|
|
112
|
-
# @flask_app.route(rule=<
|
|
113
|
-
# methods=["
|
|
114
|
-
|
|
133
|
+
# @flask_app.route(rule=<setup_server_endpoint>,
|
|
134
|
+
# methods=["POST"])
|
|
135
|
+
def service_setup_server() -> Response:
|
|
136
|
+
"""
|
|
137
|
+
Entry point to setup a *IAM* server.
|
|
138
|
+
|
|
139
|
+
These are the expected parameters in the request's body, in a JSON or as form data:
|
|
140
|
+
- *iam_server*: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
|
|
141
|
+
- *admin_id*: identifies the realm administrator
|
|
142
|
+
- *admin_secret*: password for the realm administrator
|
|
143
|
+
- *client_id*: the client's identification with the *IAM* server
|
|
144
|
+
- *client_realm*: the client's realm
|
|
145
|
+
- *client_secret*: the client's password with the *IAM* server
|
|
146
|
+
- *login_timeout*: timeout for login authentication (in seconds,defaults to no timeout)
|
|
147
|
+
- *k_lifetime*: how long to use *IAM* server's public key, before refreshing it (in seconds)
|
|
148
|
+
- *recipient_attr*: attribute in the token's payload holding the token's subject
|
|
149
|
+
- *rl_base*: base URL to request services
|
|
150
|
+
|
|
151
|
+
For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
|
|
152
|
+
environment variables. Most parameters are required to have values, which must be assigned either
|
|
153
|
+
throught the function invocation, or from the corresponding environment variables.
|
|
154
|
+
|
|
155
|
+
The parameters *admin_id* and *admin_secret* are required only if performing administrative tasks is intended.
|
|
156
|
+
The optional parameter *ogin_timeout* refers to the maximum time in seconds allowed for the user
|
|
157
|
+
to login at the *IAM* server's login page, and defaults to no time limit.
|
|
158
|
+
|
|
159
|
+
The parameter *client_secret* is required in most requests to the *IAM* server. In the case
|
|
160
|
+
it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
|
|
161
|
+
the first time it is needed.
|
|
162
|
+
|
|
163
|
+
:return: *Response OK*
|
|
164
|
+
"""
|
|
165
|
+
# retrieve the request arguments
|
|
166
|
+
args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
|
|
167
|
+
|
|
168
|
+
# log the request
|
|
169
|
+
if __IAM_LOGGER:
|
|
170
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
171
|
+
ensure_ascii=False)}")
|
|
172
|
+
# setup the server
|
|
173
|
+
from .iam_pomes import iam_setup_server
|
|
174
|
+
iam_setup_server(**args)
|
|
175
|
+
result = Response(status=200)
|
|
176
|
+
|
|
177
|
+
# log the response
|
|
178
|
+
if __IAM_LOGGER:
|
|
179
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# @flask_app.route(rule=<login_endpoint>, # IAM_ENDPOINT_LOGIN
|
|
115
185
|
# methods=["GET"])
|
|
116
186
|
def service_login() -> Response:
|
|
117
187
|
"""
|
|
118
|
-
Entry point for the IAM server's login service.
|
|
188
|
+
Entry point for the *IAM* server's login service.
|
|
189
|
+
|
|
190
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
191
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
192
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
119
193
|
|
|
120
194
|
These are the expected request parameters:
|
|
121
195
|
- user-id: optional, identifies the reference user (alias: 'login')
|
|
@@ -134,10 +208,13 @@ def service_login() -> Response:
|
|
|
134
208
|
# declare the return variable
|
|
135
209
|
result: Response | None = None
|
|
136
210
|
|
|
211
|
+
# retrieve the request arguments
|
|
212
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
213
|
+
|
|
137
214
|
# log the request
|
|
138
215
|
if __IAM_LOGGER:
|
|
139
|
-
__IAM_LOGGER.debug(msg=
|
|
140
|
-
|
|
216
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
217
|
+
ensure_ascii=False)}")
|
|
141
218
|
errors: list[str] = []
|
|
142
219
|
with _iam_lock:
|
|
143
220
|
# retrieve the IAM server
|
|
@@ -146,10 +223,10 @@ def service_login() -> Response:
|
|
|
146
223
|
logger=__IAM_LOGGER)
|
|
147
224
|
if iam_server:
|
|
148
225
|
# obtain the login URL
|
|
149
|
-
login_url: str =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
226
|
+
login_url: str = iam_login(iam_server=iam_server,
|
|
227
|
+
args=args,
|
|
228
|
+
errors=errors,
|
|
229
|
+
logger=__IAM_LOGGER)
|
|
153
230
|
if login_url:
|
|
154
231
|
result = jsonify({"login-url": login_url})
|
|
155
232
|
if errors:
|
|
@@ -158,18 +235,21 @@ def service_login() -> Response:
|
|
|
158
235
|
|
|
159
236
|
# log the response
|
|
160
237
|
if __IAM_LOGGER:
|
|
161
|
-
__IAM_LOGGER.debug(msg=f"Response {result}
|
|
238
|
+
__IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
|
|
162
239
|
|
|
163
240
|
return result
|
|
164
241
|
|
|
165
242
|
|
|
166
|
-
# @flask_app.route(rule=<logout_endpoint>, #
|
|
167
|
-
# methods=["
|
|
168
|
-
|
|
169
|
-
# methods=["GET"])
|
|
243
|
+
# @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
|
|
244
|
+
# methods=["POST"])
|
|
245
|
+
@jwt_required
|
|
170
246
|
def service_logout() -> Response:
|
|
171
247
|
"""
|
|
172
|
-
Entry point for the IAM server's logout service.
|
|
248
|
+
Entry point for the *IAM* server's logout service.
|
|
249
|
+
|
|
250
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
251
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
252
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
173
253
|
|
|
174
254
|
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
175
255
|
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
@@ -180,10 +260,13 @@ def service_logout() -> Response:
|
|
|
180
260
|
# declare the return variable
|
|
181
261
|
result: Response | None
|
|
182
262
|
|
|
263
|
+
# retrieve the request arguments
|
|
264
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
265
|
+
|
|
183
266
|
# log the request
|
|
184
267
|
if __IAM_LOGGER:
|
|
185
|
-
__IAM_LOGGER.debug(msg=
|
|
186
|
-
|
|
268
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
269
|
+
ensure_ascii=False)}")
|
|
187
270
|
errors: list[str] = []
|
|
188
271
|
with _iam_lock:
|
|
189
272
|
# retrieve the IAM server
|
|
@@ -192,30 +275,32 @@ def service_logout() -> Response:
|
|
|
192
275
|
logger=__IAM_LOGGER)
|
|
193
276
|
if iam_server:
|
|
194
277
|
# logout the user
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
278
|
+
iam_logout(iam_server=iam_server,
|
|
279
|
+
args=args,
|
|
280
|
+
errors=errors,
|
|
281
|
+
logger=__IAM_LOGGER)
|
|
199
282
|
if errors:
|
|
200
283
|
result = Response(response="; ".join(errors),
|
|
201
284
|
status=400)
|
|
202
285
|
else:
|
|
203
286
|
result = Response(status=204)
|
|
204
287
|
|
|
205
|
-
# log the response
|
|
206
288
|
if __IAM_LOGGER:
|
|
289
|
+
# log the response
|
|
207
290
|
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
208
291
|
|
|
209
292
|
return result
|
|
210
293
|
|
|
211
294
|
|
|
212
|
-
# @flask_app.route(rule=<callback_endpoint>, #
|
|
295
|
+
# @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
|
|
213
296
|
# methods=["GET", "POST"])
|
|
214
|
-
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
|
|
215
|
-
# methods=["POST"])
|
|
216
297
|
def service_callback() -> Response:
|
|
217
298
|
"""
|
|
218
|
-
Entry point for the callback from the IAM server on authentication operation.
|
|
299
|
+
Entry point for the callback from the *IAM* server on authentication operation.
|
|
300
|
+
|
|
301
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
302
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
303
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
219
304
|
|
|
220
305
|
This callback is invoked from a front-end application after a successful login at the
|
|
221
306
|
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
@@ -233,10 +318,13 @@ def service_callback() -> Response:
|
|
|
233
318
|
|
|
234
319
|
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
235
320
|
"""
|
|
321
|
+
# retrieve the request arguments
|
|
322
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
323
|
+
|
|
236
324
|
# log the request
|
|
237
325
|
if __IAM_LOGGER:
|
|
238
|
-
__IAM_LOGGER.debug(msg=
|
|
239
|
-
|
|
326
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
327
|
+
ensure_ascii=False)}")
|
|
240
328
|
errors: list[str] = []
|
|
241
329
|
token_data: tuple[str, str] | None = None
|
|
242
330
|
with _iam_lock:
|
|
@@ -246,10 +334,10 @@ def service_callback() -> Response:
|
|
|
246
334
|
logger=__IAM_LOGGER)
|
|
247
335
|
if iam_server:
|
|
248
336
|
# process the callback operation
|
|
249
|
-
token_data =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
337
|
+
token_data = iam_callback(iam_server=iam_server,
|
|
338
|
+
args=args,
|
|
339
|
+
errors=errors,
|
|
340
|
+
logger=__IAM_LOGGER)
|
|
253
341
|
result: Response
|
|
254
342
|
if errors:
|
|
255
343
|
result = jsonify({"errors": "; ".join(errors)})
|
|
@@ -264,60 +352,137 @@ def service_callback() -> Response:
|
|
|
264
352
|
return result
|
|
265
353
|
|
|
266
354
|
|
|
267
|
-
# @flask_app.route(rule=<
|
|
268
|
-
# methods=["
|
|
269
|
-
|
|
270
|
-
# methods=["GET"])
|
|
271
|
-
def service_token() -> Response:
|
|
355
|
+
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
|
|
356
|
+
# methods=["POST"])
|
|
357
|
+
def service_exchange() -> Response:
|
|
272
358
|
"""
|
|
273
|
-
Entry point for
|
|
359
|
+
Entry point for requesting the *IAM* server to exchange the token.
|
|
274
360
|
|
|
275
|
-
|
|
361
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
362
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
363
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
364
|
+
|
|
365
|
+
If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
366
|
+
Otherwise, *errors* will contain the appropriate error message.
|
|
367
|
+
|
|
368
|
+
The expected request parameters are:
|
|
369
|
+
- user-id: identification for the reference user (alias: 'login')
|
|
370
|
+
- access-token: the token to be exchanged
|
|
276
371
|
|
|
277
372
|
On success, the returned *Response* will contain the following JSON:
|
|
278
373
|
{
|
|
279
374
|
"user-id": <reference-user-identification>,
|
|
280
|
-
"access-token": <token>
|
|
375
|
+
"access-token": <the-exchanged-token>
|
|
281
376
|
}
|
|
282
377
|
|
|
283
|
-
:return: *Response* containing the user
|
|
378
|
+
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
284
379
|
"""
|
|
380
|
+
# retrieve the request arguments
|
|
381
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
382
|
+
|
|
285
383
|
# log the request
|
|
286
384
|
if __IAM_LOGGER:
|
|
287
|
-
__IAM_LOGGER.debug(msg=
|
|
288
|
-
|
|
289
|
-
# obtain the user's identification
|
|
290
|
-
args: dict[str, Any] = request.args
|
|
291
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
292
|
-
|
|
385
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
386
|
+
ensure_ascii=False)}")
|
|
293
387
|
errors: list[str] = []
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
logger=__IAM_LOGGER)
|
|
308
|
-
else:
|
|
309
|
-
msg: str = "User identification not provided"
|
|
310
|
-
errors.append(msg)
|
|
311
|
-
if __IAM_LOGGER:
|
|
312
|
-
__IAM_LOGGER.error(msg=msg)
|
|
313
|
-
|
|
388
|
+
with _iam_lock:
|
|
389
|
+
# retrieve the IAM server
|
|
390
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
391
|
+
errors=errors,
|
|
392
|
+
logger=__IAM_LOGGER)
|
|
393
|
+
# exchange the token
|
|
394
|
+
token_info: tuple[str, str] | None = None
|
|
395
|
+
if iam_server:
|
|
396
|
+
errors: list[str] = []
|
|
397
|
+
token_info = iam_exchange(iam_server=iam_server,
|
|
398
|
+
args=args,
|
|
399
|
+
errors=errors,
|
|
400
|
+
logger=__IAM_LOGGER)
|
|
314
401
|
result: Response
|
|
315
402
|
if errors:
|
|
316
403
|
result = Response(response="; ".join(errors),
|
|
317
404
|
status=400)
|
|
318
405
|
else:
|
|
319
|
-
result = jsonify({"user-id":
|
|
320
|
-
"access-token":
|
|
406
|
+
result = jsonify({"user-id": token_info[0],
|
|
407
|
+
"access-token": token_info[1]})
|
|
408
|
+
if __IAM_LOGGER:
|
|
409
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
410
|
+
__IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# @flask_app.route(rule=/iam/jusbr:callback-exchange,
|
|
416
|
+
# methods=["GET"])
|
|
417
|
+
def service_callback_exchange() -> Response:
|
|
418
|
+
"""
|
|
419
|
+
Entry point for the callback from the IAM server on authentication operation, with subsequent token exchange.
|
|
420
|
+
|
|
421
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
422
|
+
the name of the *IAM* server in charge of handling this service, and suffixed with the string *_to_*
|
|
423
|
+
followed by the name of the *IAM* server in charge of the token exchange. The prefixing, but not the suffixing,
|
|
424
|
+
is done automatically if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
425
|
+
|
|
426
|
+
This callback is invoked from a front-end application after a successful login at the
|
|
427
|
+
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
428
|
+
this data is then used to effectively obtain the token from the *IAM* server.
|
|
429
|
+
This token is stored and thereafter, a corresponding token is requested from another IAM *server*,
|
|
430
|
+
in a scheme known as "token exchange". This new token, along with the reference user identification,
|
|
431
|
+
are then stored. Note that the original token is the one actually returned.
|
|
432
|
+
|
|
433
|
+
The relevant expected request arguments are:
|
|
434
|
+
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
435
|
+
- *code*: the temporary authorization code provided by the IAM server, to be exchanged for the token
|
|
436
|
+
|
|
437
|
+
On success, the returned *Response* will contain the following JSON:
|
|
438
|
+
{
|
|
439
|
+
"user-id": <reference-user-identification>,
|
|
440
|
+
"access-token": <the-original-token>
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
444
|
+
"""
|
|
445
|
+
# declare the return variable
|
|
446
|
+
result: Response | None = None
|
|
447
|
+
|
|
448
|
+
# retrieve the request arguments
|
|
449
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
450
|
+
|
|
451
|
+
# log the request
|
|
452
|
+
if __IAM_LOGGER:
|
|
453
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
454
|
+
ensure_ascii=False)}")
|
|
455
|
+
errors: list[str] = []
|
|
456
|
+
with _iam_lock:
|
|
457
|
+
# retrieve the IAM server
|
|
458
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
459
|
+
errors=errors,
|
|
460
|
+
logger=__IAM_LOGGER)
|
|
461
|
+
# obtain the login URL
|
|
462
|
+
token_info: tuple[str, str] = iam_callback(iam_server=iam_server,
|
|
463
|
+
args=args,
|
|
464
|
+
errors=errors,
|
|
465
|
+
logger=__IAM_LOGGER)
|
|
466
|
+
if token_info:
|
|
467
|
+
args: dict[str, str] = {
|
|
468
|
+
"user-id": token_info[0],
|
|
469
|
+
"access-token": token_info[1]
|
|
470
|
+
}
|
|
471
|
+
# retrieve the exchange IAM server
|
|
472
|
+
pos: int = request.endpoint.index("_to_")
|
|
473
|
+
exchange_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint[pos+4],
|
|
474
|
+
errors=errors,
|
|
475
|
+
logger=__IAM_LOGGER)
|
|
476
|
+
token_info = iam_exchange(iam_server=exchange_server,
|
|
477
|
+
args=args,
|
|
478
|
+
logger=__IAM_LOGGER)
|
|
479
|
+
if token_info:
|
|
480
|
+
result = jsonify({"user-id": token_info[0],
|
|
481
|
+
"access-token": token_info[1]})
|
|
482
|
+
if errors:
|
|
483
|
+
result = Response("; ".join(errors))
|
|
484
|
+
result.status_code = 400
|
|
485
|
+
|
|
321
486
|
if __IAM_LOGGER:
|
|
322
487
|
# log the response (the returned data is not logged, as it contains the token)
|
|
323
488
|
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
@@ -325,19 +490,17 @@ def service_token() -> Response:
|
|
|
325
490
|
return result
|
|
326
491
|
|
|
327
492
|
|
|
328
|
-
# @flask_app.route(rule=<
|
|
329
|
-
# methods=["
|
|
330
|
-
def
|
|
493
|
+
# @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
|
|
494
|
+
# methods=["GET"])
|
|
495
|
+
def service_get_token() -> Response:
|
|
331
496
|
"""
|
|
332
|
-
Entry point for
|
|
497
|
+
Entry point for retrieving a token from the *IAM* server.
|
|
333
498
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
- access-token: the token to be exchanged
|
|
499
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
500
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
501
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
338
502
|
|
|
339
|
-
|
|
340
|
-
Otherwise, *errors* will contain the appropriate error message.
|
|
503
|
+
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
341
504
|
|
|
342
505
|
On success, the returned *Response* will contain the following JSON:
|
|
343
506
|
{
|
|
@@ -345,49 +508,91 @@ def service_exchange() -> Response:
|
|
|
345
508
|
"access-token": <token>
|
|
346
509
|
}
|
|
347
510
|
|
|
348
|
-
:return: *Response* containing the token
|
|
511
|
+
:return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
|
|
349
512
|
"""
|
|
513
|
+
# retrieve the request arguments
|
|
514
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
515
|
+
|
|
350
516
|
# log the request
|
|
351
517
|
if __IAM_LOGGER:
|
|
352
|
-
__IAM_LOGGER.debug(msg=
|
|
353
|
-
|
|
518
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
519
|
+
ensure_ascii=False)}")
|
|
354
520
|
errors: list[str] = []
|
|
521
|
+
token_info: dict[str, str] | None = None
|
|
355
522
|
with _iam_lock:
|
|
356
|
-
# retrieve the IAM server
|
|
523
|
+
# retrieve the IAM server
|
|
357
524
|
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
358
525
|
errors=errors,
|
|
359
526
|
logger=__IAM_LOGGER)
|
|
360
|
-
# exchange the token
|
|
361
|
-
token_info: tuple[str, str] | None = None
|
|
362
527
|
if iam_server:
|
|
528
|
+
# retrieve the token
|
|
363
529
|
errors: list[str] = []
|
|
364
|
-
token_info =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
530
|
+
token_info = iam_get_token(iam_server=iam_server,
|
|
531
|
+
args=args,
|
|
532
|
+
errors=errors,
|
|
533
|
+
logger=__IAM_LOGGER)
|
|
368
534
|
result: Response
|
|
369
535
|
if errors:
|
|
370
536
|
result = Response(response="; ".join(errors),
|
|
371
537
|
status=400)
|
|
372
538
|
else:
|
|
373
|
-
result = jsonify(
|
|
374
|
-
"access-token": token_info[1]})
|
|
375
|
-
|
|
376
|
-
# log the response
|
|
539
|
+
result = jsonify(token_info)
|
|
377
540
|
if __IAM_LOGGER:
|
|
378
|
-
|
|
541
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
542
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
379
543
|
|
|
380
544
|
return result
|
|
381
545
|
|
|
382
546
|
|
|
383
|
-
|
|
547
|
+
# @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_USERINFO
|
|
548
|
+
# methods=["GET"])
|
|
549
|
+
@jwt_required
|
|
550
|
+
def service_userinfo() -> Response:
|
|
384
551
|
"""
|
|
385
|
-
|
|
552
|
+
Entry point for retrieving user data from the *IAM* server.
|
|
553
|
+
|
|
554
|
+
When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
|
|
555
|
+
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
556
|
+
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
386
557
|
|
|
387
|
-
|
|
388
|
-
|
|
558
|
+
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
559
|
+
|
|
560
|
+
On success, the returned *Response* will contain a JSON with information kept by *iam_server* about *user_id*.
|
|
561
|
+
|
|
562
|
+
:return: *Response* containing user data, or *BAD REQUEST*
|
|
389
563
|
"""
|
|
564
|
+
# retrieve the request arguments
|
|
565
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
566
|
+
|
|
567
|
+
# log the request
|
|
568
|
+
if __IAM_LOGGER:
|
|
569
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
570
|
+
ensure_ascii=False)}")
|
|
571
|
+
# retrieve the bearer token
|
|
572
|
+
args["access-token"] = __get_bearer_token(request=request)
|
|
390
573
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
574
|
+
errors: list[str] = []
|
|
575
|
+
user_info: dict[str, str] | None = None
|
|
576
|
+
with _iam_lock:
|
|
577
|
+
# retrieve the IAM server
|
|
578
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
579
|
+
errors=errors,
|
|
580
|
+
logger=__IAM_LOGGER)
|
|
581
|
+
if iam_server:
|
|
582
|
+
# retrieve the token
|
|
583
|
+
errors: list[str] = []
|
|
584
|
+
user_info = iam_userinfo(iam_server=iam_server,
|
|
585
|
+
args=args,
|
|
586
|
+
errors=errors,
|
|
587
|
+
logger=__IAM_LOGGER)
|
|
588
|
+
result: Response
|
|
589
|
+
if errors:
|
|
590
|
+
result = Response(response="; ".join(errors),
|
|
591
|
+
status=400)
|
|
592
|
+
else:
|
|
593
|
+
result = jsonify(user_info)
|
|
594
|
+
if __IAM_LOGGER:
|
|
595
|
+
# log the response
|
|
596
|
+
__IAM_LOGGER.debug(msg=f"Response {result}; {json.dumps(obj=user_info,
|
|
597
|
+
ensure_ascii=False)}")
|
|
598
|
+
return result
|