pypomes-iam 0.1.1__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.1.1
3
+ Version: 0.1.2
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.2"
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
@@ -97,14 +92,15 @@ def jusbr_setup(flask_app: Flask,
97
92
 
98
93
  # configure the JusBR registry
99
94
  global _jusbr_registry # noqa: PLW0602
100
- _jusbr_registry.update({
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
+ }
108
104
 
109
105
  # establish the endpoints
110
106
  if token_endpoint:
@@ -135,48 +131,38 @@ def service_login() -> Response:
135
131
  """
136
132
  Entry point for the JusBR login service.
137
133
 
138
- Redirect the request to the JusBR authentication page, with the apprpriate parameters.
134
+ Redirect the request to the JusBR authentication page, with the appropriate parameters.
139
135
 
140
136
  :return: the response from the redirect operation
141
137
  """
142
138
  global _jusbr_registry
143
139
 
144
- # declare the return variable
145
- result: Response
146
-
147
- # retrieve user id
140
+ # retrieve user data (if not provided, 'user_id' is temporarily set to 'oauth_state'
148
141
  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)
142
+ oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
143
+ user_id: str = input_params.get("user-id") or input_params.get("login") or oauth_state
144
+ # obtain user data
145
+ user_data: dict[str, Any] = __get_user_data(user_id=user_id or oauth_state,
146
+ logger=_logger)
147
+ # build redirect url
148
+ timeout: int = __get_login_timeout()
149
+ safe_cache: Cache
150
+ if timeout:
151
+ safe_cache = TTLCache(maxsize=16,
152
+ ttl=600)
175
153
  else:
176
- result = jsonify({"errors": "User id must be provided"})
177
- result.status_code = 401
154
+ safe_cache = FIFOCache(maxsize=16)
155
+ safe_cache["oauth-state"] = oauth_state
156
+ user_data["cache-obj"] = safe_cache
157
+ auth_url: str = (f"{_jusbr_registry["auth-url"]}/protocol/openid-connect/auth?response_type=code"
158
+ f"&client_id={_jusbr_registry["client-id"]}"
159
+ f"&redirect_url={_jusbr_registry["callback-url"]}"
160
+ f"&state={oauth_state}")
161
+ if user_data.get("oauth-scope"):
162
+ auth_url += f"&scope={user_data.get("oauth-scope")}"
178
163
 
179
- return result
164
+ # redirect request
165
+ return redirect(location=auth_url)
180
166
 
181
167
 
182
168
  # @flask_app.route(rule=<login_endpoint>, # JUSBR_LOGIN_ENDPOINT: /iam/jusbr:logout
@@ -213,14 +199,18 @@ def service_callback() -> Response:
213
199
  :return: the response containing the token, or *NOT AUTHORIZED*
214
200
  """
215
201
  global _jusbr_registry
202
+ from .token_pomes import token_validate
216
203
 
217
204
  # validate the OAuth2 state
218
205
  oauth_state: str = request.args.get("state")
206
+ user_id: str | None = None
219
207
  user_data: dict[str, Any] | None = None
220
208
  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"):
209
+ for user, data in _jusbr_registry.get("users").items():
210
+ safe_cache: Cache = data.get("cache-obj")
211
+ if user == oauth_state or \
212
+ (safe_cache and oauth_state == safe_cache.get("oauth-state")):
213
+ user_id = user
224
214
  user_data = data
225
215
  # 'oauth-state' is to be used only once
226
216
  safe_cache["oauth-state"] = None
@@ -240,6 +230,20 @@ def service_callback() -> Response:
240
230
  body_data=body_data,
241
231
  errors=errors,
242
232
  logger=_logger)
233
+ # retrieve the token's claims
234
+ if not errors:
235
+ token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
236
+ issuer=_jusbr_registry.get("auth-url"),
237
+ public_key=_jusbr_registry.get("public_key"),
238
+ errors=errors,
239
+ logger=_logger)
240
+ if not errors:
241
+ token_user: str = token_claims["payload"].get("preferred_username")
242
+ if user_id == oauth_state:
243
+ user_id = token_user
244
+ _jusbr_registry["users"][user_id] = _jusbr_registry["users"].pop(oauth_state)
245
+ elif token_user != user_id:
246
+ errors.append(f"Token was issued to user '{token_user}'")
243
247
  else:
244
248
  msg: str = "Unknown OAuth2 code received"
245
249
  if __get_login_timeout():
@@ -251,7 +255,9 @@ def service_callback() -> Response:
251
255
  result = jsonify({"errors": "; ".join(errors)})
252
256
  result.status_code = 400
253
257
  else:
254
- result = jsonify({"access_token": token})
258
+ result = jsonify({
259
+ "user_id": user_id,
260
+ "access_token": token})
255
261
 
256
262
  return result
257
263
 
@@ -351,6 +357,42 @@ def jusbr_set_scope(user_id: str,
351
357
  logger.debug(f"Scope for user '{user_id}' set to '{scope}'")
352
358
 
353
359
 
360
+ def __get_public_key(url: str,
361
+ logger: Logger | None) -> str:
362
+ """
363
+ Obtain the public key used by JusBR to sign the authentication tokens.
364
+
365
+ :param url: the base URL to request the public key
366
+ :return: the public key, in *PEM* format
367
+ """
368
+ from pypomes_crypto import crypto_jwk_convert
369
+ global _jusbr_registry
370
+
371
+ # initialize the return variable
372
+ result: str | None = None
373
+
374
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
375
+ if now > _jusbr_registry.get("key-expiration"):
376
+ # obtain a new public key
377
+ url: str = f"{url}/protocol/openid-connect/certs"
378
+ response: requests.Response = requests.get(url=url)
379
+ if response.status_code == 200:
380
+ # request succeeded
381
+ reply: dict[str, Any] = response.json()
382
+ result = crypto_jwk_convert(jwk=reply["keys"][0],
383
+ fmt="PEM")
384
+ _jusbr_registry["public-key"] = result
385
+ duration: int = _jusbr_registry.get("key-lifetime") or 0
386
+ _jusbr_registry["key-expiration"] = now + duration
387
+ elif logger:
388
+ logger.error(msg=f"GET '{url}': failed, "
389
+ f"status {response.status_code}, reason '{response.reason}'")
390
+ else:
391
+ result = _jusbr_registry.get("public-key")
392
+
393
+ return result
394
+
395
+
354
396
  def __get_login_timeout() -> int | None:
355
397
  """
356
398
  Retrieve the timeout currently applicable for the login operation.
@@ -421,9 +463,10 @@ def __post_jusbr(user_data: dict[str, Any],
421
463
  if client_secret:
422
464
  body_data["client_secret"] = client_secret
423
465
 
466
+ # obtain the token
424
467
  err_msg: str | None = None
425
468
  safe_cache: Cache = user_data.get("cache-obj")
426
- url: str = _jusbr_registry.get("auth-url")
469
+ url: str = _jusbr_registry.get("auth-url") + "/protocol/openid-connect/token"
427
470
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
428
471
  try:
429
472
  # JusBR return on a token request:
@@ -0,0 +1,212 @@
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 # noqa: PLW0602
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
+ })
110
+
111
+ # establish the endpoints
112
+ if token_endpoint:
113
+ flask_app.add_url_rule(rule=token_endpoint,
114
+ endpoint="keycloak-token",
115
+ view_func=service_token,
116
+ methods=["GET"])
117
+ if login_endpoint:
118
+ flask_app.add_url_rule(rule=login_endpoint,
119
+ endpoint="keycloak-login",
120
+ view_func=service_login,
121
+ methods=["GET"])
122
+ if logout_endpoint:
123
+ flask_app.add_url_rule(rule=logout_endpoint,
124
+ endpoint="keycloak-logout",
125
+ view_func=service_logout,
126
+ methods=["GET"])
127
+ if callback_endpoint:
128
+ flask_app.add_url_rule(rule=callback_endpoint,
129
+ endpoint="keycloak-callback",
130
+ view_func=service_callback,
131
+ methods=["POST"])
132
+
133
+
134
+ # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:login
135
+ # methods=["GET"])
136
+ def service_login() -> Response:
137
+ """
138
+ Entry point for the Keycloak login service.
139
+
140
+ Redirect the request to the Keycloak authentication page, with the appropriate parameters.
141
+
142
+ :return: the response from the redirect operation
143
+ """
144
+ global _keycloak_registry
145
+
146
+ # retrieve user id
147
+ input_params: dict[str, Any] = request.args
148
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
149
+ return Response()
150
+
151
+
152
+ # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:logout
153
+ # methods=["GET"])
154
+ def service_logout() -> Response:
155
+ """
156
+ Entry point for the JusBR logout service.
157
+
158
+ Remove all data associating the user with JusBR from the registry.
159
+
160
+ :return: response *OK*
161
+ """
162
+ global _keycloak_registry
163
+
164
+ # retrieve user id
165
+ input_params: dict[str, Any] = request.args
166
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
167
+ return Response()
168
+
169
+
170
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_CALLBACK_ENDPOINT: /iam/keycloak:callback
171
+ # methods=["POST"])
172
+ def service_callback() -> Response:
173
+ """
174
+ Entry point for the callback from Keycloak on authentication operation.
175
+
176
+ :return: the response containing the token, or *NOT AUTHORIZED*
177
+ """
178
+ global _keycloak_registry
179
+ return Response()
180
+
181
+
182
+ # @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
183
+ # methods=["GET"])
184
+ def service_token() -> Response:
185
+ """
186
+ Entry point for retrieving the Keycloak token.
187
+
188
+ :return: the response containing the token, or *UNAUTHORIZED*
189
+ """
190
+ # retrieve user id
191
+ input_params: dict[str, Any] = request.args
192
+ _user_id: str = input_params.get("user-id") or input_params.get("login")
193
+ return Response()
194
+
195
+
196
+ def keycloak_get_token(user_id: str,
197
+ errors: list[str] = None,
198
+ logger: Logger = None) -> str:
199
+ """
200
+ Retrieve the authentication token for user *user_id*.
201
+
202
+ :param user_id: the user's identification
203
+ :param errors: incidental error messages
204
+ :param logger: optional logger
205
+ :return: the token for *user_id*, or *None* if error
206
+ """
207
+ global _keycloak_registry
208
+
209
+ # initialize the return variable
210
+ result: str | None = None
211
+ return result
212
+
@@ -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