pypomes-iam 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pypomes-iam might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -12,5 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: cachetools>=6.2.1
14
14
  Requires-Dist: flask>=3.1.2
15
+ Requires-Dist: pyjwt>=2.10.1
15
16
  Requires-Dist: pypomes-core>=2.8.0
17
+ Requires-Dist: pypomes-crypto>=0.4.8
16
18
  Requires-Dist: requests>=2.32.5
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.1.1"
9
+ version = "0.1.3"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -21,7 +21,9 @@ classifiers = [
21
21
  dependencies = [
22
22
  "cachetools>=6.2.1",
23
23
  "Flask>=3.1.2",
24
+ "PyJWT>=2.10.1",
24
25
  "pypomes-core>=2.8.0",
26
+ "pypomes-crypto>=0.4.8",
25
27
  "requests>=2.32.5"
26
28
  ]
27
29
 
@@ -4,12 +4,17 @@ from .jusbr_pomes import (
4
4
  from .provider_pomes import (
5
5
  provider_register, provider_get_token
6
6
  )
7
+ from .token_pomes import (
8
+ token_validate
9
+ )
7
10
 
8
11
  __all__ = [
9
12
  # jusbr_pomes
10
13
  "jusbr_setup", "jusbr_get_token", "jusbr_set_scope",
11
14
  # provider_pomes
12
- "provider_register", "provider_get_token"
15
+ "provider_register", "provider_get_token",
16
+ # token_pomes
17
+ "token_validate"
13
18
  ]
14
19
 
15
20
  from importlib.metadata import version
@@ -24,38 +24,33 @@ JUSBR_ENDPOINT_LOGOUT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_ENDPOIN
24
24
  JUSBR_ENDPOINT_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_ENDPOINT_TOKEN",
25
25
  def_value="/iam/jusbr:get-token")
26
26
 
27
+ JUSBR_PUBLIC_KEY_LIFETIME: Final[int] = env_get_int(key=f"{APP_PREFIX}_JUSBR_PUBLIC_KEY_LIFETIME",
28
+ def_value=86400) # 24 hours
29
+ JUSBR_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_BASE")
27
30
  JUSBR_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_CALLBACK")
28
- JUSBR_URL_AUTH_LOGIN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_LOGIN")
29
- JUSBR_URL_AUTH_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_TOKEN")
30
31
 
31
- # safe memory cache - structure:
32
+ # registry structure:
32
33
  # {
33
34
  # "client-id": <str>,
34
35
  # "client-secret": <str>,
35
- # "auth-url": <str>,
36
- # "token-url": <str>,
37
36
  # "client-timeout": <int>,
37
+ # "public_key": <str>,
38
+ # "key-expiration": <int>,
39
+ # "auth-url": <str>,
40
+ # "callback-url": <str>,
38
41
  # "users": {
39
42
  # "<user-id>": {
40
43
  # "cache-obj": <Cache>,
41
44
  # "oauth-scope": <str>,
42
45
  # "access-expiration": <timestamp>,
43
- # data in <TTLCache>:
46
+ # data in <Cache>:
44
47
  # "oauth-state": <str>
45
48
  # "access-token": <str>
46
49
  # "refresh-token": <str>
47
50
  # }
48
51
  # }
49
52
  # }
50
- _jusbr_registry: dict[str, Any] = {
51
- "client-id": None,
52
- "client-secret": None,
53
- "client-timeout": None,
54
- "auth-url": None,
55
- "callback-url": None,
56
- "token-url": None,
57
- "users": {}
58
- }
53
+ _jusbr_registry: dict[str, Any] | None = None
59
54
 
60
55
  # dafault logger
61
56
  _logger: Logger | None = None
@@ -65,13 +60,13 @@ def jusbr_setup(flask_app: Flask,
65
60
  client_id: str = JUSBR_CLIENT_ID,
66
61
  client_secret: str = JUSBR_CLIENT_SECRET,
67
62
  client_timeout: int = JUSBR_CLIENT_TIMEOUT,
63
+ public_key_lifetime: int = JUSBR_PUBLIC_KEY_LIFETIME,
68
64
  callback_endpoint: str = JUSBR_ENDPOINT_CALLBACK,
69
65
  token_endpoint: str = JUSBR_ENDPOINT_TOKEN,
70
66
  login_endpoint: str = JUSBR_ENDPOINT_LOGIN,
71
67
  logout_endpoint: str = JUSBR_ENDPOINT_LOGOUT,
72
- auth_url: str = JUSBR_URL_AUTH_LOGIN,
68
+ auth_url: str = JUSBR_URL_AUTH_BASE,
73
69
  callback_url: str = JUSBR_URL_AUTH_CALLBACK,
74
- token_url: str = JUSBR_URL_AUTH_TOKEN,
75
70
  logger: Logger = None) -> None:
76
71
  """
77
72
  Configure the JusBR IAM.
@@ -82,13 +77,13 @@ def jusbr_setup(flask_app: Flask,
82
77
  :param client_id: the client's identification with JusBR
83
78
  :param client_secret: the client's password with JusBR
84
79
  :param client_timeout: timeout for login authentication (in seconds,defaults to no timeout)
80
+ :param public_key_lifetime: how long to use JusBR's public key, before refreshing it (in seconds)
85
81
  :param callback_endpoint: endpoint for the callback from JusBR
86
82
  :param token_endpoint: endpoint for retrieving the JusBR authentication token
87
83
  :param login_endpoint: endpoint for redirecting user to JusBR login page
88
84
  :param logout_endpoint: endpoint for terminating user access to JusBR
89
- :param auth_url: URL to access the JusBR login page
85
+ :param auth_url: base URL to request the JusBR services
90
86
  :param callback_url: URL for JusBR to callback on login
91
- :param token_url: URL for obtaing or refreshing the token
92
87
  :param logger: optional logger
93
88
  """
94
89
  # establish the logger
@@ -96,15 +91,17 @@ def jusbr_setup(flask_app: Flask,
96
91
  _logger = logger
97
92
 
98
93
  # configure the JusBR registry
99
- global _jusbr_registry # noqa: PLW0602
100
- _jusbr_registry.update({
94
+ global _jusbr_registry
95
+ _jusbr_registry = {
101
96
  "client-id": client_id,
102
97
  "client-secret": client_secret,
103
98
  "client-timeout": client_timeout,
104
99
  "auth-url": auth_url,
105
100
  "callback-url": callback_url,
106
- "token-url": token_url
107
- })
101
+ "key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
102
+ "key-lifetime": public_key_lifetime,
103
+ "users": {}
104
+ }
108
105
 
109
106
  # establish the endpoints
110
107
  if token_endpoint:
@@ -135,48 +132,38 @@ def service_login() -> Response:
135
132
  """
136
133
  Entry point for the JusBR login service.
137
134
 
138
- Redirect the request to the JusBR authentication page, with the apprpriate parameters.
135
+ Redirect the request to the JusBR authentication page, with the appropriate parameters.
139
136
 
140
137
  :return: the response from the redirect operation
141
138
  """
142
139
  global _jusbr_registry
143
140
 
144
- # declare the return variable
145
- result: Response
146
-
147
- # retrieve user id
141
+ # retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state'
148
142
  input_params: dict[str, Any] = request.values
149
- user_id: str = input_params.get("user-id") or input_params.get("login")
150
-
151
- if user_id:
152
- # retrieve user data
153
- user_data: dict[str, Any] = __get_user_data(user_id=user_id,
154
- logger=_logger)
155
- # build redirect url
156
- oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
157
- timeout: int = __get_login_timeout()
158
- safe_cache: Cache
159
- if timeout:
160
- safe_cache = TTLCache(maxsize=16,
161
- ttl=600)
162
- else:
163
- safe_cache = FIFOCache(maxsize=16)
164
- safe_cache["oauth-state"] = oauth_state
165
- user_data["cache-obj"] = safe_cache
166
- auth_url: str = (f"{_jusbr_registry["auth-url"]}?response_type=code"
167
- f"&client_id={_jusbr_registry["client-id"]}"
168
- f"&redirect_url={_jusbr_registry["callback-url"]}"
169
- f"&state={oauth_state}")
170
- if user_data.get("oauth-scope"):
171
- auth_url += f"&scope={user_data.get("oauth-scope")}"
172
-
173
- # redirect request
174
- result = redirect(location=auth_url)
143
+ oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
144
+ user_id: str = input_params.get("user-id") or input_params.get("login") or oauth_state
145
+ # obtain user data
146
+ user_data: dict[str, Any] = __get_user_data(user_id=user_id,
147
+ logger=_logger)
148
+ # build redirect url
149
+ timeout: int = __get_login_timeout()
150
+ safe_cache: Cache
151
+ if timeout:
152
+ safe_cache = TTLCache(maxsize=16,
153
+ ttl=600)
175
154
  else:
176
- result = jsonify({"errors": "User id must be provided"})
177
- result.status_code = 401
155
+ safe_cache = FIFOCache(maxsize=16)
156
+ safe_cache["oauth-state"] = oauth_state
157
+ user_data["cache-obj"] = safe_cache
158
+ auth_url: str = (f"{_jusbr_registry["auth-url"]}/protocol/openid-connect/auth?response_type=code"
159
+ f"&client_id={_jusbr_registry["client-id"]}"
160
+ f"&redirect_url={_jusbr_registry["callback-url"]}"
161
+ f"&state={oauth_state}")
162
+ if user_data.get("oauth-scope"):
163
+ auth_url += f"&scope={user_data.get("oauth-scope")}"
178
164
 
179
- return result
165
+ # redirect request
166
+ return redirect(location=auth_url)
180
167
 
181
168
 
182
169
  # @flask_app.route(rule=<login_endpoint>, # JUSBR_LOGIN_ENDPOINT: /iam/jusbr:logout
@@ -213,14 +200,18 @@ def service_callback() -> Response:
213
200
  :return: the response containing the token, or *NOT AUTHORIZED*
214
201
  """
215
202
  global _jusbr_registry
203
+ from .token_pomes import token_validate
216
204
 
217
205
  # validate the OAuth2 state
218
206
  oauth_state: str = request.args.get("state")
207
+ user_id: str | None = None
219
208
  user_data: dict[str, Any] | None = None
220
209
  if oauth_state:
221
- for data in _jusbr_registry.get("users"):
222
- safe_cache: Cache = user_data.get("cache-obj")
223
- if safe_cache and oauth_state == safe_cache.get("oauth-state"):
210
+ for user, data in _jusbr_registry.get("users").items():
211
+ safe_cache: Cache = data.get("cache-obj")
212
+ if user == oauth_state or \
213
+ (safe_cache and oauth_state == safe_cache.get("oauth-state")):
214
+ user_id = user
224
215
  user_data = data
225
216
  # 'oauth-state' is to be used only once
226
217
  safe_cache["oauth-state"] = None
@@ -240,6 +231,20 @@ def service_callback() -> Response:
240
231
  body_data=body_data,
241
232
  errors=errors,
242
233
  logger=_logger)
234
+ # retrieve the token's claims
235
+ if not errors:
236
+ token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
237
+ issuer=_jusbr_registry.get("auth-url"),
238
+ public_key=_jusbr_registry.get("public_key"),
239
+ errors=errors,
240
+ logger=_logger)
241
+ if not errors:
242
+ token_user: str = token_claims["payload"].get("preferred_username")
243
+ if user_id == oauth_state:
244
+ user_id = token_user
245
+ _jusbr_registry["users"][user_id] = _jusbr_registry["users"].pop(oauth_state)
246
+ elif token_user != user_id:
247
+ errors.append(f"Token was issued to user '{token_user}'")
243
248
  else:
244
249
  msg: str = "Unknown OAuth2 code received"
245
250
  if __get_login_timeout():
@@ -251,7 +256,9 @@ def service_callback() -> Response:
251
256
  result = jsonify({"errors": "; ".join(errors)})
252
257
  result.status_code = 400
253
258
  else:
254
- result = jsonify({"access_token": token})
259
+ result = jsonify({
260
+ "user_id": user_id,
261
+ "access_token": token})
255
262
 
256
263
  return result
257
264
 
@@ -351,6 +358,42 @@ def jusbr_set_scope(user_id: str,
351
358
  logger.debug(f"Scope for user '{user_id}' set to '{scope}'")
352
359
 
353
360
 
361
+ def __get_public_key(url: str,
362
+ logger: Logger | None) -> str:
363
+ """
364
+ Obtain the public key used by JusBR to sign the authentication tokens.
365
+
366
+ :param url: the base URL to request the public key
367
+ :return: the public key, in *PEM* format
368
+ """
369
+ from pypomes_crypto import crypto_jwk_convert
370
+ global _jusbr_registry
371
+
372
+ # initialize the return variable
373
+ result: str | None = None
374
+
375
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
376
+ if now > _jusbr_registry.get("key-expiration"):
377
+ # obtain a new public key
378
+ url: str = f"{url}/protocol/openid-connect/certs"
379
+ response: requests.Response = requests.get(url=url)
380
+ if response.status_code == 200:
381
+ # request succeeded
382
+ reply: dict[str, Any] = response.json()
383
+ result = crypto_jwk_convert(jwk=reply["keys"][0],
384
+ fmt="PEM")
385
+ _jusbr_registry["public-key"] = result
386
+ duration: int = _jusbr_registry.get("key-lifetime") or 0
387
+ _jusbr_registry["key-expiration"] = now + duration
388
+ elif logger:
389
+ logger.error(msg=f"GET '{url}': failed, "
390
+ f"status {response.status_code}, reason '{response.reason}'")
391
+ else:
392
+ result = _jusbr_registry.get("public-key")
393
+
394
+ return result
395
+
396
+
354
397
  def __get_login_timeout() -> int | None:
355
398
  """
356
399
  Retrieve the timeout currently applicable for the login operation.
@@ -421,9 +464,10 @@ def __post_jusbr(user_data: dict[str, Any],
421
464
  if client_secret:
422
465
  body_data["client_secret"] = client_secret
423
466
 
467
+ # obtain the token
424
468
  err_msg: str | None = None
425
469
  safe_cache: Cache = user_data.get("cache-obj")
426
- url: str = _jusbr_registry.get("auth-url")
470
+ url: str = _jusbr_registry.get("auth-url") + "/protocol/openid-connect/token"
427
471
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
428
472
  try:
429
473
  # JusBR return on a token request:
@@ -0,0 +1,213 @@
1
+ # import requests
2
+ # import secrets
3
+ # import string
4
+ # import sys
5
+ # from cachetools import Cache, FIFOCache, TTLCache
6
+ # from datetime import datetime
7
+ from flask import Flask, Response, redirect, request, jsonify
8
+ from logging import Logger
9
+ from pypomes_core import (
10
+ APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str, exc_format
11
+ )
12
+ from typing import Any, Final
13
+
14
+ KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
15
+ KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
16
+ KEYCLOAK_CLIENT_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_TIMEOUT")
17
+
18
+ KEYCLOAK_ENDPOINT_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_CALLBACK",
19
+ def_value="/iam/keycloak:callback")
20
+ KEYCLOAK_ENDPOINT_LOGIN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGIN",
21
+ def_value="/iam/keycloak:login")
22
+ KEYCLOAK_ENDPOINT_LOGOUT: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_LOGOUT",
23
+ def_value="/iam/keycloak:logout")
24
+ KEYCLOAK_ENDPOINT_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_ENDPOINT_TOKEN",
25
+ def_value="/iam/keycloak:get-token")
26
+
27
+ KEYCLOAK_REALM: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_REALM")
28
+ KEYCLOAK_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_BASE")
29
+ KEYCLOAK_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_URL_AUTH_CALLBACK")
30
+
31
+ # registry structure:
32
+ # {
33
+ # "client-id": <str>,
34
+ # "client-secret": <str>,
35
+ # "client-timeout": <int>,
36
+ # "realm": <str>,
37
+ # "auth-url": <str>,
38
+ # "callback-url": <str>,
39
+ # "users": {
40
+ # "<user-id>": {
41
+ # "cache-obj": <Cache>,
42
+ # "oauth-scope": <str>,
43
+ # "access-expiration": <timestamp>,
44
+ # data in <Cache>:
45
+ # "oauth-state": <str>
46
+ # "access-token": <str>
47
+ # "refresh-token": <str>
48
+ # }
49
+ # }
50
+ # }
51
+ _keycloak_registry: dict[str, Any] = {
52
+ "client-id": None,
53
+ "client-secret": None,
54
+ "client-timeout": None,
55
+ "realm": None,
56
+ "auth-url": None,
57
+ "callback-url": None,
58
+ "users": {}
59
+ }
60
+
61
+ # dafault logger
62
+ _logger: Logger | None = None
63
+
64
+
65
+ def keycloak_setup(flask_app: Flask,
66
+ client_id: str = KEYCLOAK_CLIENT_ID,
67
+ client_secret: str = KEYCLOAK_CLIENT_SECRET,
68
+ client_timeout: int = KEYCLOAK_CLIENT_TIMEOUT,
69
+ realm: str = KEYCLOAK_REALM,
70
+ callback_endpoint: str = KEYCLOAK_ENDPOINT_CALLBACK,
71
+ token_endpoint: str = KEYCLOAK_ENDPOINT_TOKEN,
72
+ login_endpoint: str = KEYCLOAK_ENDPOINT_LOGIN,
73
+ logout_endpoint: str = KEYCLOAK_ENDPOINT_LOGOUT,
74
+ auth_url: str = KEYCLOAK_URL_AUTH_BASE,
75
+ callback_url: str = KEYCLOAK_URL_AUTH_CALLBACK,
76
+ logger: Logger = None) -> None:
77
+ """
78
+ Configure the Keycloak IAM.
79
+
80
+ This should be invoked only once, before the first access to a Keycloak service.
81
+
82
+ :param flask_app: the Flask application
83
+ :param client_id: the client's identification with JusBR
84
+ :param client_secret: the client's password with JusBR
85
+ :param client_timeout: timeout for login authentication (in seconds,defaults to no timeout)
86
+ :param realm: the Keycloak reals
87
+ :param callback_endpoint: endpoint for the callback from JusBR
88
+ :param token_endpoint: endpoint for retrieving the JusBR authentication token
89
+ :param login_endpoint: endpoint for redirecting user to JusBR login page
90
+ :param logout_endpoint: endpoint for terminating user access to JusBR
91
+ :param auth_url: base URL to request the JusBR services
92
+ :param callback_url: URL for Keycloak to callback on login
93
+ :param logger: optional logger
94
+ """
95
+ global _keycloak_registry
96
+
97
+ # establish the logger
98
+ global _logger
99
+ _logger = logger
100
+
101
+ # configure the JusBR registry
102
+ _keycloak_registry.update({
103
+ "client-id": client_id,
104
+ "client-secret": client_secret,
105
+ "client-timeout": client_timeout,
106
+ "realm": realm,
107
+ "auth-url": auth_url,
108
+ "callback-url": callback_url,
109
+ "users": []
110
+ })
111
+
112
+ # establish the endpoints
113
+ if token_endpoint:
114
+ flask_app.add_url_rule(rule=token_endpoint,
115
+ endpoint="keycloak-token",
116
+ view_func=service_token,
117
+ methods=["GET"])
118
+ if login_endpoint:
119
+ flask_app.add_url_rule(rule=login_endpoint,
120
+ endpoint="keycloak-login",
121
+ view_func=service_login,
122
+ methods=["GET"])
123
+ if logout_endpoint:
124
+ flask_app.add_url_rule(rule=logout_endpoint,
125
+ endpoint="keycloak-logout",
126
+ view_func=service_logout,
127
+ methods=["GET"])
128
+ if callback_endpoint:
129
+ flask_app.add_url_rule(rule=callback_endpoint,
130
+ endpoint="keycloak-callback",
131
+ view_func=service_callback,
132
+ methods=["POST"])
133
+
134
+
135
+ # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:login
136
+ # methods=["GET"])
137
+ def service_login() -> Response:
138
+ """
139
+ Entry point for the Keycloak login service.
140
+
141
+ Redirect the request to the Keycloak authentication page, with the appropriate parameters.
142
+
143
+ :return: the response from the redirect operation
144
+ """
145
+ global _keycloak_registry
146
+
147
+ # retrieve user id
148
+ input_params: dict[str, Any] = request.args
149
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
150
+ return Response()
151
+
152
+
153
+ # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:logout
154
+ # methods=["GET"])
155
+ def service_logout() -> Response:
156
+ """
157
+ Entry point for the JusBR logout service.
158
+
159
+ Remove all data associating the user with JusBR from the registry.
160
+
161
+ :return: response *OK*
162
+ """
163
+ global _keycloak_registry
164
+
165
+ # retrieve user id
166
+ input_params: dict[str, Any] = request.args
167
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
168
+ return Response()
169
+
170
+
171
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_CALLBACK_ENDPOINT: /iam/keycloak:callback
172
+ # methods=["POST"])
173
+ def service_callback() -> Response:
174
+ """
175
+ Entry point for the callback from Keycloak on authentication operation.
176
+
177
+ :return: the response containing the token, or *NOT AUTHORIZED*
178
+ """
179
+ global _keycloak_registry
180
+ return Response()
181
+
182
+
183
+ # @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
184
+ # methods=["GET"])
185
+ def service_token() -> Response:
186
+ """
187
+ Entry point for retrieving the Keycloak token.
188
+
189
+ :return: the response containing the token, or *UNAUTHORIZED*
190
+ """
191
+ # retrieve user id
192
+ input_params: dict[str, Any] = request.args
193
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
194
+ return Response()
195
+
196
+
197
+ def keycloak_get_token(user_id: str,
198
+ errors: list[str] = None,
199
+ logger: Logger = None) -> str:
200
+ """
201
+ Retrieve the authentication token for user *user_id*.
202
+
203
+ :param user_id: the user's identification
204
+ :param errors: incidental error messages
205
+ :param logger: optional logger
206
+ :return: the token for *user_id*, or *None* if error
207
+ """
208
+ global _keycloak_registry
209
+
210
+ # initialize the return variable
211
+ result: str | None = None
212
+ return result
213
+
@@ -0,0 +1,101 @@
1
+ import jwt
2
+ import sys
3
+ from jwt import PyJWK
4
+ from jwt.algorithms import RSAPublicKey
5
+ from logging import Logger
6
+ from pypomes_core import exc_format
7
+ from typing import Any
8
+
9
+
10
+ def token_validate(token: str,
11
+ issuer: str = None,
12
+ public_key: str | bytes | PyJWK | RSAPublicKey = None,
13
+ errors: list[str] = None,
14
+ logger: Logger = None) -> dict[str, dict[str, Any]] | None:
15
+ """
16
+ Verify whether *token* is a valid JWT token, and return its claims (sections *header* and *payload*).
17
+
18
+ The supported public key types are:
19
+ - *DER*: Distinguished Encoding Rules (bytes)
20
+ - *PEM*: Privacy-Enhanced Mail (str)
21
+ - *PyJWK*: a formar from the *PyJWT* package
22
+ - *RSAPublicKey*: a format from the *PyJWT* package
23
+
24
+ If an asymmetric algorithm was used to sign the token and *public_key* is provided, then
25
+ the token is validated, by using the data in its *signature* section.
26
+
27
+ On failure, *errors* will contain the reason(s) for rejecting *token*.
28
+ On success, return the token's claims (*header* and *payload*).
29
+
30
+ :param token: the token to be validated
31
+ :param public_key: optional public key used to sign the token, in *PEM* format
32
+ :param issuer: optional value to compare with the token's *iss* (issuer) attribute in its *payload*
33
+ :param errors: incidental error messages
34
+ :param logger: optional logger
35
+ :return: The token's claims (*header* and *payload*) if it is valid, *None* otherwise
36
+ """
37
+ # initialize the return variable
38
+ result: dict[str, dict[str, Any]] | None = None
39
+
40
+ if logger:
41
+ logger.debug(msg="Validate JWT token")
42
+
43
+ # make sure to have an errors list
44
+ if not isinstance(errors, list):
45
+ errors = []
46
+
47
+ # extract needed data from token header
48
+ token_header: dict[str, Any] | None = None
49
+ try:
50
+ token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
51
+ except Exception as e:
52
+ exc_err: str = exc_format(exc=e,
53
+ exc_info=sys.exc_info())
54
+ if logger:
55
+ logger.error(msg=f"Error retrieving the token's header: {exc_err}")
56
+ errors.append(exc_err)
57
+
58
+ # validate the token
59
+ if not errors:
60
+ token_alg: str = token_header.get("alg")
61
+ options: dict[str, Any] = {
62
+ "require": ["exp", "iat"],
63
+ "verify_aud": False,
64
+ "verify_exp": True,
65
+ "verify_iat": True,
66
+ "verify_iss": issuer is not None,
67
+ "verify_nbf": False,
68
+ "verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
69
+ }
70
+ if issuer:
71
+ options["require"].append("iss")
72
+ try:
73
+ # raises:
74
+ # InvalidTokenError: token is invalid
75
+ # InvalidKeyError: authentication key is not in the proper format
76
+ # ExpiredSignatureError: token and refresh period have expired
77
+ # InvalidSignatureError: signature does not match the one provided as part of the token
78
+ # ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
79
+ # InvalidAlgorithmError: the specified algorithm is not recognized
80
+ # InvalidIssuedAtError: 'iat' claim is non-numeric
81
+ # MissingRequiredClaimError: a required claim is not contained in the claimset
82
+ payload: dict[str, Any] = jwt.decode(jwt=token,
83
+ key=public_key,
84
+ algorithms=[token_alg],
85
+ options=options,
86
+ issuer=issuer)
87
+ result = {
88
+ "header": token_header,
89
+ "payload": payload
90
+ }
91
+ except Exception as e:
92
+ exc_err: str = exc_format(exc=e,
93
+ exc_info=sys.exc_info())
94
+ if logger:
95
+ logger.error(msg=f"Error decoding the token: {exc_err}")
96
+ errors.append(exc_err)
97
+
98
+ if not errors and logger:
99
+ logger.debug(msg="Token is valid")
100
+
101
+ return result
File without changes
File without changes
File without changes