jaaql-middleware-python 4.26.2__tar.gz → 4.27.1__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.
Files changed (76) hide show
  1. {jaaql-middleware-python-4.26.2/jaaql_middleware_python.egg-info → jaaql-middleware-python-4.27.1}/PKG-INFO +7 -6
  2. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/constants.py +2 -1
  3. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/documentation/documentation_internal.py +59 -17
  4. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/base_controller.py +6 -1
  5. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/base_model.py +30 -0
  6. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/controller.py +5 -1
  7. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/model.py +172 -19
  8. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/utils_no_project_imports.py +12 -0
  9. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1/jaaql_middleware_python.egg-info}/PKG-INFO +7 -6
  10. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql_middleware_python.egg-info/requires.txt +6 -5
  11. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/LICENSE.txt +0 -0
  12. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/README.md +0 -0
  13. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/__init__.py +0 -0
  14. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/config/__init__.py +0 -0
  15. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/config/config-docker.ini +0 -0
  16. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/config/config-test.ini +0 -0
  17. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/config/config.ini +0 -0
  18. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/config_constants.py +0 -0
  19. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/db/__init__.py +0 -0
  20. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/db/db_interface.py +0 -0
  21. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/db/db_pg_interface.py +0 -0
  22. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/db/db_utils.py +0 -0
  23. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/db/db_utils_no_circ.py +0 -0
  24. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/documentation/__init__.py +0 -0
  25. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/documentation/documentation_public.py +0 -0
  26. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/documentation/documentation_shared.py +0 -0
  27. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/email/__init__.py +0 -0
  28. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/email/email_manager.py +0 -0
  29. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/email/email_manager_service.py +0 -0
  30. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/email/patch_ems.py +0 -0
  31. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/exceptions/__init__.py +0 -0
  32. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/exceptions/custom_http_status.py +0 -0
  33. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/exceptions/http_status_exception.py +0 -0
  34. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +0 -0
  35. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
  36. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/generated_constants.py +0 -0
  37. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/interpreter/__init__.py +0 -0
  38. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/interpreter/interpret_jaaql.py +0 -0
  39. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/jaaql.py +0 -0
  40. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/migrations/__init__.py +0 -0
  41. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/migrations/migrations.py +0 -0
  42. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/__init__.py +0 -0
  43. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/controller_interface.py +0 -0
  44. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/exception_queries.py +0 -0
  45. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/generated_queries.py +0 -0
  46. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/handmade_queries.py +0 -0
  47. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/model_interface.py +0 -0
  48. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/mvc/response.py +0 -0
  49. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/openapi/__init__.py +0 -0
  50. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/openapi/swagger_documentation.py +0 -0
  51. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/patch.py +0 -0
  52. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/01.install_domains.generated.sql +0 -0
  53. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/02.install_super_user.exceptions.sql +0 -0
  54. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/03.install_super_user.handwritten.sql +0 -0
  55. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +0 -0
  56. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
  57. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
  58. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
  59. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
  60. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/scripts/swagger_template.html +0 -0
  61. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/__init__.py +0 -0
  62. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/cached_canned_query_service.py +0 -0
  63. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/migrations_manager_service.py +0 -0
  64. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/patch_mms.py +0 -0
  65. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/patch_shared_var_service.py +0 -0
  66. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/services/shared_var_service.py +0 -0
  67. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/__init__.py +0 -0
  68. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/crypt_utils.py +0 -0
  69. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/options.py +0 -0
  70. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/utils.py +0 -0
  71. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql/utilities/vault.py +0 -0
  72. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql_middleware_python.egg-info/SOURCES.txt +0 -0
  73. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
  74. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
  75. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/setup.cfg +0 -0
  76. {jaaql-middleware-python-4.26.2 → jaaql-middleware-python-4.27.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jaaql-middleware-python
3
- Version: 4.26.2
3
+ Version: 4.27.1
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -8,7 +8,7 @@ Author-email: aaron.tasker@sqmi.nl
8
8
  License: Mozilla Public License Version 2.0 with Commons Clause
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
- Requires-Dist: jaaql-monitor~=1.4.17
11
+ Requires-Dist: jaaql-monitor~=1.6.3
12
12
  Requires-Dist: psycopg[binary]~=3.1.18
13
13
  Requires-Dist: Pillow~=10.3.0
14
14
  Requires-Dist: cryptography~=42.0.5
@@ -21,14 +21,15 @@ Requires-Dist: werkzeug~=3.0.1
21
21
  Requires-Dist: argon2-cffi~=23.1.0
22
22
  Requires-Dist: pyotp~=2.9.0
23
23
  Requires-Dist: qrcode~=7.4.2
24
- Requires-Dist: gunicorn~=21.2.0
24
+ Requires-Dist: gunicorn~=23.0.0
25
25
  Requires-Dist: gevent~=24.2.1
26
- Requires-Dist: requests~=2.31.0
26
+ Requires-Dist: requests~=2.32.3
27
27
  Requires-Dist: pyzbar~=0.1.9
28
- Requires-Dist: parsys-requests-unixsocket~=0.3.1
28
+ Requires-Dist: parsys-requests-unixsocket~=0.3.2
29
29
  Requires-Dist: selenium~=4.18.1
30
30
  Requires-Dist: twine~=5.0.0
31
- Requires-Dist: urllib3~=2.2.1
31
+ Requires-Dist: urllib3~=2.3.0
32
+ Requires-Dist: jwcrypto~=1.5.6
32
33
 
33
34
  # JAAQL-middleware-python
34
35
  Please navigate to docker/docker.md to see setup instructions
@@ -164,6 +164,7 @@ ENDPOINT__report_sentinel_error = "/sentinel/reporting/error"
164
164
  ENDPOINT__install = "/internal/install"
165
165
  ENDPOINT__set_shared_var = "/set-shared-var"
166
166
  ENDPOINT__get_shared_var = "/get-shared-var"
167
+ ENDPOINT__oidc_get_token = "/exchange-auth-code"
167
168
 
168
169
  CONFIG__default = "Default config"
169
170
  CONFIG__default_desc = "Default config description"
@@ -182,5 +183,5 @@ ROLE__postgres = "postgres"
182
183
 
183
184
  PROTOCOL__postgres = "postgresql://"
184
185
 
185
- VERSION = "4.26.2"
186
+ VERSION = "4.27.1"
186
187
 
@@ -403,24 +403,66 @@ DOCUMENTATION__oidc_exchange_code = SwaggerDocumentation(
403
403
  methods=SwaggerMethod(
404
404
  name="Fetch OIDC code",
405
405
  description="Exchanges OIDC auth code for auth token, returns the token",
406
- method=REST__POST,
407
- body=[
408
- SwaggerArgumentResponse(
409
- name=KEY__code,
410
- description="The OIDC Auth code",
411
- arg_type=str,
412
- example=["SplxlOBeZQQYbYS6WxSbIA"]
413
- ),
414
- SwaggerArgumentResponse(
415
- name=KEY__state,
416
- description="The state",
417
- arg_type=str,
418
- example=["SplxlOBeZQQYbYS6WxSbIA"]
419
- )
420
- ],
406
+ method=REST__GET,
407
+ arguments=SwaggerArgumentResponse(
408
+ name="response",
409
+ description="The OIDC response JWT object",
410
+ arg_type=str,
411
+ example=["eyJ..."]
412
+ ),
421
413
  response=SwaggerFlatResponse(
422
- description="Access token",
423
- body="eyJ..."
414
+ description="URL",
415
+ code=HTTPStatus.FOUND,
416
+ body="You are being redirected back to your place in the app..."
424
417
  )
425
418
  )
426
419
  )
420
+
421
+ DOCUMENTATION__jwks = SwaggerDocumentation(
422
+ tags="jwks",
423
+ security=False,
424
+ methods=SwaggerMethod(
425
+ name="Fetch JWKS",
426
+ description="Fetches the JWKS so that mTLS can be used with JAAQL",
427
+ method=REST__GET,
428
+ response=SwaggerResponse(
429
+ description="The Keys",
430
+ response=SwaggerArgumentResponse(
431
+ name="keys",
432
+ description="A list of keys",
433
+ arg_type=SwaggerList(
434
+ SwaggerArgumentResponse(
435
+ name="e",
436
+ description="JWKS e",
437
+ arg_type=str,
438
+ example=["AQAB"]
439
+ ),
440
+ SwaggerArgumentResponse(
441
+ name="kty",
442
+ description="JWKS kty",
443
+ arg_type=str,
444
+ example=["RSA"]
445
+ ),
446
+ SwaggerArgumentResponse(
447
+ name="n",
448
+ description="The public key",
449
+ arg_type=str,
450
+ example=["tKiq..."]
451
+ ),
452
+ SwaggerArgumentResponse(
453
+ name="kid",
454
+ description="The unique id",
455
+ arg_type=str,
456
+ example=["tKiq..."]
457
+ ),
458
+ SwaggerArgumentResponse(
459
+ name="alg",
460
+ description="The algorithm",
461
+ arg_type=str,
462
+ example=["RS256"]
463
+ )
464
+ )
465
+ )
466
+ )
467
+ )
468
+ )
@@ -15,7 +15,7 @@ import sys
15
15
  import dataclasses
16
16
  import decimal
17
17
  from queue import Queue
18
- from jaaql.utilities.utils_no_project_imports import get_cookie_attrs, COOKIE_JAAQL_AUTH
18
+ from jaaql.utilities.utils_no_project_imports import get_cookie_attrs, COOKIE_JAAQL_AUTH, COOKIE_LOGIN_MARKER, COOKIE_ATTR_PATH
19
19
  from jaaql.utilities.utils import time_delta_ms, Profiler
20
20
  from flask import Response, Flask, request, jsonify, current_app
21
21
  from flask.json.provider import DefaultJSONProvider
@@ -676,6 +676,11 @@ class BaseJAAQLController:
676
676
  get_cookie_attrs(self.model.vigilant_sessions, remember_me, self.model.is_container),
677
677
  self.model.is_https))
678
678
 
679
+ if request.cookies.get(COOKIE_LOGIN_MARKER) is not None:
680
+ resp.headers.add("Set-Cookie", format_cookie(COOKIE_LOGIN_MARKER, "",
681
+ {COOKIE_ATTR_EXPIRES: format_date_time(0), COOKIE_ATTR_PATH: "/"},
682
+ self.model.is_https))
683
+
679
684
  for _, cookie in jaaql_resp.cookies.items():
680
685
  resp.headers.add("Set-Cookie", cookie)
681
686
 
@@ -1,5 +1,9 @@
1
1
  import sys
2
2
 
3
+ import json
4
+ import requests
5
+
6
+ from cryptography import x509
3
7
  from jaaql.utilities.vault import Vault, DIR__vault
4
8
  from jaaql.db.db_interface import DBInterface
5
9
  import jaaql.utilities.crypt_utils as crypt_utils
@@ -202,6 +206,25 @@ class BaseJAAQLModel:
202
206
  self.force_mfa = config[CONFIG_KEY__security][CONFIG_KEY_SECURITY__force_mfa]
203
207
  self.do_audit = config[CONFIG_KEY__security][CONFIG_KEY_SECURITY__do_audit]
204
208
 
209
+ self.jwks = None
210
+ if self.is_container:
211
+ with open('/tmp/jwks.json', 'r') as f:
212
+ self.jwks = json.load(f)
213
+
214
+ self.application_url = os.environ.get("SERVER_ADDRESS", "")
215
+ self.use_fapi_advanced = os.environ.get("USE_FAPI_ADVANCED", "").lower() == "true"
216
+
217
+ self.fapi_pem = None
218
+ self.fapi_cert = None
219
+ if self.is_container:
220
+ with open('/tmp/client_key.pem', "rb") as f:
221
+ self.fapi_pem = f.read()
222
+
223
+ if self.use_fapi_advanced:
224
+ with open(f"/etc/letsencrypt/live/{self.application_url}/fullchain.pem", "rb") as f:
225
+ self.fapi_cert = f.read()
226
+ self.fapi_cert = x509.load_pem_x509_certificate(self.fapi_cert)
227
+
205
228
  self.vault = Vault(vault_key, DIR__vault)
206
229
  self.jaaql_lookup_connection = None
207
230
  self.email_manager = EmailManager(self.is_container)
@@ -260,9 +283,16 @@ class BaseJAAQLModel:
260
283
  install_key_file.write(self.install_key)
261
284
  print("INSTALL KEY: " + self.install_key, file=sys.stderr) # Print to stderr as unbuffered
262
285
 
286
+ self.idp_session = requests.Session()
287
+
263
288
  def get_db_crypt_key(self):
264
289
  return self.vault.get_obj(VAULT_KEY__db_crypt_key).encode(crypt_utils.ENCODING__ascii)
265
290
 
291
+ def reload_fapi_cert(self):
292
+ with open(f"/etc/letsencrypt/live/{self.application_url}/fullchain.pem", "rb") as f:
293
+ self.fapi_cert = f.read()
294
+ self.fapi_cert = x509.load_pem_x509_certificate(self.fapi_cert)
295
+
266
296
  def get_vault_repeatable_salt(self):
267
297
  return self.vault.get_obj(VAULT_KEY__db_repeatable_salt)
268
298
 
@@ -163,6 +163,10 @@ class JAAQLController(BaseJAAQLController):
163
163
  def fetch_redirect_url(http_inputs: dict, response: JAAQLResponse):
164
164
  self.model.fetch_redirect_uri(http_inputs, response)
165
165
 
166
- @self.publish_route('/exchange-auth-code', DOCUMENTATION__oidc_exchange_code)
166
+ @self.publish_route(ENDPOINT__oidc_get_token, DOCUMENTATION__oidc_exchange_code)
167
167
  def exchange_auth_code(http_inputs: dict, ip_address: str, response: JAAQLResponse):
168
168
  self.model.exchange_auth_code(http_inputs, request.cookies.get(COOKIE_OIDC), ip_address, response)
169
+
170
+ @self.publish_route('/.well-known/jwks', DOCUMENTATION__jwks)
171
+ def fetch_jwks():
172
+ return self.model.fetch_jwks()
@@ -6,6 +6,7 @@ import traceback
6
6
  import urllib.parse
7
7
  import uuid
8
8
  import secrets
9
+ from cryptography.hazmat.primitives import hashes
9
10
 
10
11
  import re
11
12
 
@@ -26,7 +27,8 @@ from jaaql.utilities.utils import get_jaaql_root, get_base_url
26
27
  from jaaql.db.db_utils import create_interface, jaaql__encrypt, create_interface_for_db, jaaql__decrypt
27
28
  from jaaql.db.db_utils_no_circ import submit, get_required_db, objectify
28
29
  from jaaql.utilities import crypt_utils
29
- from jaaql.utilities.utils_no_project_imports import get_cookie_attrs, COOKIE_JAAQL_AUTH, COOKIE_ATTR_EXPIRES, time_delta_ms, COOKIE_OIDC
30
+ from jaaql.utilities.utils_no_project_imports import get_cookie_attrs, COOKIE_JAAQL_AUTH, COOKIE_OIDC, COOKIE_LOGIN_MARKER, \
31
+ get_sloppy_cookie_attrs
30
32
  from jaaql.mvc.response import *
31
33
  import threading
32
34
  from datetime import datetime, timedelta
@@ -509,13 +511,19 @@ WHERE
509
511
  challenge = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
510
512
  return challenge
511
513
 
514
+ def fetch_jwks(self):
515
+ return self.jwks
516
+
517
+ def get_cert_thumbprint(self) -> str:
518
+ fingerprint = self.fapi_cert.fingerprint(hashes.SHA256())
519
+ return base64.urlsafe_b64encode(fingerprint).rstrip(b'=').decode('utf-8')
520
+
512
521
  def exchange_auth_code(self, inputs: dict, oidc_cookie: str, ip_address: str, response: JAAQLResponse):
513
522
  response.delete_cookie(COOKIE_OIDC, self.is_https)
514
523
  oidc_state = crypt_utils.jwt_decode(self.vault.get_obj(VAULT_KEY__jwt_crypt_key), oidc_cookie, JWT_PURPOSE__oidc)
515
- if inputs[KEY__state] != oidc_state.get('state'):
516
- raise UserUnauthorized()
517
524
 
518
525
  application = oidc_state[KEY__application]
526
+ application_tuple = application__select(self.jaaql_lookup_connection, application)
519
527
  provider = oidc_state[KG__user_registry__provider]
520
528
  tenant = oidc_state[KG__user_registry__tenant]
521
529
  database = jaaql__decrypt(oidc_state["database"], self.get_db_crypt_key())
@@ -538,27 +546,71 @@ WHERE
538
546
  if not allowed_algs:
539
547
  raise Exception(f"No allowed algs for {provider}, {tenant}")
540
548
 
549
+ jarms_response = inputs["response"]
550
+ signing_key = jwk_client.get_signing_key_from_jwt(jarms_response)
551
+ try:
552
+ jarms_payload = jwt.decode(
553
+ jarms_response,
554
+ signing_key.key,
555
+ algorithms=allowed_algs,
556
+ audience=database_user_registry[KG__database_user_registry__client_id],
557
+ issuer=expected_issuer
558
+ )
559
+ except:
560
+ raise UserUnauthorized()
561
+
562
+ if jarms_payload[KEY__state] != oidc_state.get('state'):
563
+ raise UserUnauthorized()
564
+
541
565
  token_request_payload = {
542
566
  'grant_type': 'authorization_code',
543
- 'code': inputs[KEY__code],
544
- 'redirect_uri': oidc_state["redirect_uri"],
567
+ 'code': jarms_payload[KEY__code],
568
+ 'redirect_uri': application_tuple[KG__application__base_url] + "/api" + ENDPOINT__oidc_get_token,
545
569
  'client_id': database_user_registry[KG__database_user_registry__client_id],
546
- 'code_verifier': code_verifier,
570
+ 'code_verifier': code_verifier
547
571
  }
548
- if database_user_registry[KG__database_user_registry__client_secret]:
549
- token_request_payload["client_secret"] = database_user_registry[KG__database_user_registry__client_secret]
550
572
 
551
- token_response = requests.post(
573
+ kwargs = {}
574
+ if self.use_fapi_advanced:
575
+ token_request_payload = {
576
+ **token_request_payload,
577
+ "iss": database_user_registry[KG__database_user_registry__client_id],
578
+ "aud": discovery.get("pushed_authorization_request_endpoint"),
579
+ "jti": str(uuid.uuid4()),
580
+ "iat": int(time.time()),
581
+ "exp": int(time.time()) + 300 # Token valid for 5 minutes
582
+ }
583
+ token_request_payload = {
584
+ "client_id": database_user_registry[KG__database_user_registry__client_id],
585
+ "request": jwt.encode(token_request_payload, self.fapi_pem, algorithm="PS256", headers={
586
+ "kid": self.jwks["keys"][0]["kid"]
587
+ })
588
+ }
589
+ kwargs["verify"] = True
590
+ kwargs["cert"] = (f"/etc/letsencrypt/live/{self.application_url}/fullchain.pem", f"/etc/letsencrypt/live/{self.application_url}/privkey.pem")
591
+ else:
592
+ payload = {
593
+ "iss": database_user_registry[KG__database_user_registry__client_id],
594
+ "sub": database_user_registry[KG__database_user_registry__client_id],
595
+ "aud": token_endpoint,
596
+ "jti": str(uuid.uuid4()),
597
+ "iat": int(time.time()),
598
+ "exp": int(time.time()) + 300 # Token valid for 5 minutes
599
+ }
600
+ token_request_payload["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
601
+ token_request_payload["client_assertion"] = jwt.encode(payload, self.fapi_pem, algorithm="PS256", headers={
602
+ "kid": self.jwks["keys"][0]["kid"]
603
+ })
604
+
605
+ token_response = self.idp_session.post(
552
606
  token_endpoint.replace("localhost", "host.docker.internal"),
553
607
  data=token_request_payload,
608
+ **kwargs
554
609
  )
555
610
 
556
- if os.environ.get("JAAQL_DEBUGGING") == "TRUE":
557
- print(token_response.status_code)
558
- print(token_response.text)
559
-
560
611
  token_data = token_response.json()
561
612
  id_token = token_data.get('id_token')
613
+ access_token = token_data.get('access_token')
562
614
 
563
615
  signing_key = jwk_client.get_signing_key_from_jwt(id_token)
564
616
  try:
@@ -575,6 +627,38 @@ WHERE
575
627
  if id_payload.get("nonce") != oidc_state["nonce"]:
576
628
  raise UserUnauthorized()
577
629
 
630
+ if self.use_fapi_advanced:
631
+ access_signing_key = jwk_client.get_signing_key_from_jwt(access_token)
632
+ try:
633
+ access_payload = jwt.decode(
634
+ access_token,
635
+ access_signing_key.key,
636
+ algorithms=allowed_algs,
637
+ audience=database_user_registry[KG__database_user_registry__client_id],
638
+ issuer=expected_issuer
639
+ )
640
+ except:
641
+ raise UserUnauthorized()
642
+
643
+ # 2. Extract and check the cnf claim.
644
+ cnf_claim = access_payload.get("cnf")
645
+ if not cnf_claim:
646
+ raise UserUnauthorized() # Access token is missing the 'cnf' claim.
647
+
648
+ expected_thumbprint = cnf_claim.get("x5t#S256")
649
+ if not expected_thumbprint:
650
+ raise UserUnauthorized() # The 'cnf' claim does not contain the 'x5t#S256' field.
651
+
652
+ # 3. Compute the thumbprint of the certificate used for mTLS.
653
+ client_cert_thumbprint = self.get_cert_thumbprint()
654
+
655
+ # 4. Compare the thumbprints.
656
+ if client_cert_thumbprint != expected_thumbprint:
657
+ self.reload_fapi_cert()
658
+ client_cert_thumbprint = self.get_cert_thumbprint()
659
+ if client_cert_thumbprint != expected_thumbprint:
660
+ raise UserUnauthorized() # Access token 'cnf' claim does not match the client's certificate thumbprint.
661
+
578
662
  sub = id_payload.get("sub")
579
663
 
580
664
  account = None
@@ -659,6 +743,11 @@ WHERE
659
743
  attributes=get_cookie_attrs(self.vigilant_sessions, False, self.is_container),
660
744
  is_https=self.is_https)
661
745
 
746
+ response.set_cookie(COOKIE_LOGIN_MARKER, value="true", is_https=True, attributes=get_sloppy_cookie_attrs())
747
+
748
+ response.response_code = HTTPStatus.FOUND
749
+ response.raw_headers["Location"] = oidc_state[KEY__redirect_uri]
750
+
662
751
  def fetch_redirect_uri(self, inputs: dict, response: JAAQLResponse):
663
752
  schema = inputs.get(KEY__schema, None)
664
753
  application = application__select(self.jaaql_lookup_connection, inputs[KEY__application])
@@ -686,10 +775,11 @@ WHERE
686
775
  state = secrets.token_urlsafe(32)
687
776
  code_verifier = secrets.token_urlsafe(64)
688
777
  code_challenge = self.generate_code_challenge(code_verifier)
689
- redirect_uri = application[KG__application__base_url] + "/" + inputs[KEY__redirect_uri]
778
+ real_redirect_uri = application[KG__application__base_url] + "/" + inputs[KEY__redirect_uri]
779
+ oidc_redirect_uri = application[KG__application__base_url] + "/api" + ENDPOINT__oidc_get_token
690
780
 
691
781
  oidc_session = crypt_utils.jwt_encode(self.vault.get_obj(VAULT_KEY__jwt_crypt_key), {
692
- "redirect_uri": redirect_uri,
782
+ "redirect_uri": real_redirect_uri,
693
783
  "code_verifier": jaaql__encrypt(code_verifier, self.get_db_crypt_key()),
694
784
  "nonce": nonce,
695
785
  "state": state,
@@ -709,12 +799,75 @@ WHERE
709
799
  if scope not in default_scopes:
710
800
  default_scopes.append(scope)
711
801
 
712
- redirect = auth_endpoint + f"?client_id={client_id}&response_type=code&code_challenge_method=S256&scope={
713
- urllib.parse.quote(" ".join(["openid"]))}&nonce={nonce}&state={
714
- state}&code_challenge={code_challenge}&redirect_uri={urllib.parse.quote(redirect_uri, safe='')}"
802
+ par_endpoint = discovery.get("pushed_authorization_request_endpoint")
803
+ if not par_endpoint:
804
+ raise Exception("Pushed Authorization Request endpoint not found in discovery document.")
805
+ par_endpoint = par_endpoint.replace("localhost", "host.docker.internal")
806
+
807
+ par_payload = {
808
+ "client_id": database_user_registry[KG__database_user_registry__client_id],
809
+ "response_type": "code",
810
+ "code_challenge_method": "S256",
811
+ "scope": " ".join(["openid"]), # should later be default scopes, may cause issues now
812
+ "nonce": nonce,
813
+ "state": state,
814
+ "code_challenge": code_challenge,
815
+ "redirect_uri": oidc_redirect_uri,
816
+ }
817
+
818
+ kwargs = {}
819
+ if self.use_fapi_advanced:
820
+ payload = {
821
+ **par_payload,
822
+ "iss": database_user_registry[KG__database_user_registry__client_id],
823
+ "aud": discovery.get("pushed_authorization_request_endpoint"),
824
+ "jti": str(uuid.uuid4()),
825
+ "iat": int(time.time()),
826
+ "exp": int(time.time()) + 300 # Token valid for 5 minutes
827
+ }
828
+ par_payload = {
829
+ "client_id": database_user_registry[KG__database_user_registry__client_id],
830
+ "request": jwt.encode(payload, self.fapi_pem, algorithm="PS256", headers={
831
+ "kid": self.jwks["keys"][0]["kid"]
832
+ })
833
+ }
834
+ kwargs["verify"] = True
835
+ kwargs["cert"] = (f"/etc/letsencrypt/live/{self.application_url}/fullchain.pem", f"/etc/letsencrypt/live/{self.application_url}/privkey.pem")
836
+ else:
837
+ payload = {
838
+ "iss": database_user_registry[KG__database_user_registry__client_id],
839
+ "sub": database_user_registry[KG__database_user_registry__client_id],
840
+ "aud": discovery.get("pushed_authorization_request_endpoint"),
841
+ "jti": str(uuid.uuid4()),
842
+ "iat": int(time.time()),
843
+ "exp": int(time.time()) + 300 # Token valid for 5 minutes
844
+ }
845
+ par_payload["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
846
+ par_payload["client_assertion"] = jwt.encode(payload, self.fapi_pem, algorithm="PS256", headers={
847
+ "kid": self.jwks["keys"][0]["kid"]
848
+ })
849
+
850
+ par_response = self.idp_session.post(
851
+ par_endpoint,
852
+ data=par_payload,
853
+ **kwargs
854
+ )
855
+
856
+ if par_response.status_code not in (200, 201):
857
+ print(par_response.status_code)
858
+ print(par_response.text)
859
+ raise Exception(f"PAR request failed with status {par_response.status_code}: {par_response.text}")
860
+
861
+ par_data = par_response.json()
862
+ request_uri = par_data.get("request_uri")
863
+ if not request_uri:
864
+ raise Exception("No request_uri returned from the PAR endpoint.")
865
+
866
+ redirect_url = auth_endpoint + "?request_uri=" + urllib.parse.quote(request_uri) + "&response_mode=query.jwt&client_id=" + client_id
867
+ print(request_uri)
715
868
 
716
869
  response.response_code = HTTPStatus.FOUND
717
- response.raw_headers["Location"] = redirect
870
+ response.raw_headers["Location"] = redirect_url
718
871
 
719
872
  def set_web_config(self, connection: DBInterface):
720
873
  self.is_super_admin(connection)
@@ -71,6 +71,7 @@ def pull_from_dict(self, inputs: dict, keys: Union[list, str, dict]):
71
71
 
72
72
 
73
73
  COOKIE_JAAQL_AUTH = "jaaql_auth"
74
+ COOKIE_LOGIN_MARKER = "jaaql_successful_auth"
74
75
  COOKIE_OIDC = "oidc"
75
76
  COOKIE_FLAG_HTTP_ONLY = "HttpOnly"
76
77
  COOKIE_FLAG_SECURE = "Secure"
@@ -99,8 +100,19 @@ def get_cookie_attrs(vigilant_sessions: bool, remember_me: bool, is_gunicorn: bo
99
100
  return cookie_attrs
100
101
 
101
102
 
103
+ def get_sloppy_cookie_attrs():
104
+ cookie_attrs = {}
105
+ cookie_attrs[COOKIE_ATTR_PATH] = "/"
106
+
107
+ cookie_attrs[COOKIE_ATTR_EXPIRES] = format_date_time(mktime((datetime.now() + timedelta(minutes=15)).timetuple()))
108
+
109
+ return cookie_attrs
110
+
111
+
102
112
  def format_cookie(name, value, attributes, is_https: bool):
103
113
  cookie_flags = [COOKIE_FLAG_HTTP_ONLY]
114
+ if name == COOKIE_LOGIN_MARKER:
115
+ cookie_flags = []
104
116
  if is_https:
105
117
  cookie_flags.append(COOKIE_FLAG_SECURE)
106
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jaaql-middleware-python
3
- Version: 4.26.2
3
+ Version: 4.27.1
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -8,7 +8,7 @@ Author-email: aaron.tasker@sqmi.nl
8
8
  License: Mozilla Public License Version 2.0 with Commons Clause
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
- Requires-Dist: jaaql-monitor~=1.4.17
11
+ Requires-Dist: jaaql-monitor~=1.6.3
12
12
  Requires-Dist: psycopg[binary]~=3.1.18
13
13
  Requires-Dist: Pillow~=10.3.0
14
14
  Requires-Dist: cryptography~=42.0.5
@@ -21,14 +21,15 @@ Requires-Dist: werkzeug~=3.0.1
21
21
  Requires-Dist: argon2-cffi~=23.1.0
22
22
  Requires-Dist: pyotp~=2.9.0
23
23
  Requires-Dist: qrcode~=7.4.2
24
- Requires-Dist: gunicorn~=21.2.0
24
+ Requires-Dist: gunicorn~=23.0.0
25
25
  Requires-Dist: gevent~=24.2.1
26
- Requires-Dist: requests~=2.31.0
26
+ Requires-Dist: requests~=2.32.3
27
27
  Requires-Dist: pyzbar~=0.1.9
28
- Requires-Dist: parsys-requests-unixsocket~=0.3.1
28
+ Requires-Dist: parsys-requests-unixsocket~=0.3.2
29
29
  Requires-Dist: selenium~=4.18.1
30
30
  Requires-Dist: twine~=5.0.0
31
- Requires-Dist: urllib3~=2.2.1
31
+ Requires-Dist: urllib3~=2.3.0
32
+ Requires-Dist: jwcrypto~=1.5.6
32
33
 
33
34
  # JAAQL-middleware-python
34
35
  Please navigate to docker/docker.md to see setup instructions
@@ -1,4 +1,4 @@
1
- jaaql-monitor~=1.4.17
1
+ jaaql-monitor~=1.6.3
2
2
  psycopg[binary]~=3.1.18
3
3
  Pillow~=10.3.0
4
4
  cryptography~=42.0.5
@@ -11,11 +11,12 @@ werkzeug~=3.0.1
11
11
  argon2-cffi~=23.1.0
12
12
  pyotp~=2.9.0
13
13
  qrcode~=7.4.2
14
- gunicorn~=21.2.0
14
+ gunicorn~=23.0.0
15
15
  gevent~=24.2.1
16
- requests~=2.31.0
16
+ requests~=2.32.3
17
17
  pyzbar~=0.1.9
18
- parsys-requests-unixsocket~=0.3.1
18
+ parsys-requests-unixsocket~=0.3.2
19
19
  selenium~=4.18.1
20
20
  twine~=5.0.0
21
- urllib3~=2.2.1
21
+ urllib3~=2.3.0
22
+ jwcrypto~=1.5.6