pypomes-iam 0.7.4__py3-none-any.whl → 0.8.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pypomes-iam might be problematic. Click here for more details.
- pypomes_iam/__init__.py +20 -12
- pypomes_iam/iam_actions.py +227 -62
- pypomes_iam/iam_common.py +71 -29
- pypomes_iam/iam_pomes.py +122 -99
- pypomes_iam/iam_services.py +327 -121
- pypomes_iam/provider_pomes.py +203 -33
- pypomes_iam/token_pomes.py +27 -0
- {pypomes_iam-0.7.4.dist-info → pypomes_iam-0.8.3.dist-info}/METADATA +2 -2
- pypomes_iam-0.8.3.dist-info/RECORD +11 -0
- pypomes_iam-0.7.4.dist-info/RECORD +0 -11
- {pypomes_iam-0.7.4.dist-info → pypomes_iam-0.8.3.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.7.4.dist-info → pypomes_iam-0.8.3.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,17 +130,71 @@ 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')
|
|
122
196
|
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
197
|
+
-target-idp: optionally, identify a target identity provider for the login operation
|
|
123
198
|
|
|
124
199
|
If provided, the user identification will be validated against the authorization data
|
|
125
200
|
returned by *iam_server* upon login. On success, the following JSON, containing the appropriate
|
|
@@ -133,10 +208,13 @@ def service_login() -> Response:
|
|
|
133
208
|
# declare the return variable
|
|
134
209
|
result: Response | None = None
|
|
135
210
|
|
|
211
|
+
# retrieve the request arguments
|
|
212
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
213
|
+
|
|
136
214
|
# log the request
|
|
137
215
|
if __IAM_LOGGER:
|
|
138
|
-
__IAM_LOGGER.debug(msg=
|
|
139
|
-
|
|
216
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
217
|
+
ensure_ascii=False)}")
|
|
140
218
|
errors: list[str] = []
|
|
141
219
|
with _iam_lock:
|
|
142
220
|
# retrieve the IAM server
|
|
@@ -145,10 +223,10 @@ def service_login() -> Response:
|
|
|
145
223
|
logger=__IAM_LOGGER)
|
|
146
224
|
if iam_server:
|
|
147
225
|
# obtain the login URL
|
|
148
|
-
login_url: str =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
login_url: str = iam_login(iam_server=iam_server,
|
|
227
|
+
args=args,
|
|
228
|
+
errors=errors,
|
|
229
|
+
logger=__IAM_LOGGER)
|
|
152
230
|
if login_url:
|
|
153
231
|
result = jsonify({"login-url": login_url})
|
|
154
232
|
if errors:
|
|
@@ -157,18 +235,21 @@ def service_login() -> Response:
|
|
|
157
235
|
|
|
158
236
|
# log the response
|
|
159
237
|
if __IAM_LOGGER:
|
|
160
|
-
__IAM_LOGGER.debug(msg=f"Response {result}
|
|
238
|
+
__IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
|
|
161
239
|
|
|
162
240
|
return result
|
|
163
241
|
|
|
164
242
|
|
|
165
|
-
# @flask_app.route(rule=<logout_endpoint>, #
|
|
166
|
-
# methods=["
|
|
167
|
-
|
|
168
|
-
# methods=["GET"])
|
|
243
|
+
# @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
|
|
244
|
+
# methods=["POST"])
|
|
245
|
+
@jwt_required
|
|
169
246
|
def service_logout() -> Response:
|
|
170
247
|
"""
|
|
171
|
-
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()*.
|
|
172
253
|
|
|
173
254
|
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
174
255
|
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
@@ -179,10 +260,13 @@ def service_logout() -> Response:
|
|
|
179
260
|
# declare the return variable
|
|
180
261
|
result: Response | None
|
|
181
262
|
|
|
263
|
+
# retrieve the request arguments
|
|
264
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
265
|
+
|
|
182
266
|
# log the request
|
|
183
267
|
if __IAM_LOGGER:
|
|
184
|
-
__IAM_LOGGER.debug(msg=
|
|
185
|
-
|
|
268
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
269
|
+
ensure_ascii=False)}")
|
|
186
270
|
errors: list[str] = []
|
|
187
271
|
with _iam_lock:
|
|
188
272
|
# retrieve the IAM server
|
|
@@ -191,30 +275,32 @@ def service_logout() -> Response:
|
|
|
191
275
|
logger=__IAM_LOGGER)
|
|
192
276
|
if iam_server:
|
|
193
277
|
# logout the user
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
278
|
+
iam_logout(iam_server=iam_server,
|
|
279
|
+
args=args,
|
|
280
|
+
errors=errors,
|
|
281
|
+
logger=__IAM_LOGGER)
|
|
198
282
|
if errors:
|
|
199
283
|
result = Response(response="; ".join(errors),
|
|
200
284
|
status=400)
|
|
201
285
|
else:
|
|
202
286
|
result = Response(status=204)
|
|
203
287
|
|
|
204
|
-
# log the response
|
|
205
288
|
if __IAM_LOGGER:
|
|
289
|
+
# log the response
|
|
206
290
|
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
207
291
|
|
|
208
292
|
return result
|
|
209
293
|
|
|
210
294
|
|
|
211
|
-
# @flask_app.route(rule=<callback_endpoint>, #
|
|
295
|
+
# @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
|
|
212
296
|
# methods=["GET", "POST"])
|
|
213
|
-
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
|
|
214
|
-
# methods=["POST"])
|
|
215
297
|
def service_callback() -> Response:
|
|
216
298
|
"""
|
|
217
|
-
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()*.
|
|
218
304
|
|
|
219
305
|
This callback is invoked from a front-end application after a successful login at the
|
|
220
306
|
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
@@ -232,10 +318,13 @@ def service_callback() -> Response:
|
|
|
232
318
|
|
|
233
319
|
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
234
320
|
"""
|
|
321
|
+
# retrieve the request arguments
|
|
322
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
323
|
+
|
|
235
324
|
# log the request
|
|
236
325
|
if __IAM_LOGGER:
|
|
237
|
-
__IAM_LOGGER.debug(msg=
|
|
238
|
-
|
|
326
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
327
|
+
ensure_ascii=False)}")
|
|
239
328
|
errors: list[str] = []
|
|
240
329
|
token_data: tuple[str, str] | None = None
|
|
241
330
|
with _iam_lock:
|
|
@@ -245,10 +334,10 @@ def service_callback() -> Response:
|
|
|
245
334
|
logger=__IAM_LOGGER)
|
|
246
335
|
if iam_server:
|
|
247
336
|
# process the callback operation
|
|
248
|
-
token_data =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
337
|
+
token_data = iam_callback(iam_server=iam_server,
|
|
338
|
+
args=args,
|
|
339
|
+
errors=errors,
|
|
340
|
+
logger=__IAM_LOGGER)
|
|
252
341
|
result: Response
|
|
253
342
|
if errors:
|
|
254
343
|
result = jsonify({"errors": "; ".join(errors)})
|
|
@@ -263,60 +352,137 @@ def service_callback() -> Response:
|
|
|
263
352
|
return result
|
|
264
353
|
|
|
265
354
|
|
|
266
|
-
# @flask_app.route(rule=<
|
|
267
|
-
# methods=["
|
|
268
|
-
|
|
269
|
-
# methods=["GET"])
|
|
270
|
-
def service_token() -> Response:
|
|
355
|
+
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
|
|
356
|
+
# methods=["POST"])
|
|
357
|
+
def service_exchange() -> Response:
|
|
271
358
|
"""
|
|
272
|
-
Entry point for
|
|
359
|
+
Entry point for requesting the *IAM* server to exchange the token.
|
|
273
360
|
|
|
274
|
-
|
|
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
|
|
275
371
|
|
|
276
372
|
On success, the returned *Response* will contain the following JSON:
|
|
277
373
|
{
|
|
278
374
|
"user-id": <reference-user-identification>,
|
|
279
|
-
"access-token": <token>
|
|
375
|
+
"access-token": <the-exchanged-token>
|
|
280
376
|
}
|
|
281
377
|
|
|
282
|
-
:return: *Response* containing the user
|
|
378
|
+
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
283
379
|
"""
|
|
380
|
+
# retrieve the request arguments
|
|
381
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
382
|
+
|
|
284
383
|
# log the request
|
|
285
384
|
if __IAM_LOGGER:
|
|
286
|
-
__IAM_LOGGER.debug(msg=
|
|
287
|
-
|
|
288
|
-
# obtain the user's identification
|
|
289
|
-
args: dict[str, Any] = request.args
|
|
290
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
291
|
-
|
|
385
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
386
|
+
ensure_ascii=False)}")
|
|
292
387
|
errors: list[str] = []
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
logger=__IAM_LOGGER)
|
|
307
|
-
else:
|
|
308
|
-
msg: str = "User identification not provided"
|
|
309
|
-
errors.append(msg)
|
|
310
|
-
if __IAM_LOGGER:
|
|
311
|
-
__IAM_LOGGER.error(msg=msg)
|
|
312
|
-
|
|
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)
|
|
313
401
|
result: Response
|
|
314
402
|
if errors:
|
|
315
403
|
result = Response(response="; ".join(errors),
|
|
316
404
|
status=400)
|
|
317
405
|
else:
|
|
318
|
-
result = jsonify({"user-id":
|
|
319
|
-
"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
|
+
|
|
320
486
|
if __IAM_LOGGER:
|
|
321
487
|
# log the response (the returned data is not logged, as it contains the token)
|
|
322
488
|
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
@@ -324,19 +490,17 @@ def service_token() -> Response:
|
|
|
324
490
|
return result
|
|
325
491
|
|
|
326
492
|
|
|
327
|
-
# @flask_app.route(rule=<
|
|
328
|
-
# methods=["
|
|
329
|
-
def
|
|
493
|
+
# @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
|
|
494
|
+
# methods=["GET"])
|
|
495
|
+
def service_get_token() -> Response:
|
|
330
496
|
"""
|
|
331
|
-
Entry point for
|
|
497
|
+
Entry point for retrieving a token from the *IAM* server.
|
|
332
498
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
- 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()*.
|
|
337
502
|
|
|
338
|
-
|
|
339
|
-
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.
|
|
340
504
|
|
|
341
505
|
On success, the returned *Response* will contain the following JSON:
|
|
342
506
|
{
|
|
@@ -344,49 +508,91 @@ def service_exchange() -> Response:
|
|
|
344
508
|
"access-token": <token>
|
|
345
509
|
}
|
|
346
510
|
|
|
347
|
-
:return: *Response* containing the token
|
|
511
|
+
:return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
|
|
348
512
|
"""
|
|
513
|
+
# retrieve the request arguments
|
|
514
|
+
args: dict[str, Any] = dict(request.args) or {}
|
|
515
|
+
|
|
349
516
|
# log the request
|
|
350
517
|
if __IAM_LOGGER:
|
|
351
|
-
__IAM_LOGGER.debug(msg=
|
|
352
|
-
|
|
518
|
+
__IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
|
|
519
|
+
ensure_ascii=False)}")
|
|
353
520
|
errors: list[str] = []
|
|
521
|
+
token_info: dict[str, str] | None = None
|
|
354
522
|
with _iam_lock:
|
|
355
|
-
# retrieve the IAM server
|
|
523
|
+
# retrieve the IAM server
|
|
356
524
|
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
357
525
|
errors=errors,
|
|
358
526
|
logger=__IAM_LOGGER)
|
|
359
|
-
# exchange the token
|
|
360
|
-
token_info: tuple[str, str] | None = None
|
|
361
527
|
if iam_server:
|
|
528
|
+
# retrieve the token
|
|
362
529
|
errors: list[str] = []
|
|
363
|
-
token_info =
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
530
|
+
token_info = iam_get_token(iam_server=iam_server,
|
|
531
|
+
args=args,
|
|
532
|
+
errors=errors,
|
|
533
|
+
logger=__IAM_LOGGER)
|
|
367
534
|
result: Response
|
|
368
535
|
if errors:
|
|
369
536
|
result = Response(response="; ".join(errors),
|
|
370
537
|
status=400)
|
|
371
538
|
else:
|
|
372
|
-
result = jsonify(
|
|
373
|
-
"access-token": token_info[1]})
|
|
374
|
-
|
|
375
|
-
# log the response
|
|
539
|
+
result = jsonify(token_info)
|
|
376
540
|
if __IAM_LOGGER:
|
|
377
|
-
|
|
541
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
542
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
378
543
|
|
|
379
544
|
return result
|
|
380
545
|
|
|
381
546
|
|
|
382
|
-
|
|
547
|
+
# @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_USERINFO
|
|
548
|
+
# methods=["GET"])
|
|
549
|
+
@jwt_required
|
|
550
|
+
def service_userinfo() -> Response:
|
|
383
551
|
"""
|
|
384
|
-
|
|
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()*.
|
|
385
557
|
|
|
386
|
-
|
|
387
|
-
|
|
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*
|
|
388
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)
|
|
389
573
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|