xtr-security-http 4.0.4__tar.gz → 5.0.0__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 (298) hide show
  1. xtr_security_http-5.0.0/PKG-INFO +1745 -0
  2. xtr_security_http-5.0.0/README.md +1706 -0
  3. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/pyproject.toml +24 -13
  4. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/pyproject.toml.orig +34 -10
  5. xtr_security_http-5.0.0/src/xtr_security_http/__init__.py +363 -0
  6. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/_challenge.py +47 -7
  7. xtr_security_http-5.0.0/src/xtr_security_http/_parked_session.py +44 -0
  8. xtr_security_http-5.0.0/src/xtr_security_http/_request_parameters.py +95 -0
  9. xtr_security_http-5.0.0/src/xtr_security_http/_security_state.py +137 -0
  10. xtr_security_http-5.0.0/src/xtr_security_http/access_map.py +74 -0
  11. xtr_security_http-5.0.0/src/xtr_security_http/access_map_interface.py +39 -0
  12. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/__init__.py +2 -0
  13. xtr_security_http-5.0.0/src/xtr_security_http/access_token/access_token_extractor_interface.py +24 -0
  14. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/chain_access_token_extractor.py +3 -10
  15. xtr_security_http-5.0.0/src/xtr_security_http/access_token/form_encoded_body_extractor.py +85 -0
  16. xtr_security_http-5.0.0/src/xtr_security_http/access_token/header_access_token_extractor.py +68 -0
  17. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/__init__.py +14 -0
  18. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_introspected_user.py +70 -0
  19. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_introspection_claims.py +193 -0
  20. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_signed_response.py +141 -0
  21. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_signed_response_discovery.py +149 -0
  22. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/oauth2_token_handler.py +471 -0
  23. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/oidc/__init__.py +19 -8
  24. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/__init__.py +9 -0
  25. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/invalid_signature_error.py +23 -0
  26. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/missing_claim_error.py +23 -0
  27. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_token_generator.py +190 -0
  28. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/oidc/oidc_token_handler.py +7 -3
  29. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_trait.py +90 -0
  30. xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_user_info_token_handler.py +218 -0
  31. xtr_security_http-5.0.0/src/xtr_security_http/access_token/query_access_token_extractor.py +45 -0
  32. xtr_security_http-5.0.0/src/xtr_security_http/access_token/sender_constraint_interface.py +69 -0
  33. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/__init__.py +12 -0
  34. xtr_security_http-5.0.0/src/xtr_security_http/authentication/_authentication_outcome.py +191 -0
  35. xtr_security_http-5.0.0/src/xtr_security_http/authentication/_manager_reporting.py +48 -0
  36. xtr_security_http-5.0.0/src/xtr_security_http/authentication/_referer.py +67 -0
  37. xtr_security_http-5.0.0/src/xtr_security_http/authentication/_supports_pass.py +96 -0
  38. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/authentication_failure_handler_interface.py +10 -10
  39. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/authentication_success_handler_interface.py +3 -5
  40. xtr_security_http-5.0.0/src/xtr_security_http/authentication/authentication_utils.py +119 -0
  41. xtr_security_http-5.0.0/src/xtr_security_http/authentication/authenticator_manager.py +381 -0
  42. xtr_security_http-5.0.0/src/xtr_security_http/authentication/authenticator_manager_interface.py +55 -0
  43. xtr_security_http-5.0.0/src/xtr_security_http/authentication/custom_authentication_failure_handler.py +66 -0
  44. xtr_security_http-5.0.0/src/xtr_security_http/authentication/custom_authentication_success_handler.py +81 -0
  45. xtr_security_http-5.0.0/src/xtr_security_http/authentication/default_authentication_failure_handler.py +175 -0
  46. xtr_security_http-5.0.0/src/xtr_security_http/authentication/default_authentication_success_handler.py +173 -0
  47. xtr_security_http-5.0.0/src/xtr_security_http/authentication/user_authenticator_interface.py +54 -0
  48. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/__init__.py +45 -0
  49. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_access_token_challenge.py +96 -0
  50. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_access_token_scopes.py +40 -0
  51. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_form_login_credentials.py +75 -0
  52. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_attempts.py +115 -0
  53. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_authorization.py +148 -0
  54. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_callback.py +102 -0
  55. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_claims.py +149 -0
  56. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_options.py +136 -0
  57. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_public_client.py +55 -0
  58. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_token.py +83 -0
  59. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/abstract_authenticator.py +7 -8
  60. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/abstract_login_form_authenticator.py +137 -0
  61. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/abstract_pre_authenticated_authenticator.py +248 -0
  62. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/access_token_authenticator.py +285 -0
  63. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/authenticator_interface.py +7 -8
  64. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/__init__.py +13 -0
  65. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/traceable_authenticator.py +168 -0
  66. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/traceable_authenticator_manager_listener.py +101 -0
  67. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/unsupported_reasons.py +41 -0
  68. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/fallback_user_loader.py +52 -0
  69. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/form_login_authenticator.py +264 -0
  70. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/http_basic_authenticator.py +183 -0
  71. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/interactive_authenticator_interface.py +32 -0
  72. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/json_login_authenticator.py +254 -0
  73. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/login_link_authenticator.py +216 -0
  74. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/oidc_login_authenticator.py +379 -0
  75. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/__init__.py +6 -0
  76. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/authentication_method_badge.py +48 -0
  77. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/csrf_token_badge.py +60 -0
  78. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/remember_me_badge.py +60 -0
  79. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/user_badge.py +14 -16
  80. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/custom_credentials.py +17 -6
  81. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/password_credentials.py +4 -1
  82. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/passport.py +121 -0
  83. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/self_validating_passport.py +53 -0
  84. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/remember_me_authenticator.py +209 -0
  85. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/remote_user_authenticator.py +76 -0
  86. xtr_security_http-5.0.0/src/xtr_security_http/authenticator/x509_authenticator.py +111 -0
  87. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/access_denied_handler_interface.py +4 -3
  88. xtr_security_http-5.0.0/src/xtr_security_http/authorization/insufficient_scope_access_denied_handler.py +136 -0
  89. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/oauth2_scope_voter.py +43 -23
  90. xtr_security_http-5.0.0/src/xtr_security_http/command/__init__.py +7 -0
  91. xtr_security_http-5.0.0/src/xtr_security_http/command/_registry.py +16 -0
  92. xtr_security_http-5.0.0/src/xtr_security_http/command/oidc_token_generate_command.py +152 -0
  93. xtr_security_http-5.0.0/src/xtr_security_http/decorator/__init__.py +41 -0
  94. xtr_security_http-5.0.0/src/xtr_security_http/decorator/_methods.py +24 -0
  95. xtr_security_http-5.0.0/src/xtr_security_http/decorator/current_token.py +56 -0
  96. xtr_security_http-5.0.0/src/xtr_security_http/decorator/current_user.py +82 -0
  97. xtr_security_http-5.0.0/src/xtr_security_http/decorator/firewall_listener_order.py +79 -0
  98. xtr_security_http-5.0.0/src/xtr_security_http/decorator/is_csrf_token_valid.py +119 -0
  99. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/decorator/is_granted.py +56 -53
  100. xtr_security_http-5.0.0/src/xtr_security_http/entry_point/__init__.py +15 -0
  101. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/entry_point/authentication_entry_point_interface.py +6 -3
  102. xtr_security_http-5.0.0/src/xtr_security_http/entry_point/fallback_authentication_entry_point_interface.py +54 -0
  103. xtr_security_http-5.0.0/src/xtr_security_http/entry_point/re_authentication_entry_point_interface.py +50 -0
  104. xtr_security_http-5.0.0/src/xtr_security_http/event/__init__.py +35 -0
  105. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/check_passport_event.py +10 -0
  106. xtr_security_http-5.0.0/src/xtr_security_http/event/check_refreshed_user_event.py +102 -0
  107. xtr_security_http-5.0.0/src/xtr_security_http/event/interactive_login_event.py +47 -0
  108. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/login_failure_event.py +36 -16
  109. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/login_success_event.py +41 -10
  110. xtr_security_http-5.0.0/src/xtr_security_http/event/logout_event.py +68 -0
  111. xtr_security_http-5.0.0/src/xtr_security_http/event/oidc_authorization_request_event.py +87 -0
  112. xtr_security_http-5.0.0/src/xtr_security_http/event/switch_user_event.py +77 -0
  113. xtr_security_http-5.0.0/src/xtr_security_http/event/token_deauthenticated_event.py +63 -0
  114. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/__init__.py +62 -0
  115. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/authentication_proofs_listener.py +112 -0
  116. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/check_credentials_listener.py +10 -9
  117. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/check_remember_me_conditions_listener.py +148 -0
  118. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/clear_site_data_listener.py +74 -0
  119. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/cleared_cookie.py +33 -0
  120. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/cookie_clearing_listener.py +88 -0
  121. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/csrf_protection_listener.py +97 -0
  122. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/csrf_token_clearing_listener.py +66 -0
  123. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/default_logout_listener.py +69 -0
  124. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/login_throttling_listener.py +191 -0
  125. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/oidc_back_channel_logout_listener.py +98 -0
  126. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/oidc_end_session_listener.py +122 -0
  127. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/password_migrating_listener.py +42 -12
  128. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/remember_me_listener.py +103 -0
  129. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/save_session_listener.py +129 -0
  130. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/session_logout_listener.py +69 -0
  131. xtr_security_http-5.0.0/src/xtr_security_http/event_listener/session_strategy_listener.py +111 -0
  132. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/user_checker_listener.py +28 -7
  133. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/user_provider_listener.py +22 -11
  134. xtr_security_http-5.0.0/src/xtr_security_http/exception/__init__.py +43 -0
  135. xtr_security_http-5.0.0/src/xtr_security_http/exception/expired_login_link_error.py +19 -0
  136. xtr_security_http-5.0.0/src/xtr_security_http/exception/impersonation_error.py +18 -0
  137. xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_csrf_token_error.py +22 -0
  138. xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_authentication_error.py +21 -0
  139. xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_error.py +20 -0
  140. xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_error_interface.py +22 -0
  141. xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_session_strategy_error.py +33 -0
  142. xtr_security_http-5.0.0/src/xtr_security_http/exception/logout_error.py +17 -0
  143. xtr_security_http-5.0.0/src/xtr_security_http/exception/missing_session_error.py +20 -0
  144. xtr_security_http-5.0.0/src/xtr_security_http/exception/missing_state_bag_error.py +20 -0
  145. xtr_security_http-5.0.0/src/xtr_security_http/exception/oidc_invalid_grant_error.py +22 -0
  146. xtr_security_http-5.0.0/src/xtr_security_http/exception/oidc_session_ended_error.py +21 -0
  147. xtr_security_http-5.0.0/src/xtr_security_http/firewall/__init__.py +48 -0
  148. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_exception_answers.py +89 -0
  149. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_re_authentication.py +63 -0
  150. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_load.py +55 -0
  151. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_request.py +48 -0
  152. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_uri.py +33 -0
  153. xtr_security_http-5.0.0/src/xtr_security_http/firewall/_user_refresh.py +197 -0
  154. xtr_security_http-5.0.0/src/xtr_security_http/firewall/abstract_listener.py +30 -0
  155. xtr_security_http-5.0.0/src/xtr_security_http/firewall/access_listener.py +200 -0
  156. xtr_security_http-5.0.0/src/xtr_security_http/firewall/authenticator_manager_listener.py +98 -0
  157. xtr_security_http-5.0.0/src/xtr_security_http/firewall/channel_listener.py +179 -0
  158. xtr_security_http-5.0.0/src/xtr_security_http/firewall/context_listener.py +262 -0
  159. xtr_security_http-5.0.0/src/xtr_security_http/firewall/exception_listener.py +387 -0
  160. xtr_security_http-5.0.0/src/xtr_security_http/firewall/firewall.py +201 -0
  161. xtr_security_http-5.0.0/src/xtr_security_http/firewall/firewall_listener_interface.py +71 -0
  162. xtr_security_http-5.0.0/src/xtr_security_http/firewall/logout_listener.py +179 -0
  163. xtr_security_http-5.0.0/src/xtr_security_http/firewall/oidc_token_refresh_listener.py +125 -0
  164. xtr_security_http-5.0.0/src/xtr_security_http/firewall/refresh_user_result.py +42 -0
  165. xtr_security_http-5.0.0/src/xtr_security_http/firewall/switch_user_listener.py +384 -0
  166. xtr_security_http-5.0.0/src/xtr_security_http/firewall/user_changed.py +123 -0
  167. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_context_interface.py +26 -12
  168. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_map.py +42 -14
  169. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_map_interface.py +29 -5
  170. xtr_security_http-5.0.0/src/xtr_security_http/http_utils.py +190 -0
  171. xtr_security_http-5.0.0/src/xtr_security_http/impersonate/__init__.py +15 -0
  172. xtr_security_http-5.0.0/src/xtr_security_http/impersonate/impersonate_url_generator.py +265 -0
  173. xtr_security_http-5.0.0/src/xtr_security_http/impersonate/impersonation_form.py +32 -0
  174. xtr_security_http-5.0.0/src/xtr_security_http/impersonate/switch_user_config.py +32 -0
  175. xtr_security_http-5.0.0/src/xtr_security_http/impersonate/switch_user_config_lookup_interface.py +38 -0
  176. xtr_security_http-5.0.0/src/xtr_security_http/login_link/__init__.py +20 -0
  177. xtr_security_http-5.0.0/src/xtr_security_http/login_link/_presented_link.py +103 -0
  178. xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_details.py +52 -0
  179. xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_handler.py +286 -0
  180. xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_handler_interface.py +67 -0
  181. xtr_security_http-5.0.0/src/xtr_security_http/logout/__init__.py +8 -0
  182. xtr_security_http-5.0.0/src/xtr_security_http/logout/logout_form.py +30 -0
  183. xtr_security_http-5.0.0/src/xtr_security_http/logout/logout_url_generator.py +298 -0
  184. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/__init__.py +43 -0
  185. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/__init__.py +14 -0
  186. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/access_token_type_interface.py +115 -0
  187. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/bearer_token_type.py +119 -0
  188. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/__init__.py +29 -0
  189. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/abstract_client_assertion.py +258 -0
  190. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/abstract_tls_client_auth.py +57 -0
  191. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_authentication_interface.py +64 -0
  192. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_basic.py +73 -0
  193. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_jwt.py +107 -0
  194. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_post.py +74 -0
  195. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/no_client_authentication.py +85 -0
  196. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/private_key_jwt.py +83 -0
  197. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/self_signed_tls_client_auth.py +29 -0
  198. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/tls_client_auth.py +29 -0
  199. xtr_security_http-5.0.0/src/xtr_security_http/oauth2/jws_algorithms.py +113 -0
  200. xtr_security_http-5.0.0/src/xtr_security_http/oidc/__init__.py +44 -0
  201. xtr_security_http-5.0.0/src/xtr_security_http/oidc/discovery_oidc_key_set_provider.py +258 -0
  202. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_back_channel_logout.py +109 -0
  203. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_client.py +339 -0
  204. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_client_interface.py +117 -0
  205. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_discovery.py +376 -0
  206. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_ended_sessions.py +83 -0
  207. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_id_token.py +219 -0
  208. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_jwks.py +276 -0
  209. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_logout_token.py +181 -0
  210. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_signature_verifier.py +227 -0
  211. xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_token_refresher.py +218 -0
  212. xtr_security_http-5.0.0/src/xtr_security_http/rate_limiter/__init__.py +16 -0
  213. xtr_security_http-5.0.0/src/xtr_security_http/rate_limiter/default_login_rate_limiter.py +158 -0
  214. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/__init__.py +29 -0
  215. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/_signature_digest.py +126 -0
  216. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/abstract_remember_me_handler.py +189 -0
  217. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/persistent_remember_me_handler.py +269 -0
  218. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_cookie.py +81 -0
  219. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_details.py +168 -0
  220. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_handler_interface.py +63 -0
  221. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/response_listener.py +58 -0
  222. xtr_security_http-5.0.0/src/xtr_security_http/remember_me/signature_remember_me_handler.py +139 -0
  223. xtr_security_http-5.0.0/src/xtr_security_http/resolver/__init__.py +21 -0
  224. xtr_security_http-5.0.0/src/xtr_security_http/resolver/csrf_token_checker.py +65 -0
  225. xtr_security_http-5.0.0/src/xtr_security_http/resolver/is_granted_checker.py +124 -0
  226. xtr_security_http-5.0.0/src/xtr_security_http/resolver/security_token_resolver.py +57 -0
  227. xtr_security_http-5.0.0/src/xtr_security_http/resolver/user_value_resolver.py +82 -0
  228. xtr_security_http-5.0.0/src/xtr_security_http/security_events.py +75 -0
  229. xtr_security_http-5.0.0/src/xtr_security_http/security_request_attributes.py +122 -0
  230. xtr_security_http-5.0.0/src/xtr_security_http/session/__init__.py +11 -0
  231. xtr_security_http-5.0.0/src/xtr_security_http/session/session_authentication_strategy.py +140 -0
  232. xtr_security_http-5.0.0/src/xtr_security_http/session/session_authentication_strategy_interface.py +38 -0
  233. xtr_security_http-5.0.0/src/xtr_security_http/util/__init__.py +7 -0
  234. xtr_security_http-5.0.0/src/xtr_security_http/util/target_path.py +65 -0
  235. xtr_security_http-4.0.4/PKG-INFO +0 -429
  236. xtr_security_http-4.0.4/README.md +0 -396
  237. xtr_security_http-4.0.4/src/xtr_security_http/__init__.py +0 -173
  238. xtr_security_http-4.0.4/src/xtr_security_http/_runner.py +0 -151
  239. xtr_security_http-4.0.4/src/xtr_security_http/_state.py +0 -58
  240. xtr_security_http-4.0.4/src/xtr_security_http/access_map.py +0 -52
  241. xtr_security_http-4.0.4/src/xtr_security_http/access_map_interface.py +0 -24
  242. xtr_security_http-4.0.4/src/xtr_security_http/access_token/access_token_extractor_interface.py +0 -31
  243. xtr_security_http-4.0.4/src/xtr_security_http/access_token/form_encoded_body_extractor.py +0 -120
  244. xtr_security_http-4.0.4/src/xtr_security_http/access_token/header_access_token_extractor.py +0 -80
  245. xtr_security_http-4.0.4/src/xtr_security_http/access_token/oidc/exception/__init__.py +0 -7
  246. xtr_security_http-4.0.4/src/xtr_security_http/access_token/query_access_token_extractor.py +0 -51
  247. xtr_security_http-4.0.4/src/xtr_security_http/authentication/authenticator_manager.py +0 -232
  248. xtr_security_http-4.0.4/src/xtr_security_http/authentication/authenticator_manager_interface.py +0 -40
  249. xtr_security_http-4.0.4/src/xtr_security_http/authenticator/__init__.py +0 -9
  250. xtr_security_http-4.0.4/src/xtr_security_http/authenticator/access_token_authenticator.py +0 -209
  251. xtr_security_http-4.0.4/src/xtr_security_http/authenticator/passport/passport.py +0 -113
  252. xtr_security_http-4.0.4/src/xtr_security_http/authenticator/passport/self_validating_passport.py +0 -42
  253. xtr_security_http-4.0.4/src/xtr_security_http/authorization/insufficient_scope_access_denied_handler.py +0 -71
  254. xtr_security_http-4.0.4/src/xtr_security_http/decorator/__init__.py +0 -16
  255. xtr_security_http-4.0.4/src/xtr_security_http/decorator/current_user.py +0 -82
  256. xtr_security_http-4.0.4/src/xtr_security_http/entry_point/__init__.py +0 -7
  257. xtr_security_http-4.0.4/src/xtr_security_http/event/__init__.py +0 -21
  258. xtr_security_http-4.0.4/src/xtr_security_http/event_listener/__init__.py +0 -21
  259. xtr_security_http-4.0.4/src/xtr_security_http/exception/__init__.py +0 -18
  260. xtr_security_http-4.0.4/src/xtr_security_http/exception/firewall_not_booted_error.py +0 -18
  261. xtr_security_http-4.0.4/src/xtr_security_http/firewall/__init__.py +0 -15
  262. xtr_security_http-4.0.4/src/xtr_security_http/firewall/access_listener.py +0 -66
  263. xtr_security_http-4.0.4/src/xtr_security_http/firewall/exception_listener.py +0 -156
  264. xtr_security_http-4.0.4/src/xtr_security_http/firewall/firewall.py +0 -142
  265. xtr_security_http-4.0.4/src/xtr_security_http/firewall_scheme.py +0 -114
  266. xtr_security_http-4.0.4/src/xtr_security_http/firewall_scheme_registry.py +0 -83
  267. xtr_security_http-4.0.4/src/xtr_security_http/oidc/__init__.py +0 -7
  268. xtr_security_http-4.0.4/src/xtr_security_http/oidc/oidc_discovery.py +0 -318
  269. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/__init__.py +0 -28
  270. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/_pattern.py +0 -27
  271. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/callable_request_matcher.py +0 -37
  272. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/chain_request_matcher.py +0 -37
  273. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/host_request_matcher.py +0 -51
  274. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/ip_request_matcher.py +0 -61
  275. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/method_request_matcher.py +0 -32
  276. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/path_request_matcher.py +0 -43
  277. xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/request_matcher_interface.py +0 -25
  278. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/LICENSE +0 -0
  279. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/access_token_handler_interface.py +0 -0
  280. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/oidc/exception/oidc_key_set_error.py +0 -0
  281. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/_sensitive.py +0 -0
  282. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/expose_security_level.py +0 -0
  283. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/oidc/__init__.py +0 -0
  284. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/oidc/oidc_jwks.py +0 -0
  285. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/__init__.py +0 -0
  286. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/badge_interface.py +0 -0
  287. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/password_upgrade_badge.py +0 -0
  288. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/pre_authenticated_user_badge.py +0 -0
  289. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/__init__.py +0 -0
  290. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/credentials_interface.py +0 -0
  291. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/token/__init__.py +0 -0
  292. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/token/post_authentication_token.py +0 -0
  293. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/__init__.py +0 -0
  294. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/decorator/is_granted_context.py +0 -0
  295. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/authentication_token_created_event.py +0 -0
  296. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/exception/invalid_access_token_error.py +0 -0
  297. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/exception/unknown_firewall_error.py +0 -0
  298. {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/py.typed +0 -0
@@ -0,0 +1,1745 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-security-http
3
+ Version: 5.0.0
4
+ Summary: The HTTP edge of xtr security: firewalls, authenticators, bearer access tokens and the request surface.
5
+ Keywords: security,authentication,firewall,fastapi,oauth2
6
+ Author: Xterr
7
+ Author-email: Xterr <me@xterr.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: xtr-security-core>=5.0,<6
18
+ Requires-Dist: xtr-security-csrf>=5.0,<6
19
+ Requires-Dist: xtr-http-kernel>=5.0,<6
20
+ Requires-Dist: xtr-http-foundation>=5.0,<6
21
+ Requires-Dist: xtr-http-message-contracts>=5.0,<6
22
+ Requires-Dist: xtr-http-client-contracts>=5.0,<6
23
+ Requires-Dist: xtr-cache-contracts>=5.0,<6
24
+ Requires-Dist: xtr-dependency-injection>=5.0,<6
25
+ Requires-Dist: xtr-event-dispatcher>=5.0,<6
26
+ Requires-Dist: xtr-event-dispatcher-contracts>=5.0,<6
27
+ Requires-Dist: xtr-clock>=5.0,<6
28
+ Requires-Dist: xtr-logging-contracts>=5.0,<6
29
+ Requires-Dist: xtr-password-hasher>=5.0,<6
30
+ Requires-Dist: xtr-rate-limiter>=5.0,<6
31
+ Requires-Dist: fastapi>=0.123
32
+ Requires-Dist: starlette>=0.40
33
+ Requires-Dist: anyio>=4.0
34
+ Requires-Dist: typing-extensions>=4.12
35
+ Requires-Dist: joserfc>=1.7 ; extra == 'oidc'
36
+ Requires-Python: >=3.11
37
+ Provides-Extra: oidc
38
+ Description-Content-Type: text/markdown
39
+
40
+ <div align="center">
41
+
42
+ # xtr-security-http
43
+
44
+ **The HTTP edge of security: firewalls, authenticators, bearer access tokens and the request surface.**
45
+
46
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
47
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
48
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
49
+
50
+ </div>
51
+
52
+ ---
53
+
54
+ ## Why?
55
+
56
+ FastAPI already parses an `Authorization` header, documents a security scheme in OpenAPI, and
57
+ fills a dependency on a route. What it does not do is turn a credential into a *user*, enforce a
58
+ *rule* against that user before the endpoint runs, and answer an unauthenticated or denied
59
+ request with the right challenge. That part — a **firewall** over a slice of the request space,
60
+ the **authenticators** it runs, and the response it gives when authentication or authorization
61
+ fails — is this package.
62
+
63
+ It builds on [xtr-security-core](../xtr-security-core)'s users, tokens and voters, and on
64
+ [xtr-http-kernel](../xtr-http-kernel)'s request lifecycle:
65
+
66
+ - 🔥 **Firewalls** — a `FirewallMap` matches a request to one firewall; the firewall runs its
67
+ authenticators once per request and checks the request against an access map.
68
+ - 🎟️ **Authenticators and passports** — an authenticator reads a request into a `Passport` of
69
+ badges and credentials; a fixed event order resolves and checks it, then mints a token.
70
+ - 🪙 **Bearer access tokens** — extractors pull a token from a header, query or form; a handler
71
+ turns it into a user. An OAuth2 handler introspects the token on the authorization server; an
72
+ OIDC handler verifies a third-party issuer's tokens.
73
+ - 🛂 **The FastAPI surface stays FastAPI's** — `IsGranted` and `CurrentUser` are dependencies
74
+ and decorators; the firewall runs as a subscriber over the request lifecycle, before
75
+ routing, and a route documents its security with the framework's own params.
76
+ - 🧩 **RFC 6750 challenges** — a `401` with `WWW-Authenticate: Bearer`, an `invalid_token` or
77
+ `insufficient_scope` challenge, written for you.
78
+
79
+ What FastAPI already does, it keeps doing: scheme parsing, argument resolution and the generated
80
+ schema stay the framework's. This package is the user, the rule and the challenge around them.
81
+
82
+ ## Install
83
+
84
+ ```sh
85
+ uv add xtr-security-http # firewalls, authenticators, access tokens
86
+ uv add "xtr-security-http[oidc]" # + verifying a third-party OIDC issuer's tokens
87
+ ```
88
+
89
+ Requires Python 3.11+. The web framework and the security core come with the package; the `oidc`
90
+ extra adds [joserfc](https://jose.authlib.org/) and [httpx](https://www.python-httpx.org/) for
91
+ verifying and discovering an issuer's keys, and for verifying the signed introspection responses
92
+ the OAuth2 handler can ask for.
93
+
94
+ ## Quick start
95
+
96
+ The HTTP edge is built from plain objects, and the pieces that turn a credential into a token
97
+ run without a container. Below: an access-token authenticator over a bearer-header extractor and
98
+ a handler you write, an access map matched with a request matcher, and the OAuth2 scope helpers —
99
+ all standalone.
100
+
101
+ ```python
102
+ from __future__ import annotations
103
+
104
+ import asyncio
105
+
106
+ from xtr_event_dispatcher import EventDispatcher
107
+ from xtr_http_foundation import PathRequestMatcher
108
+ from xtr_http_message import ServerRequest
109
+ from xtr_security_core import InMemoryUser, TokenStorage
110
+ from xtr_security_core.authorization.access_decision_manager import AccessDecisionManager
111
+ from xtr_security_http import (
112
+ AccessListener,
113
+ AccessMap,
114
+ AccessTokenAuthenticator,
115
+ AccessTokenHandlerInterface,
116
+ AuthenticatorManager,
117
+ HeaderAccessTokenExtractor,
118
+ InvalidAccessTokenError,
119
+ OAuth2ScopeVoter,
120
+ UserBadge,
121
+ oauth2_scope,
122
+ )
123
+ from xtr_security_http._security_state import SecurityState
124
+ from xtr_security_http.authorization.oauth2_scope_voter import parse_oauth2_scope
125
+
126
+
127
+ class MyTokenHandler(AccessTokenHandlerInterface):
128
+ async def get_user_badge_from(self, access_token: str) -> UserBadge:
129
+ if access_token != "s3cret":
130
+ raise InvalidAccessTokenError("Unknown token.")
131
+ return UserBadge("ada", user_loader=lambda i: InMemoryUser(i, roles=["ROLE_USER"]))
132
+
133
+
134
+ def make_message(path: str, headers: dict[str, str] | None = None) -> ServerRequest:
135
+ return ServerRequest("GET", f"https://example.com{path}", dict(headers or {}))
136
+
137
+
138
+ async def main() -> None:
139
+ storage = TokenStorage()
140
+ manager = AuthenticatorManager(
141
+ authenticators=[
142
+ AccessTokenAuthenticator(
143
+ handler=MyTokenHandler(),
144
+ extractor=HeaderAccessTokenExtractor(),
145
+ )
146
+ ],
147
+ token_storage=storage,
148
+ event_dispatcher=EventDispatcher(),
149
+ firewall_name="api",
150
+ )
151
+
152
+ message = make_message("/api/me", {"Authorization": "Bearer s3cret"})
153
+ state = SecurityState()
154
+ manager.supports(message, state)
155
+ await manager.authenticate_request(message, state)
156
+ token = storage.get_token()
157
+ print("authenticated:", token is not None and token.get_user_identifier())
158
+
159
+ access_map = AccessMap()
160
+ access_map.add(PathRequestMatcher(r"^/api"), "ROLE_USER")
161
+ AccessListener(
162
+ storage, AccessDecisionManager(), access_map
163
+ ) # decides a token against the rules
164
+ print("rule attribute:", (await access_map.get_patterns(make_message("/api/me")))[0])
165
+
166
+ attribute = oauth2_scope("books:read", "books:write")
167
+ print("oauth2_scope:", attribute)
168
+ print("parse_oauth2_scope:", parse_oauth2_scope(attribute))
169
+ print("voter supports:", OAuth2ScopeVoter().supports_attribute(attribute))
170
+
171
+
172
+ asyncio.run(main())
173
+ ```
174
+
175
+ ```console
176
+ authenticated: ada
177
+ rule attribute: ROLE_USER
178
+ oauth2_scope: OAUTH2_SCOPE(books:read books:write)
179
+ parse_oauth2_scope: ('books:read', 'books:write')
180
+ voter supports: True
181
+ ```
182
+
183
+ ### The full HTTP path needs the bundle
184
+
185
+ Running an actual firewall over a served request is more than these primitives. The `Firewall`
186
+ subscriber runs the chain of the firewall a request matched over the
187
+ [xtr-http-kernel](../xtr-http-kernel) lifecycle — before routing — and the `ExceptionListener`
188
+ turns a security error into a challenge. Wiring all of that is exactly what the
189
+ [`xtr-security`](../xtr-security) bundle exists to do: an application lists `SecurityBundle`
190
+ and writes firewalls as configuration. See [xtr-security's Quick
191
+ start](../xtr-security#quick-start) for the whole loop, and its
192
+ [Use in an application](../xtr-security#use-in-an-application) for what adding it takes.
193
+
194
+ ## Concepts
195
+
196
+ ### Firewalls and the map
197
+
198
+ A **`FirewallContextInterface`** is one firewall's runtime pieces: its `name`, its
199
+ `authenticator_manager`, its `access_listener`, whether it has `security` at all, the optional
200
+ `entry_point`,
201
+ `access_denied_handler` and `scope_denied_handler` that answer a failure, and its
202
+ listener chain — `listeners` in the order they run (the order the bundle's sorting
203
+ pass left them in), the `exception_listener` that answers a security error, and the
204
+ optional `logout_listener`. A **`FirewallMap`** holds those contexts: `await match(message)`
205
+ returns the first whose matcher claims the request message, `get(name)` fetches one by
206
+ name (raising `UnknownFirewallError`), `has(name)` and `names()` report what is
207
+ registered, and `await get_listeners(message)` answers the matched firewall's
208
+ `(listeners, exception_listener, logout_listener)` — or an empty chain and no
209
+ listeners when no firewall claims the message. `FirewallMapInterface` is the protocol
210
+ the bundle wires a container-backed map to.
211
+
212
+ **`Firewall`** is the subscriber that runs the firewalls: a listener over the kernel
213
+ lifecycle's events that the [xtr-security](../xtr-security) bundle registers on the main
214
+ dispatcher. At the request it matches the map, runs the matched firewall's chain — before
215
+ routing, so a request no route would answer is still challenged as a request — and parks the
216
+ matched firewall and its exception listener on the request's security state; a failure the
217
+ chain raises is announced on the exception event and answered by that listener, exactly once;
218
+ the finish of the request puts the stored listener away. A route documents its security with
219
+ the framework's own params; the firewall contributes no OpenAPI scheme of its own.
220
+
221
+ ### Firewall listeners
222
+
223
+ A **`FirewallListenerInterface`** is one step of the chain a firewall runs over a request:
224
+ `supports(request)` tells whether the step handles the request — `False` skips it, while `None`
225
+ (lazy) and `True` (eager) both run `authenticate(request_event)`, which does the work and
226
+ typically answers by setting a response on the kernel's request event, which also stops the
227
+ chain. `get_priority()` declares the step's position — the higher, the earlier — and
228
+ **`AbstractListener`** is the template a concrete listener starts from: it fixes the default
229
+ priority `0` and leaves both hooks to the subclass. **`FirewallListenerOrder`** declares the
230
+ neighbours a listener runs `before` or `after` — a constraint that only reorders listeners
231
+ sharing a priority, and one that would cross it fails the build. The [xtr-security](../xtr-security)
232
+ bundle sorts each firewall's chain by them.
233
+
234
+ **`AuthenticatorManagerListener`** is the chain's authentication step: a thin delegation to a
235
+ firewall's authenticator manager — `supports` answers the manager's supports pass,
236
+ `authenticate` runs the pass that authenticates and sets the response a handler produced on the
237
+ event. The manager's two passes hand each other what they learned through the request's security
238
+ state, which rides the kernel event's bag: `authenticate` reads it off the event, while the
239
+ caller running the chain parks that bag on the request it asks `supports` about — the one place
240
+ both hooks reach the same carrier. **`TraceableAuthenticatorManagerListener`** wraps the
241
+ listener for the tooling: it re-parks the supporters the supports pass chose as traceable
242
+ wrappers, so what each authenticator answered is recorded and read through
243
+ `get_authenticators_info()`.
244
+
245
+ **`AccessListener`** is the chain's access step, at priority `-255` — after the authentication
246
+ step. `supports` matches the firewall's access map and parks what the matched rule requires on
247
+ the request's security state, answering `True` when there is something to decide and `None` —
248
+ the lazy join — when the match requires nothing or only public access; `authenticate` reads the
249
+ parked attributes back, decides the token from the token storage against them with the request
250
+ itself as the subject, and raises `AccessDeniedError` carrying the attributes, the subject and
251
+ the decision when it is refused. A null token stands in for an anonymous request, so the
252
+ decision — not the listener — is what turns an anonymous request away from a rule that needs
253
+ authentication.
254
+
255
+ **`LogoutListener`** is the chain's logout step, at priority `-127` — behind the steps that
256
+ authenticate and decide, ahead of the ones that only clean up. A firewall with one configured
257
+ runs it where the firewall's insertion rule places it: ahead of the first chain step running at
258
+ a lower priority. `supports` answers whether the request's path is the configured logout path —
259
+ a strict `False` for anything else, so the chain skips it; `authenticate` proves the request
260
+ first (the cross-site request forgery token under the configured parameter name, in the
261
+ submitted form or the query string, when a manager is wired — `LogoutError` when it does not
262
+ check out), announces `LogoutEvent` on the firewall's own dispatcher, clears the token from the
263
+ storage and the request's security state, and sets the response a logout listener answered with
264
+ on the request's event, which also stops the chain. The event is announced before the token is
265
+ cleared, so its listeners still read the token being logged out.
266
+
267
+ **`SwitchUserListener`** is the chain's user-switch step, at the default priority `0` and
268
+ declared — through `@FirewallListenerOrder` — to run after the authentication step, so the
269
+ switch is carried out over a request the firewall has already authenticated. `supports` answers
270
+ whether the request carries the configured parameter naming the user to become — in its query
271
+ string, in the body of a request that is not safe, or in a header of the same name — or, when
272
+ the switch is scoped to a dedicated path, only at that path; `authenticate` proves the request
273
+ first (the forgery token under the configured parameter and id, when a manager is wired —
274
+ `AccessDeniedError` when it does not check out), then carries the switch out: the right to
275
+ switch is decided — the configured role, against the target user as the subject — the target
276
+ user is loaded beside a throwaway identifier no provider can know (and a target that cannot be
277
+ loaded burns a load of the current user instead, so a switch's timing never tells whether a
278
+ user exists), checked after authentication, and settled on a `SwitchUserToken` that keeps the
279
+ token the switch started from. The switch is announced as `SwitchUserEvent` under
280
+ `SecurityEvents.SWITCH_USER` before the token is stored — a listener may replace it — and the
281
+ request is answered with a redirect to the page it was asked at, the switch's own parameters
282
+ stripped from the address. The exit is the same parameter carrying `SwitchUserListener.EXIT_VALUE`
283
+ (`_exit`): the original token is restored, its user reloaded first, and the exit announced the
284
+ same way. A `stateless` step switches without answering the request.
285
+
286
+ ### Logout
287
+
288
+ A logout is carried by the `LogoutEvent` — the request asking for it and the token it logs out —
289
+ announced on the firewall's own dispatcher, where each of the firewall's logout listeners does
290
+ its part. The first response set on the event is the one the request is answered with:
291
+
292
+ | Listener | Priority | Does |
293
+ |---|---|---|
294
+ | `DefaultLogoutListener` | 64 | answers a logout nothing else answered with a redirect to the configured target (`/` by default) — the first response wins |
295
+ | `ClearSiteDataListener` | 0 | sets the `Clear-Site-Data` header on the response, with the configured directives quoted and comma-joined, unless it already carries one |
296
+ | `CsrfTokenClearingListener` | 0 | clears every cross-site request forgery token the firewall's clearable storage minted |
297
+ | `SessionLogoutListener` | 0 | invalidates the session the request's security state carries — the one the context listener parked |
298
+ | `CookieClearingListener` | -255 | adds the `Set-Cookie` headers that clear the configured cookies, repeating the path and domain each was set with (`ClearedCookie`) |
299
+
300
+ **`LogoutUrlGenerator`** generates the logout addresses of the firewalls that registered one:
301
+ `register_listener(key, path, csrf_token_id, csrf_parameter, csrf_token_manager?, context?)`
302
+ records a firewall's logout, and `get_logout_path(key?)`, `get_logout_url(key?)` and
303
+ `get_logout_form(key?)` generate its path, its URL and the action and hidden fields of a form
304
+ posting to it — the form keeps the forgery token out of the address bar. A keyless generation
305
+ resolves its firewall from the token the storage holds (the firewall that authenticated it),
306
+ then the firewall named by `set_current_firewall`, then the one sharing the current context;
307
+ `reset()` puts that tracking back to none. Paths are literal — an absolute path or an absolute
308
+ URL, never a route name — and the URL runs the path through the injected URI builder, the path
309
+ itself by default.
310
+
311
+ The bundle's logout wiring — the configuration surface that builds the listener per firewall,
312
+ parks it on the context's `logout_listener` slot, registers the event listeners on the
313
+ firewall's dispatcher and registers each firewall with the generator — is the
314
+ [xtr-security](../xtr-security) bundle's `LogoutConfig`, documented there; the pieces above
315
+ are what it wires.
316
+
317
+ ### Authenticators, passports, badges and credentials
318
+
319
+ An **`AuthenticatorInterface`** answers `supports(request)` — `True` to handle and stop others,
320
+ `False` to skip, `None` to handle lazily (let a later authenticator try if this one yields no
321
+ token) — reads the request into a `Passport` with `authenticate(request)`, and mints a token
322
+ with `create_token(passport, firewall_name)`. `AbstractAuthenticator` is the base that builds a
323
+ `PostAuthenticationToken` carrying the user and roles.
324
+
325
+ A **`Passport`** gathers a `UserBadge`, a `CredentialsInterface` to verify for that user, and any
326
+ other `BadgeInterface` badges, keyed by class (`add_badge` — with an explicit key, so a custom badge
327
+ can stand in for a built-in — `get_badge`, `has_badge`, `get_user`, attributes). A badge
328
+ `is_resolved()` reports whether it has been handled; the manager fails a passport whose badges are
329
+ not all resolved once the check is done. The badges:
330
+
331
+ | Badge | Resolves when | Carries |
332
+ |---|---|---|
333
+ | `UserBadge(identifier, user_loader=None, ...)` | always | the identifier and how to load the user |
334
+ | `AuthenticationMethodBadge(*methods)` | always | the authentication methods the attempt verified |
335
+ | `CsrfTokenBadge(csrf_token_id, csrf_token)` | the CSRF listener checked the token | the token's identifier and the presented value |
336
+ | `PasswordUpgradeBadge(plaintext, upgrader=None)` | always | a verified plaintext to rehash and store |
337
+ | `RememberMeBadge(parameters=None)` | always | whether a remember-me cookie is wanted (`enable`/`disable`), and what to check |
338
+ | `PreAuthenticatedUserBadge()` | always | nothing — the user is already trusted |
339
+
340
+ A **`CredentialsInterface`** is a badge to verify: `PasswordCredentials(password)` holds a
341
+ plaintext (`get_password()`, `mark_resolved()` — reading it after resolution is refused, the
342
+ credential is consumed), `CustomCredentials(checker, credentials)` runs a callable through
343
+ `execute_custom_checker(user)`, which fails authentication unless it returns strictly `True`. A
344
+ **`SelfValidatingPassport`** carries no credentials, for an authenticator — like the access-token
345
+ one — that already trusts the credential; it attaches no `PreAuthenticatedUserBadge` of its own, so
346
+ the account checks still run.
347
+
348
+ ### The authenticator manager and its event order
349
+
350
+ **`AuthenticatorManager(authenticators, token_storage, event_dispatcher, firewall_name, ...)`**
351
+ runs authentication for one firewall. `supports(request, state)` asks every authenticator,
352
+ parking the supporters and the skipped on the request's security state;
353
+ `authenticate_request(request, state)` reads that handoff back — once — and drives a fixed order
354
+ over the firewall's own dispatcher:
355
+
356
+ 1. the first supporter that still applies reads the request into a passport;
357
+ 2. **`CheckPassportEvent`** — listeners resolve the badges (load the user, verify the password);
358
+ 3. every badge must report resolved, and every `required_badges` type must be present, else
359
+ `BadCredentialsError`;
360
+ 4. a token is created, then **`AuthenticationTokenCreatedEvent`** lets a listener replace it;
361
+ 5. **`AuthenticationSuccessEvent`** ([core](../xtr-security-core)) runs the post-authentication
362
+ account check;
363
+ 6. the token is stored, the authenticator's success handler runs, and **`LoginSuccessEvent`** —
364
+ carrying the token the storage held before the login — lets a listener migrate a password or
365
+ replace the response.
366
+
367
+ Between the success handler and the login-success event, an authenticator that is an
368
+ **`InteractiveAuthenticatorInterface`** reporting `is_interactive()` announces the login through
369
+ **`InteractiveLoginEvent`** — a user actively logging in, as opposed to a session or a bearer token
370
+ re-establishing one — under the name `SecurityEvents.INTERACTIVE_LOGIN`. The form-login and
371
+ JSON-login authenticators are interactive — a login through either announces itself; so is the
372
+ login-link one, the user having followed the link themselves.
373
+
374
+ A lazy authenticator that raises a plain `BadCredentialsError` is read as *no credential
375
+ presented* — it did not apply, so the next authenticator is tried. Any other failure is masked to
376
+ the configured `ExposeSecurityLevel` (`NONE` hides everything as bad credentials,
377
+ `ACCOUNT_STATUS` reveals disabled/locked/expired, `ALL` reveals the failure), answered by the
378
+ authenticator's failure handler and announced as **`LoginFailureEvent`** — never raised: a
379
+ response from the handler or a listener is returned for the firewall to send, and the lack of one
380
+ lets the request go on anonymous, where the access decision is what turns it into the entry
381
+ point's challenge. `AuthenticationSuccessHandlerInterface` and
382
+ `AuthenticationFailureHandlerInterface` are the two handler shapes a route may answer with, and
383
+ `authenticate_user(user, authenticator, request, badges=(), attributes=())` on the manager logs a
384
+ known user in through one authenticator outside the request-driven flow.
385
+
386
+ ### Login forms
387
+
388
+ **`AbstractLoginFormAuthenticator(http_utils)`** is the base a form login builds on: the subclass
389
+ names its login page's URL (`get_login_url(request)` — a path, compared strictly with the
390
+ request's own), `supports` answers only a POST to exactly that page, a failure keeps the error on
391
+ the request's session for the form that renders again and redirects back to the page, `start` —
392
+ the entry point — redirects there too, and `is_interactive()` reports the login as the user's own
393
+ act.
394
+
395
+ **`FormLoginAuthenticator(http_utils, user_provider, success_handler, failure_handler, options)`**
396
+ reads a submitted form by its configured names (`username_parameter`, `password_parameter`,
397
+ `check_path`, `post_only`, `form_only`, `enable_csrf`, `csrf_parameter`, `csrf_token_id`, and the
398
+ required `login_path`) into a passport carrying a `PasswordCredentials`, a `RememberMeBadge` and an
399
+ `AuthenticationMethodBadge` — plus a `CsrfTokenBadge` when `enable_csrf` is set, and a
400
+ `PasswordUpgradeBadge` when the provider upgrades passwords — and mints a `UsernamePasswordToken`.
401
+ It also implements `ReAuthenticationEntryPointInterface`: `start_re_authentication(request, token)`
402
+ sends an already-authenticated caller back to the form to prove themselves again, keeping their
403
+ identifier and the denied attribute on the session for the page.
404
+
405
+ **`JsonLoginAuthenticator(http_utils, user_provider, responses, success_handler=None,
406
+ failure_handler=None, options=None)`** is the stateless sibling: a JSON body read by its configured
407
+ paths (`username_path`, `password_path` — a dotted path walks a nested document), answered — when
408
+ no failure handler is given — with a `401` carrying the error's message as JSON. The password is
409
+ erased from the document before its fields ride along as remember-me parameters.
410
+
411
+ **`HttpBasicAuthenticator(realm_name, user_provider, responses, logger=None)`** authenticates the
412
+ `Authorization: Basic` header's credentials and is its own entry point: a `401` carrying
413
+ `WWW-Authenticate: Basic realm="…"`, which a failed attempt is answered with again.
414
+
415
+ The answers a form login gives are delegated. **`DefaultAuthenticationSuccessHandler(http_utils,
416
+ options=None, logger=None)`** redirects by its target ladder — the `target_path_parameter` the
417
+ request carried, the path the interrupted request saved on the session (read and removed — a
418
+ return trip happens once), the `Referer` when `use_referer` is set, then `default_target_path` —
419
+ and **`DefaultAuthenticationFailureHandler(http_utils, options=None, logger=None,
420
+ firewall_name=None)`** keeps the error on the session for the form that renders again and redirects
421
+ by its own ladder (`failure_path_parameter`, `failure_path`, then `login_path`), keeping the
422
+ `Referer` as the return trip's target when `use_referer` is set and the handler knows its firewall.
423
+ **`CustomAuthenticationSuccessHandler(handler, options, firewall_name)`** and
424
+ **`CustomAuthenticationFailureHandler(handler, options)`** are the seams for an application's own
425
+ handlers, handing them the wiring's options — and the firewall's name, for a success handler that
426
+ keys by one — through duck-typed setters.
427
+
428
+ **`AuthenticationUtils(request)`** reads what the last login attempt left, for the page that
429
+ renders the form again: `get_last_authentication_error(clear_session=True)`, `get_last_username()`
430
+ and `get_re_authentication_attribute()`, each reading the request's own security state first, then
431
+ the session. **`FallbackUserLoader(inner=None)`** wraps a user loader the wiring must not
432
+ override — a badge carrying one of these keeps it through the passport check, where a badge
433
+ with no loader gets the firewall's provider. The access-token authenticator carries the
434
+ replacement rule of its own for the badges its handlers build: a loader that is missing or
435
+ marked as a stand-in is replaced by the firewall's provider, when one is configured.
436
+
437
+ ### Remember me
438
+
439
+ **`RememberMeDetails(user_identifier, expires, value)`** is the cookie's whole content: its
440
+ serialization — `:<b64url(identifier)>:<expires>:<value>`, the identifier in the URL-safe base64
441
+ alphabet, the expiry a Unix timestamp — is the format itself, and `from_raw_cookie` reads back
442
+ exactly what `str()` writes. **`RememberMeCookie`** is one cookie staged on the request's security
443
+ state under the `REMEMBER_ME_COOKIE` key, moved onto the outgoing head by
444
+ **`ResponseListener`** — a subscriber on the kernel's response event, registered on the
445
+ application's main dispatcher — which appends the finished `Set-Cookie` value the cookie renders
446
+ for itself.
447
+
448
+ A **`RememberMeHandlerInterface`** is the one voice on the cookie: `create_remember_me_cookie(request, user)`
449
+ stages it, `consume_remember_me_cookie(request, details)` spends it and loads its user,
450
+ `clear_remember_me_cookie(request)` stages its clearing. Every method takes the request, because
451
+ the cookie is staged on that request's own state — there is no request stack to read the current
452
+ one from. **`AbstractRememberMeHandler(user_provider, options, logger)`** loads the user a cookie
453
+ names and stages every cookie; its options default to the name `REMEMBERME`, a one-year lifetime,
454
+ `path=/`, `httponly`, and the two the conditions listener reads (`always_remember_me`,
455
+ `remember_me_parameter=_remember_me`). Two handlers complete it:
456
+
457
+ - **`SignatureRememberMeHandler(signature_hasher, user_provider, options, logger, clock)`** signs
458
+ the value with the core `SignatureHasher` — checked against the identifier before the user is
459
+ loaded, against the user's covered fields after — and reissues a fresh cookie on every consume.
460
+ Stateless: no series can be revoked alone, only every cookie of the user, by changing what the
461
+ signature covers.
462
+ - **`PersistentRememberMeHandler(token_provider, user_provider, options, logger, token_verifier,
463
+ signature_properties, clock)`** stores each cookie's series and value — 33 random bytes each —
464
+ through the core `TokenProviderInterface`, rotates the value on every consume (a spent value
465
+ presented again raises the core `CookieTheftError`), skips the rotation within a 60-second
466
+ window so concurrent requests do not spend one another, and binds a digest of the user's
467
+ signature properties — the password by default, properties the user does not expose skipped —
468
+ so a changed password retires the series. A verifier accepts a value the store no longer holds
469
+ exactly; the provider itself is used when it implements the verifier's contract. Clearing drops
470
+ the series the request's own cookie names.
471
+
472
+ Around the handlers: **`CheckRememberMeConditionsListener(options, logger)`** enables the passport's
473
+ `RememberMeBadge` on a successful login when the application always wants the cookie or the request
474
+ carries the remember-me parameter (the badge's own parameters first, then the request's attributes,
475
+ query, and body for a non-`GET`); **`RememberMeListener(handler, logger)`** — at a lower priority —
476
+ clears any cookie the request still carries and sets a new one when the badge was enabled, and
477
+ clears the cookie on a failed login, a logout and a deauthentication. **`RememberMeAuthenticator(handler,
478
+ token_storage, cookie_name, logger)`** spends the cookie a returning request presents: `supports`
479
+ answers `None` — lazily — for a request carrying the cookie and storing no token, `authenticate`
480
+ builds a self-validating passport whose user loader is the cookie being spent through the handler,
481
+ and the token is the core `RememberMeToken`, which a trust resolver tells apart from a fully
482
+ proven login. It reports itself interactive, as the flow always has.
483
+
484
+ ### Login links
485
+
486
+ **`LoginLinkDetails(url, expires_at)`** is one minted link: the URL whole, and the moment the
487
+ hash inside it stops verifying. A **`LoginLinkHandlerInterface`** is the one voice on the
488
+ links: `create_login_link(user, request=None, lifetime=None, parameters=None)` mints one for
489
+ a user, and `consume_login_link(request)` checks the one a request presents and loads the
490
+ user it signs in.
491
+
492
+ **`LoginLinkHandler(user_provider, signature_hasher, options, uri_builder=None, clock=None)`**
493
+ signs stateless links with the core `SignatureHasher`: the hash covers the user, the expiry
494
+ and the link's extra parameters, and is checked against the identifier before the user is
495
+ loaded, then against the loaded user's own covered fields — so a changed password retires
496
+ every link issued before it, which is also the limit: no single link can be revoked alone.
497
+ The options are `lifetime` (600 seconds by default) and `path`, the literal path every
498
+ minted link leads to, required. Extra parameter names must match `[A-Za-z0-9_.-]+` and may
499
+ not take the reserved names `user`, `expires`, `hash` or `hash_parameters`; the extras are
500
+ sorted and their names listed in the link's `hash_parameters`, each covered by the hash.
501
+ Consuming reads the link's parameters from the query string, or the body of a request that
502
+ is not a `GET`; a refused link raises `InvalidLoginLinkError` — `ExpiredLoginLinkError`
503
+ when only its moment has passed — and the extra parameters it carried are parked on the
504
+ request's security state under the `LOGIN_LINK_PARAMETERS` key, for the handlers that
505
+ answer the login.
506
+
507
+ **`LoginLinkAuthenticator(login_link_handler, http_utils, success_handler, failure_handler,
508
+ options)`** spends the link a followed request presents: `supports` answers for the request
509
+ arriving at the configured `check_path` — a `POST` there when `check_post_only` is set —
510
+ and `authenticate` builds a self-validating passport whose user loader, wrapped in
511
+ `FallbackUserLoader` so the wiring leaves it alone, is the link being consumed through the
512
+ handler, wearing a `RememberMeBadge`. The answers are delegated to the success and failure
513
+ handlers, and the login reports itself interactive: the user followed the link themselves.
514
+
515
+ ### Switch user
516
+
517
+ A user switch — one user temporarily acting as another, like the `su` command — is carried by
518
+ **`SwitchUserListener`** (see [Firewall listeners](#firewall-listeners)) and announced as
519
+ **`SwitchUserEvent`**: the request the switch was asked through, the user being switched to —
520
+ on an exit, the user returned to — and the token the switch settles on, which a listener may
521
+ replace through `set_token`. It runs under the name `SecurityEvents.SWITCH_USER`
522
+ (`security.switch_user`), announced once before the switched token is stored and once before
523
+ an exit restores the original.
524
+
525
+ **`ImpersonateUrlGenerator(config_lookup, token_storage, uri_builder=None,
526
+ csrf_token_manager=None)`** generates the addresses a switch is asked by:
527
+ `generate_impersonation_path(request, identifier, target_uri=None)` and its `..._url` twin,
528
+ `generate_exit_path(request, target_uri=None)` and its twin, and
529
+ `generate_impersonation_form` / `generate_exit_form`, answering the `ImpersonationForm` shape
530
+ the logout form has — the action, the hidden fields, the forgery field's name. The exit
531
+ addresses answer empty while the current user impersonates nobody, so a template can skip
532
+ them. How the request's firewall asks for switches is read through
533
+ **`SwitchUserConfigLookupInterface`** — `await get_switch_user_config(request)` answering the
534
+ firewall's **`SwitchUserConfig`** (the parameter name, the forgery toggle with its parameter
535
+ and id, the optional dedicated path) or `None` — the local stand-in for the bundle-level
536
+ firewall map the upstream generator reads, so whatever knows the firewalls can answer without
537
+ the generator knowing how they are held. A dedicated path carries the page to come back to as
538
+ an explicit `_target_path`; the URL methods write through the injected URI builder, which is
539
+ path-only by default. A generation over a firewall without a switch, a forgery-asking switch
540
+ without a manager, or a dedicated path configured as a route name raises
541
+ `ImpersonationError`.
542
+
543
+ ### Pre-authenticated
544
+
545
+ Some requests arrive already vouched for — the server in front of the application authenticated
546
+ them, or a TLS certificate named their user. **`AbstractPreAuthenticatedAuthenticator(user_provider,
547
+ token_storage, firewall_name, logger=None)`** is the base those authenticators share: a subclass
548
+ implements `extract_username(request)` — the one abstract part, reading the username the request
549
+ carries as already proven — and the base decides the rest. `supports` skips a request whose
550
+ reading yields no user, one whose reading raises `BadCredentialsError` (clearing the token the
551
+ failure invalidates), and one whose user already holds a `PreAuthenticatedToken` for this
552
+ firewall: the token from the session vouches for everything a new one would. `authenticate`
553
+ builds a self-validating passport whose user loader is the provider's `load_user_by_identifier`,
554
+ wearing a `PreAuthenticatedUserBadge` — the badge that tells the passport check there are no
555
+ credentials to verify and the account was vouched for before the passport existed. The token it
556
+ settles on is the core `PreAuthenticatedToken`, a failure clears the firewall's pre-authenticated
557
+ token and answers nothing, and the login reports itself interactive.
558
+
559
+ **`RemoteUserAuthenticator(user_provider, token_storage, firewall_name, user_key="REMOTE_USER",
560
+ logger=None)`** reads the account a server in front authenticated, from the environment the
561
+ request arrives with: a request whose environment carries no `user_key` is refused, and one whose
562
+ value is empty reads as no user. **`X509Authenticator(user_provider, token_storage,
563
+ firewall_name, user_key="SSL_CLIENT_S_DN_Email", credentials_key="SSL_CLIENT_S_DN", logger=None,
564
+ credential_user_identifier="emailAddress")`** reads what the server verified of the request's
565
+ client certificate: the email key first, as it is, and the configured identifier out of the
566
+ distinguished name when the email is not there — `emailAddress` also answers its dotted OID
567
+ spelling `1.2.840.113549.1.9.1`, a custom identifier is matched anywhere in the name, and the
568
+ value read out of a name is trimmed. A request carrying neither key, or a name without the
569
+ identifier, is refused.
570
+
571
+ ### Listeners
572
+
573
+ The bundle registers these on each firewall's dispatcher; they are the badge-resolving half of
574
+ the event order:
575
+
576
+ | Listener | Resolves |
577
+ |---|---|
578
+ | `UserProviderListener` | gives a `UserBadge` its loader from the firewall's user provider, unless it has one |
579
+ | `CheckCredentialsListener` | verifies a password passport's `PasswordCredentials` and `CustomCredentials` |
580
+ | `CsrfProtectionListener` | settles a passport's `CsrfTokenBadge` — the token manager must vouch for the value the request presented, or the login fails |
581
+ | `UserCheckerListener` | runs the account's pre- and post-authentication checks |
582
+ | `PasswordMigratingListener` | rehashes a password flagged by a `PasswordUpgradeBadge`, best-effort, storing through the badge's, the listener's, or the user provider's upgrader — a provider implementing `PasswordUpgraderInterface` is derived from the passport's user loader |
583
+ | `SessionStrategyListener` | nothing on a passport — hands a successful login to the firewall's session strategy, which moves the session the request reattached |
584
+ | `CheckRememberMeConditionsListener` | nothing on a passport — enables the `RememberMeBadge` when the login asked to be remembered |
585
+ | `RememberMeListener` | nothing on a passport — clears any cookie the request carried and sets the one the enabled badge earned; clears it on a failure, a logout, a deauthentication |
586
+ | `LoginThrottlingListener` | nothing on a passport — refuses an attempt already over the login limits, counts a failed one, forgets the account's count on a successful one |
587
+ | `AuthenticationProofsListener` | nothing on a passport — records the authentication methods an interactive, non-remember-me login proved on its token, and carries the previous token's proofs over for the same user |
588
+
589
+ The logout listeners — the ones answering `LogoutEvent` rather than a passport — have their own
590
+ table under [Logout](#logout); the [xtr-security](../xtr-security) bundle wires them from the
591
+ firewall's `LogoutConfig`. The remember-me pair runs on the login-success event at the priorities above; the
592
+ response listener that moves the staged cookie onto the response belongs to the kernel's response
593
+ event, on the application's main dispatcher.
594
+
595
+ **Login throttling.** The throttling listener runs first of all on the passport check (priority
596
+ 2080, above the provider listener's 2048), and answers through one request limiter —
597
+ `RequestRateLimiterInterface` from [xtr-http-foundation](../xtr-http-foundation), a peekable one
598
+ preferred. The limiter builds its own keys, and the default one —
599
+ `DefaultLoginRateLimiter` in `rate_limiter/`, over the [xtr-rate-limiter](../xtr-rate-limiter)
600
+ factories — counts a login twice: under the address it came from (a global limit, bounding how
601
+ many accounts one client may sweep) and under the account it named from that address (a local
602
+ limit). Both keys are eight-character HMAC-SHA256 prefixes over a non-empty application secret,
603
+ so a store shared across servers keeps no readable account or address; a successful login forgets
604
+ the local count only — the global one must survive the successful login of any one account. A
605
+ peekable limiter is looked ahead at on the check (nothing spent), counted on a failure; a
606
+ non-peekable one is counted on the check and still forgotten on a success. An attempt over the
607
+ limit is refused with the core `TooManyLoginAttemptsAuthenticationError`, carrying the minutes to
608
+ wait, and announced as a `RateLimitExceededEvent` on the dispatcher the listener was given.
609
+
610
+ **The dummy-hash timing guard.** When a password passport names an *unknown* user,
611
+ `CheckCredentialsListener` still verifies against a dummy hash, so the timing of a bad username
612
+ and a bad password is the same — neither reveals which was wrong. The dummy is hashed once per
613
+ listener instance and kept, so no request pays the cost twice.
614
+
615
+ ### Access map and request matchers
616
+
617
+ An **`AccessMap`** is an ordered list of rules: `add(request_matcher, attribute)` appends one,
618
+ `await get_patterns(message)` returns the first matching rule's attribute — as the one member of
619
+ a tuple — beside the channel it demands the request arrive over (`None` while no rule carries
620
+ one). An **`AccessListener(token_storage, access_decision_manager, access_map)`** is the chain
621
+ step that decides them: `supports` parks what the matched rule requires on the request's
622
+ security state and answers whether there is a decision to make, and `authenticate` reads it
623
+ back and decides the token from the token storage against it, with the request itself as the
624
+ subject, raising `AccessDeniedError` — carrying the attributes, the subject and the decision —
625
+ when it is refused. Until the chain runs the request, the same rules are decided the way the
626
+ firewall runner drives: `await check_access(message, token)` takes the token the runner
627
+ authenticated and short-circuits a public-access rule. The step sits at priority `-255`, after
628
+ the authentication step. A **`RequestMatcherInterface`** (from
629
+ [xtr-http-foundation](../xtr-http-foundation)) answers
630
+ `await matches(message)` over the request as a `ServerRequestInterface`:
631
+
632
+ | Matcher | Claims a request by |
633
+ |---|---|
634
+ | `PathRequestMatcher(pattern)` | its path (searched, not anchored; the mount prefix stripped and per-cent escapes undone) |
635
+ | `HostRequestMatcher(pattern)` | its host (matched case-insensitively) |
636
+ | `MethodRequestMatcher(methods)` | its method (empty `methods` claims every request) |
637
+ | `IpsRequestMatcher(ips)` | its client address (empty `ips` claims every request) |
638
+ | `ChainRequestMatcher(matchers)` | all of several matchers (an empty chain claims everything) |
639
+
640
+ This package keeps the escape hatch: **`CallableRequestMatcher(decide)`** defers the claim to a
641
+ callable `(message) -> bool`, for a rule none of the matchers above express.
642
+
643
+ ### The context listener: a session's token, restored and written back
644
+
645
+ **`ContextListener(token_storage, user_providers, context_key, session_storage, *, logger=None,
646
+ dispatcher=None, trust_resolver=None)`** is the chain step a *stateful* firewall runs: it reads
647
+ the token the request's session carries — under the key `_security.<context>`, the firewall's
648
+ own name composed onto the prefix every user-facing key of the flow carries — and restores it
649
+ into the token storage, after refreshing the user it carries through the firewall's user
650
+ providers. `supports()` answers `None` always — the lazy join: nothing in a request's head
651
+ commits the listener, but a stateful firewall restores its context on every request. The step
652
+ sits at the default priority `0`, tied with the authentication step and ahead of the access
653
+ step, registered before the manager so the token is restored before anything reads it.
654
+
655
+ The refresh reloads the user through each provider that supports its class, in order, and
656
+ deauthenticates the token when the reload changed the user: the password changed (with the
657
+ legacy carve-out — a stored eight-character password that is the crc32c checksum of the
658
+ reloaded one is the same password), a role was granted or withdrawn, the identifier names
659
+ someone else, or a user comparing itself says so. Each reload is weighed by the listener's own
660
+ comparison and then announced as **`CheckRefreshedUserEvent`** — seeded with that verdict, for
661
+ a listener to confirm, add its own `AuthenticationError` reason to, or clear. A deauthentication
662
+ is announced as **`TokenDeauthenticatedEvent`** — the original token, the reason, the provider
663
+ classes that contributed, the error a listener gave — and the storage is left empty, so the
664
+ request goes on anonymous. A switch token refreshes from the inside out: the original token —
665
+ the impersonator's — is refreshed first, and a reload that deauthenticates it deauthenticates
666
+ the switch with it, because a switch whose origin is gone has nothing to return to. A session
667
+ holding no token, or something that is not one — a corrupt
668
+ payload, a foreign class — leaves the storage empty the same way, without failing the request.
669
+
670
+ The response half, **`on_response(response_event)`** — registered beside the chain on the
671
+ kernel's response event — writes the token back: what the request authenticated is stored under
672
+ the same session key, and an anonymous request's key is removed, so a logout does not linger in
673
+ the session. It runs only for the requests the request half marked with its own session key —
674
+ the firewall-run mark — so a response of a request another firewall ran is left to that
675
+ firewall's listener. The token is stored as the object it is: the session storage's own contract
676
+ owns the wire format, and the lazy-object initialisation guard the upstream listener opens with
677
+ has no lazy user to guard on this stack.
678
+
679
+ The session itself reaches the storage through **`SaveSessionListener`** — the last word on
680
+ the kernel's response event, at priority `-1000`, below every other listener, so the token
681
+ the context step's response half wrote is in the session before the session is written. It
682
+ saves the session the request's security state carries and places the cookie
683
+ `staged_set_cookie()` returns on the response's `Set-Cookie` — a new or rotated ID being the
684
+ only times one is staged, so an untouched session mints no cookie. A storage that fails
685
+ fails the request loudly, before the head leaves, rather than losing the session quietly;
686
+ the finish event saves what a request that failed without producing a response still wrote,
687
+ skipping the ones the response half already saved.
688
+
689
+ ### The session strategy: what a login does to the session it reattached
690
+
691
+ A login is the moment a session's ID is most worth stealing, and
692
+ **`SessionAuthenticationStrategy(strategy, csrf_token_storage=None)`** is what the login does
693
+ about it, under the strategy the firewall configured — `SessionAuthenticationStrategy.NONE`
694
+ keeps the session as it is, `MIGRATE` carries its attributes to a fresh ID and destroys the
695
+ entry under the old one (`session.migrate(destroy_old=True)`, so a session fixation stops
696
+ answering for the account the login just named) and clears the clearable CSRF token storage
697
+ with it, so a token value stolen before the rotation cannot answer after it, and `INVALIDATE`
698
+ empties the session entirely. An unknown strategy name is refused when the strategy runs —
699
+ `InvalidSessionStrategyError`, carrying the name as configured. The strategy moves the session
700
+ the request's security state carries — the context listener's park — so the ID it rotates is
701
+ the ID the response half writes the token back under; a request whose state holds no session
702
+ is refused with `MissingSessionError` rather than silently rotated around.
703
+
704
+ **`SessionStrategyListener(session_authentication_strategy)`** hands a successful login to
705
+ that strategy: a subscriber on `LoginSuccessEvent`, on the firewall's own dispatcher, that
706
+ skips a login with no session to move — a stateless firewall, or a request no stateful chain
707
+ ran — and a re-authentication of the same account, where the previous token names the same
708
+ non-empty user identifier and is of the same class as the new one: the session already answers
709
+ for this account, and rotating it would log out the very login it celebrates. The upstream
710
+ guard is the request's *session cookie* — a request that reattached no session is skipped —
711
+ which the local session keeps private; a session made fresh for the request migrates too,
712
+ harmlessly, since its storage entry is empty and nothing stored travels with it.
713
+
714
+ ### The channel listener: the scheme a rule demands
715
+
716
+ **`ChannelListener(access_map, responses, bridge, *, logger=None, http_port=80,
717
+ https_port=443)`** is the chain step that moves a request onto the scheme its matched rule
718
+ demands it arrive over. `supports` asks the access map — the same rules the access step
719
+ decides attributes from — and answers `True` when the matched rule demands `https` of a
720
+ request that arrived over plain `http`, or `http` of one that arrived encrypted; every other
721
+ answer — no rule matched, the rule demands no channel, the request already carries the
722
+ demanded one — is `False`, the strict skip: the step parks nothing, so a request with nothing
723
+ to change has nothing to come back for. `authenticate` answers the request with a **301** to
724
+ the same request over the demanded scheme: the host, the whole path and its query string kept,
725
+ the port the *target* scheme is configured to answer on spelled out only when it is not that
726
+ scheme's default (`http_port` for `http`, `https_port` for `https`), and the port the request
727
+ itself arrived on left behind — the target scheme has a port of its own.
728
+
729
+ The scheme a request is read by is its URI's own — the connection's answer, not a forwarded
730
+ header's claim; this stack has no trusted-proxy machinery to weigh one. A proxy that
731
+ terminates the encryption in front of the server therefore leaves the request reading plain,
732
+ and the redirect this step builds would loop — the step's log names the `X-Forwarded-Proto`
733
+ and `Forwarded` headers when they claim the request was encrypted already. The step sits at
734
+ the default priority `0`, tied with the authentication step and ahead of the access step. No
735
+ rule the bundle builds carries a channel today, so no chain the bundle wires runs it yet.
736
+
737
+ ### The exception listener: 401, 403 and RFC 6750
738
+
739
+ **`ExceptionListener`** is one object per firewall, built from that firewall's parts — its
740
+ name, its entry point, its access-denied handler — and parked by the firewall on the
741
+ request's carrier, so a request's failure is answered by the firewall that ran it. It turns a
742
+ security error raised while handling into the right response, walking the failure's chain
743
+ of causes to the first one it owns:
744
+
745
+ - an **`AuthenticationError`** becomes the firewall's entry-point challenge, or a bare `401`
746
+ with `WWW-Authenticate: Bearer` when the firewall has no entry point. The path the request
747
+ was heading for is kept on the session for the return trip — unless the firewall is
748
+ stateless, or the entry point only stands in (a
749
+ `FallbackAuthenticationEntryPointInterface`) — and an account-status error clears the
750
+ token, so a disabled account cannot loop its own challenge;
751
+ - an **`AccessDeniedError`** from a caller who is not fully authenticated becomes the entry-point
752
+ challenge for insufficient authentication — naming the masked authentication error the
753
+ manager parked, when one was; from a fully authenticated caller, a fresh proof may be
754
+ asked through a `ReAuthenticationEntryPointInterface` when a recency vote asked for one
755
+ (the asked attribute parked under the re-authentication key), else a scope challenge (when
756
+ the denied attribute is a scope), the firewall's access-denied handler, or a plain `403`;
757
+ - any other exception is left untouched, to whatever else handles it.
758
+
759
+ The upstream error page — a sub-request rendering a denial — has no local counterpart: this
760
+ kernel has no sub-request mechanism, so the `error_page` parameter is kept for the surface
761
+ that will carry it and a configured page is not rendered; the refusal chain falls through to
762
+ the plain `403`. The target path is kept under `_security.<firewall>.target_path` by the
763
+ `util` helpers `save_target_path`, `get_target_path` and `remove_target_path`, over the
764
+ session and the firewall's name.
765
+
766
+ `AccessTokenAuthenticator` only stands in as an entry point (a
767
+ `FallbackAuthenticationEntryPointInterface`): `start(request, error)` answers a request no
768
+ authenticator applied to with the RFC 6750 bearer challenge — the `realm` when one is
769
+ configured, the protected-resource metadata address when one is, and the scheme and
770
+ parameters of the sender constraint when one is configured. The challenge of a request
771
+ that carried no token at all holds no error code; a token that was rejected is answered by
772
+ the authenticator's own failure hook, with `invalid_token`.
773
+
774
+ ### Decorators
775
+
776
+ Five markers put the edge on a route, and stay out of the generated schema:
777
+
778
+ | Decorator | Does |
779
+ |---|---|
780
+ | `IsGranted(attribute, subject=None, *, message=None, status_code=None, exception_code=None, methods=(), consume_security_scopes=False)` | require an attribute, raising `AccessDeniedError` (or answering `status_code`) |
781
+ | `CurrentUser(user_class=None, *, optional=False, disabled=False)` | inject the current user; `optional` yields `None` when anonymous, `disabled` switches the resolution off |
782
+ | `CurrentToken(*, optional=False)` | inject the authenticated token; `optional` yields `None` when anonymous |
783
+ | `IsCsrfTokenValid(token_id, *, token_key="_token", methods=(), token_source=SOURCE_PAYLOAD)` | require the request to present the value minted for `token_id` |
784
+ | `IsGrantedContext` | the context a closure `IsGranted` attribute is handed (from [core](../xtr-security-core)) |
785
+
786
+ Every marker is pure metadata: the enforcement lives in an injectable service in
787
+ `resolver/` — `UserValueResolver`, `SecurityTokenResolver`, `IsGrantedChecker`,
788
+ `CsrfTokenChecker` — each built by hand from its constructor or filled from a
789
+ container, and each testable without the web framework. The thin dependency a
790
+ route carries delegates to them.
791
+
792
+ `IsGranted`'s `subject` is `None`, the name of a path parameter, or a callable used as a
793
+ dependency; its `attribute` is a string a voter matches or a callable
794
+ `(IsGrantedContext, subject) -> bool`. `methods` limits a check to those methods
795
+ (upper-cased; `GET` extends to `HEAD`); an empty list applies to every method.
796
+ `CurrentUser` raises `AuthenticationCredentialsNotFoundError` on an anonymous
797
+ request (a `401`), or `UnsupportedUserError` when the user is not of the
798
+ asked-for class — or of any of the classes, given a tuple. `CurrentToken` raises
799
+ the same on an anonymous request. `IsCsrfTokenValid` reads the presented value
800
+ from the sources its `token_source` bitmask names — `SOURCE_PAYLOAD` (the JSON
801
+ body, else the parsed form; the default), `SOURCE_QUERY`, `SOURCE_HEADER` — the
802
+ first named source that carries one — and refuses the request with a `403` when
803
+ the token manager does not vouch for it.
804
+
805
+ **The security scopes a route declares natively are never consumed silently.**
806
+ `IsGranted` ignores the request's accumulated `SecurityScopes` unless the route
807
+ opts in with `consume_security_scopes=True`; opted in, each accumulated scope is
808
+ decided through the voter stack too, as an `oauth2_scope(...)` attribute that
809
+ must be granted.
810
+ Without the opt-in, `Security(..., scopes=[...])` stays what declaring it alone
811
+ ever makes it: documentation.
812
+
813
+ ### Deviations and decisions
814
+
815
+ - **The OIDC discovery reader is async where its upstream twin is not.** The
816
+ cache contract here is awaitable — a fetch-or-compute over I/O — so
817
+ `get_secure_endpoint` and `get_issuer` are `async def` where the upstream
818
+ reader answers synchronously from a memoized document. Callers await them;
819
+ the semantics are the upstream ones.
820
+ - **`OidcDiscovery.prefetch` cannot probe the cache.** The cache contract
821
+ offers no read that computes nothing, so a prefetch sends its request
822
+ unconditionally (unless a response is already in flight or the document
823
+ memoized) where the upstream one stays quiet when the entry is warm; the
824
+ prefetched response is dropped unread when the cache answers a warm entry,
825
+ exactly as upstream drops it.
826
+ - **The discovery reader caches unconditionally.** The upstream reader skips
827
+ saving when the URL that served the document is unknown; the cache contract
828
+ here has no "do not keep" answer for a computed value, so the entry is saved
829
+ with the URL it was served by, empty or not.
830
+ - **The discovery key-set provider keeps a no-op `aclose`.** The provider no
831
+ longer owns an HTTP client — the application's is injected — so it holds
832
+ nothing to release; the method stays so the wiring that collects closeable
833
+ providers needs no special case.
834
+ - **The discovery key-set provider serves stale keys no longer.** The rebuilt
835
+ provider keeps its keys in the injected cache, whose expiry is the
836
+ provider's freshness boundary; the in-memory `max_stale` ceiling and the
837
+ cold-start failure memo of the previous design went with it, as did the
838
+ same-origin `jwks_uri` check — the aligned discovery weighs the announced
839
+ endpoint's scheme and the issuer the document announces instead, which is
840
+ the upstream shape.
841
+ - **The OIDC client omits the DPoP seam.** The upstream client takes an
842
+ optional proof factory and asks for a bound access token when one is given;
843
+ the proof machinery is deferred here, so the client always asks for a
844
+ bearer token and the single-retry hook stays the access token type's
845
+ `on_response` — which the bearer type answers `False`.
846
+ - **The OIDC login tracks its pending attempts in an index attribute.** The
847
+ upstream authenticator enumerates the session's attribute names to find
848
+ its pending attempts; the local session contract exposes no enumeration,
849
+ so the states are kept in a list under
850
+ `_security.oidc_login.<firewall>.attempts`, oldest first, and the attempt
851
+ keys are derived from it. The cap (five, oldest dropped first), the
852
+ constant-time state match and the consume-on-callback semantics are the
853
+ upstream ones, pinned by tests.
854
+ - **The OIDC login validates the ID token against `get_issuer()`.** The
855
+ upstream authenticator reads the issuer straight off the discovery
856
+ document's configuration; the local one goes through
857
+ `OidcDiscovery.get_issuer()`, which requires an expected issuer to have
858
+ been configured — a discovery document nobody vouched for cannot name the
859
+ audience of the flow. The value validated against is the same one.
860
+ - **The OIDC login's redirects answer through `HttpUtils`.** The upstream
861
+ entry point returns a bare redirect response; the local one goes through
862
+ `create_redirect_response`, so a configured domain regexp weighs the
863
+ provider's authorization endpoint too — a further restriction, never a
864
+ widening, and the family's convention.
865
+ - **The OIDC login folds the upstream's logic exceptions into
866
+ `InvalidArgumentError`.** The upstream constructor guard raises an
867
+ invalid-argument exception and the runtime guards (a listener setting a
868
+ managed parameter, a public client without PKCE or a signature verifier)
869
+ a logic one; the local taxonomy has no separate logic-error class, so all
870
+ four raise `InvalidArgumentError`, and a request without a session raises
871
+ `MissingSessionError` — the stateful family's refusal.
872
+ - **The ID token reader validates claims itself.** The upstream reader leans
873
+ on the JOSE library's claim-checker manager for `iss`/`aud`/`exp`/`iat`/`nbf`
874
+ and hand-writes the OIDC-specific rules (`sub`, `azp`, `nonce`, `max_age`).
875
+ The local JOSE library does have a claims registry — its clock is injectable
876
+ and its leeway maps to the allowed drift — but it reads a time claim with a
877
+ bare `isinstance(value, (int, float))`, which accepts `true` as the timestamp
878
+ `1` where the upstream check refuses it; it dispatches per claim in the
879
+ token's document order, where which error a multi-defect token surfaces
880
+ first follows the manager's fixed checker order upstream and does here; and
881
+ its terse library errors would need translating into the reader's texts
882
+ regardless. The rules are written out against the injected clock: the same
883
+ set, in the same fixed order, with the same refusal of a non-numeric time
884
+ claim.
885
+ - **No controller-arguments events.** The upstream listeners that enforce the
886
+ route markers run on a controller-arguments event the
887
+ [kernel](../xtr-http-kernel) deliberately lacks — the web framework resolves
888
+ arguments. Enforcement here lives in the injectable services in `resolver/`,
889
+ consumed by thin dependencies on the route: the same split, dispatched where
890
+ this stack dispatches.
891
+ - **No expression language.** An `IsGranted` attribute or subject written as an
892
+ expression, and an `IsCsrfTokenValid` id written as one, are not supported:
893
+ the workspace ships no expression component. `IsGranted`'s attribute and
894
+ subject accept strings and callables; an `IsCsrfTokenValid` id is a plain
895
+ string — the expression and callable forms it takes upstream are not ported.
896
+ - **`exception_code` is recorded, not applied.** The local error taxonomy
897
+ carries no exception-code channel, so `IsGranted(exception_code=...)` is
898
+ metadata a caller reads off the marker; the denial itself carries the
899
+ attribute, the subject and the decision.
900
+ - **Nullability is stated on the marker.** The web framework does not hand a
901
+ dependency the annotation of the parameter it fills, so `optional` on
902
+ `CurrentUser`/`CurrentToken` states what `User | None` would declare, and the
903
+ upstream argument-default rule translates to that same `optional`.
904
+ - **`IsCsrfTokenValid` answers `403` directly.** A request that fails the token
905
+ check is refused with a plain `403` — the caller is already identified —
906
+ rather than routed through the firewall's exception listener.
907
+ - **The local hardenings are kept.** Four softenings upstream allows are not
908
+ taken here: a password passport naming an unknown user is still verified
909
+ against a dummy hash — the guard's own hasher a required constructor
910
+ parameter, the dummy computed once per listener — so a bad username and a
911
+ bad password cost the same time and neither reveals which was wrong; an
912
+ authentication failure is masked to the configured `ExposeSecurityLevel`,
913
+ so a refused credential reads the one message whatever part of it failed;
914
+ and the password-migrating listener takes its upgrader and its logger as
915
+ explicit constructor parameters rather than discovering them at runtime.
916
+ The form-body extractor's 64 KiB cap is measured on the body itself, not
917
+ trusted from the declared content length. None of the four changes what a
918
+ correct caller reads; each closes what a wrong one could measure.
919
+ - **`PostAuthenticationToken` has no credentials to ask for.** Upstream's
920
+ token carries a `get_credentials` that answers an empty list; the local
921
+ token stack has no credentials concept — a password is a badge, consumed
922
+ at the passport check — so the method is not ported, and nothing calls
923
+ for one.
924
+ - **Generated addresses are literal paths, or the application's own URIs.**
925
+ No routing component exists to turn a route name into an address, so the
926
+ logout URL generator, the login-link handler, the impersonate generator
927
+ and the default success handler's redirects all take literal paths —
928
+ route-name options are refused — and anything that writes an absolute URL
929
+ writes it through the injected URI builder, the path itself by default.
930
+ The per-feature bullets below record each refusal where it lives.
931
+ - **No lazy response event.** The firewall subscriber runs every matched
932
+ request's chain eagerly, at the request event before routing, and the
933
+ token storage is written as the chain runs — there is no lazy token
934
+ storage for a lazy response event to serve, so the event is not ported
935
+ and the exception listener's lazy branch stays vacant, as its own
936
+ documentation says. A consequence: nothing defers a chain step to the
937
+ response phase; what a firewall does, it does before the route runs.
938
+ - **The exception listener weighs a parked token first, the storage
939
+ second.** A chain step that parks the request's token — the access step
940
+ is the one that does — decides what a denial weighs; a step that runs
941
+ before it, like the switch's, has nothing parked, so the listener falls
942
+ back to the token the storage holds. Without the fallback a switch's
943
+ denial on a restored session would be read as an anonymous caller and
944
+ answered with the entry point's redirect instead of the `403` it earned.
945
+ - **The chain's request carries its body unparsed.** The firewall runs at
946
+ the request event, where the served message is born with its body lazy:
947
+ `get_parsed_body()` answers what has been materialised so far, and nothing
948
+ in the chain materialises it — that happens where the route's own request
949
+ service is built. A reader that awaits the body (the form-encoded
950
+ access-token extractor) materialises it itself; the plain parameter
951
+ readers see nothing of the body at chain time, so a submission's fields
952
+ ride the query string, the fallback the landed readers document — a form
953
+ login with `post_only` off reads the query first, and the logout's, the
954
+ switch's and the login link's proofs all read the query string beside the
955
+ body. A login page's own route, running after the materialisation, reads
956
+ the body as ever.
957
+ - **A route cannot read the chain's parked state through its own request
958
+ message.** What the chain parks — the session the context listener
959
+ reattached, the error a failed login left, the last username — lives on
960
+ the kernel event's state bag, handed to the chain's listeners on a
961
+ derived request; the request service a route resolves builds its own
962
+ message, without the state-bag attribute, so `parked_session_of` answers
963
+ `None` for it and an `AuthenticationUtils` built over it reads nothing.
964
+ The honest pattern for a login page today is to build a session view of
965
+ its own — `Session.from_request(request, storage)` over the application's
966
+ `SessionStorageInterface` service — and read the user-facing keys
967
+ (`_security.last_error`, `_security.last_username`,
968
+ `_security.re_authentication_attribute`) from it.
969
+ - **`authenticate_user` hands its hooks the caller's request as it
970
+ arrived.** The manager's programmatic login takes no state, so the
971
+ success handlers it runs read no security state off the request they are
972
+ handed — a handler that stages on it, like the remember-me flow's, has
973
+ nothing to stage on unless the caller parked a state bag on the request
974
+ first.
975
+ - **`failure_forward` is refused.** The default failure handler answers with
976
+ the redirect only: this stack has no sub-request to hand the login page to,
977
+ so a configuration asking for the forward is a wiring mistake reported where
978
+ it would cost a request, not a request answered as if it had been forwarded.
979
+ - **A malformed form field fails the login, not the request.** A username or
980
+ password that is missing, empty or not a string at all raises
981
+ `BadCredentialsError` — the failure handler answers it — where the upstream
982
+ stack answers some of them with a bare `400`: no HTTP-status distinction
983
+ lives in this stack's error taxonomy, and a form login is a form answered,
984
+ not an API request rejected.
985
+ - **The `Referer` is weighed by path.** The URI builder is path-only (no
986
+ routing component), so the pages a referer must not win — the login page,
987
+ the page the request asked for — are recognised by their paths, and the
988
+ failure handler's save checks the referer's host against the request's own.
989
+ - **`FormLoginAuthenticator` requires a `login_path` option.** Upstream's
990
+ bundle config guarantees one; here the constructor refuses to build without
991
+ it, where upstream would fail later on a missing array key.
992
+ - **`JsonLoginAuthenticator` and `HttpBasicAuthenticator` take a response
993
+ factory.** Upstream constructs its `JsonResponse`/`Response` directly; here
994
+ the class names no response class, so the factory is a constructor parameter.
995
+ - **The throttling listener reads the request off the passport event.** Upstream
996
+ reaches the current request through a request stack, and parks the username
997
+ on the request's mutable attributes; here `CheckPassportEvent` carries the
998
+ request (the manager passes it), and the username is parked on the request's
999
+ security state — the per-request carrier — because a request message here has
1000
+ no mutable attributes. The exceeded-rate-limit event is normalised into the
1001
+ rate-limiter package's `RateLimit` value object, which is the state its
1002
+ constructor declares, rather than passing a third-party limiter's own answer
1003
+ through unchecked.
1004
+ - **The remember-me handlers take the request.** Upstream pulls the main request from a request
1005
+ stack; this stack has none, so every handler method takes the request the flow is running and
1006
+ stages the cookie on that request's own security state.
1007
+ - **The staged cookie is a value object, rendered where it is read.** Upstream stages a framework
1008
+ `Cookie` and the response listener sets it on the response; here the staged `RememberMeCookie`
1009
+ renders its own finished `Set-Cookie` value — through the [foundation](../xtr-http-foundation)
1010
+ cookie helpers — and the response listener appends it to the outgoing head. The response
1011
+ listener keeps no main-request guard: this kernel has no sub-requests.
1012
+ - **The persistent handler reads properties, not property paths.** Upstream walks `foo[bar]`
1013
+ paths through a property accessor; here a signature property is a plain attribute name read
1014
+ with `getattr`, the same reading the core `SignatureHasher` does.
1015
+ - **The remember-me parameter is a plain name.** Upstream evaluates `foo[bar]` paths against the
1016
+ request; the conditions listener reads one name — the badge's parameters first, then the
1017
+ request's attributes, query, and body for a non-`GET`.
1018
+ - **An unsupported user class is not logged by the authenticator.** Upstream's
1019
+ `RememberMeAuthenticator` logs `UnsupportedUserException` at warning; the local
1020
+ `UnsupportedUserError` derives from `SecurityError` and `TypeError`, never from
1021
+ `AuthenticationError`, so it cannot reach the failure hook and the branch has no local
1022
+ counterpart.
1023
+ - **Login links lead by a literal path, not a route name.** Upstream generates the link's
1024
+ URL from a route name through a routing component this stack does not have; the handler's
1025
+ `path` option and the authenticator's `check_path` name the literal path, a `route_name`
1026
+ or `check_route` option is refused, and the URI a path becomes is written by the injected
1027
+ URI builder — the path itself by default, and the builder receives the request the link
1028
+ is generated against, the translation of building the link for that request's own
1029
+ address.
1030
+ - **The login-link notification is not ported.** Upstream's `LoginLinkNotification` wraps
1031
+ a minted link for a notifier component this workspace does not ship; sending the link is
1032
+ the application's own.
1033
+ - **A non-string `user` parameter is refused as missing.** Upstream would hand a repeated
1034
+ query parameter's array to the badge and fail later; the local authenticator answers the
1035
+ same "Missing user from link." error for a value that is not a string.
1036
+ - **A user named `"0"` is a user.** Upstream's truthiness reads the string `"0"` as empty,
1037
+ so a link minted for such an identifier could never be consumed there; the local readers
1038
+ test presence, not truthiness, and the identifier is loaded like any other.
1039
+ - **The login-link marker is a base class, not a protocol.** Every refusal the handler
1040
+ raises — `InvalidLoginLinkError`, `ExpiredLoginLinkError` — inherits
1041
+ `InvalidLoginLinkErrorInterface`, a plain `Exception` subclass, because the
1042
+ authenticator catches it: a `Protocol` would answer a runtime check for anything, and a
1043
+ catch must name a base the errors actually derive from.
1044
+ - **The switch listener takes its HTTP utilities and a bridge as required parameters.**
1045
+ Upstream takes its HTTP utilities optionally — needed only with a dedicated path — and
1046
+ builds the non-path redirect directly; here they are the one redirect builder — the
1047
+ class names no response class — and the bridge converts the built message into what the
1048
+ kernel's event accepts, so both are constructor parameters.
1049
+ - **The switch's target route is not ported.** Upstream can redirect a completed switch to
1050
+ a configured `target_route` through a routing component; no routing component exists here,
1051
+ so the redirect falls to the request's own stripped URI — or the submitted `_target_path`
1052
+ and then `/` on a dedicated path — and the generator refuses a dedicated path configured
1053
+ as a route name.
1054
+ - **The query strip is written on the URI's text.** Upstream removes the switch's parameters
1055
+ from the request's query bag and rebuilds the query string; a request message is immutable
1056
+ here, so the strip removes each parameter's pattern from the URI text and mends the join,
1057
+ keeping the spelling the client sent for the parameters that remain.
1058
+ - **The burned identifier is hashed with the standard library.** Upstream derives its
1059
+ timing-equalisation identifier with its own 128-bit hash; here SHA-256 over random bytes
1060
+ serves the same purpose — an unguessable identifier no provider can know — without a
1061
+ hashing dependency.
1062
+ - **The impersonate generator takes the request as a parameter.** Upstream reads the
1063
+ current request from a request stack; this stack has none, so every generate method
1064
+ takes the request it generates for, and the URL methods write through the injected URI
1065
+ builder — the path itself by default, an absolute URL when the application's builder
1066
+ writes one.
1067
+ - **The generator reads the firewall's switch configuration through a local interface.**
1068
+ Upstream reaches into the bundle's own firewall map; here
1069
+ `SwitchUserConfigLookupInterface` answers the switch-user configuration of the request's
1070
+ firewall — or `None` — so whatever knows the firewalls can answer, and the generator
1071
+ works with no bundle behind it.
1072
+ - **A non-string switch parameter reads as no username.** Upstream would hand a repeated
1073
+ query parameter's array on and fail later; the local readers narrow to text, so anything
1074
+ but a name is not a switch this flow can carry out.
1075
+ - **The pre-authenticated username is read twice, not handed over.** Upstream parks the
1076
+ username its supports pass extracted on the request and reads the park back when
1077
+ authenticating; a request message is immutable here, and the pass that authenticates is
1078
+ handed the request as it arrived — not the one the supports pass read — so
1079
+ `authenticate` reads the request again through the same `extract_username` hook. The
1080
+ reading is the request's own environment, so it answers what it answered before.
1081
+ - **The access-token authenticator answers lazily on a tokenless request.** Upstream's
1082
+ `supports` answers `False` when its extractor finds no token, collecting the reason for
1083
+ the tooling; the local extractor's read is async and the supports pass is sync, so the
1084
+ miss cannot be answered there without inventing a synchronous reading the extractor
1085
+ contract does not carry. The miss is answered one step later — `authenticate` raises the
1086
+ no-token error and the manager's lazy abstain skips the authenticator — which leaves the
1087
+ request anonymous exactly as a `False` would.
1088
+ - **A marked user loader is the wiring's word, not the authenticator's.** The
1089
+ `UserProviderListener` leaves a `FallbackUserLoader`-marked loader alone — the marker's
1090
+ word to the wiring — while the access-token authenticator replaces a marked loader with
1091
+ the firewall's provider when one is configured, the rule the upstream authenticator
1092
+ itself carries: a handler that knows how to load its own user marks its loader so the
1093
+ wiring keeps it, and a stand-in loader is the provider's to replace.
1094
+ - **The introspection handler reads the members as they are spelled.** Upstream re-cases every
1095
+ claim name before building the user; the local `OAuth2User` speaks RFC 7662's own spelling
1096
+ (`client_id`, `updated_at`), so the members keep the names the response carried and the
1097
+ normalisations — the `updated_at` moment, the two verified flags — read the response's
1098
+ spellings too.
1099
+ - **`enable_signed_response` requires its key set.** Upstream accepts none because its
1100
+ discovery variant fills the keys from the authorization server's metadata; that discovery
1101
+ machinery is the OIDC discovery alignment's, so until it lands the key set is a required
1102
+ argument and the discovery setter is not ported.
1103
+ - **An empty algorithm allow-list is refused.** The JOSE library reads an empty allow-list as
1104
+ *no restriction*, so a handler configured without algorithms would verify a signed response
1105
+ against anything; the setter refuses the empty list rather than silently disabling the
1106
+ restriction.
1107
+ - **The OIDC errors end in `Error`, not `Exception`.** The local convention names every error
1108
+ the package raises with the `Error` suffix, so the OIDC family's `InvalidSignatureError` and
1109
+ `MissingClaimError` follow it; both derive from the core `AuthenticationError`, as their
1110
+ upstream counterparts do from the authentication exception.
1111
+ - **`InvalidSignatureError` is raised by nothing yet.** Upstream's verifier-side OIDC handler
1112
+ raises it on a bad signature; the local one landed earlier and reports the same failure as an
1113
+ `InvalidAccessTokenError` directly, so the error completes the family's surface — the
1114
+ signature-verification side names it — without a raiser of its own.
1115
+ - **The generator takes an algorithm allow-list, not an algorithm manager.** The JOSE library
1116
+ signs by the header's `alg`, so the manager's list becomes a sequence of names the generator
1117
+ validates a chosen algorithm against; the signing key is chosen with the library's own key-set
1118
+ selection, which picks among the suitable keys rather than the first.
1119
+ - **The userinfo handler rides the `oidc` extra.** It needs no JOSE library — only the HTTP
1120
+ client and cache contracts — but it lives in the OIDC subpackage, whose import gate requires
1121
+ the extra; an application using only the userinfo handler installs it anyway.
1122
+ - **The userinfo handler fetches its own discovery document.** Upstream delegates to a discovery
1123
+ reader the local side does not have yet — the OIDC discovery alignment owns it — so the
1124
+ handler reads the well-known document itself, through its own client and cache, and weighs the
1125
+ announced endpoint against the URL that served the document exactly as that reader would.
1126
+ When the alignment lands, the fetch moves behind it.
1127
+ - **The userinfo handler keeps no memo to reset.** Upstream memoizes the decoded document per
1128
+ instance and resets it between requests; here the cache is the memo — every token reads it,
1129
+ as the introspection handler reads its own — so there is nothing to reset and no reset hook.
1130
+ - **The trait refuses with the access-token error.** Upstream's shared behaviour throws a
1131
+ generic runtime error its handlers wrap; here it raises `InvalidAccessTokenError` directly —
1132
+ the failure currency of the access-token handlers it serves — which those handlers log and
1133
+ re-raise as-is.
1134
+ - **The client suite's audience is an issuer string, not a discovery reader.** Upstream's
1135
+ `AbstractClientAssertion` takes a discovery object and reads its issuer; the discovery
1136
+ reader is the OIDC client alignment's own deliverable, so the local base takes the
1137
+ issuer identifier outright — a caller that has a reader hands its `issuer` over, and
1138
+ nothing here knows the reader.
1139
+ - **The MAC key length is measured, not exercised.** Upstream signs once with the secret
1140
+ at construction to surface one its library rejects; the local JOSE library warns where
1141
+ the upstream one refuses, so `ClientSecretJwt` measures the RFC 7518, Section 3.2
1142
+ minimum — as many bytes as the digest produces — itself.
1143
+ - **The PKCE refusal is an invalid argument.** Upstream answers a code exchange without
1144
+ a verifier with a logic exception; the local error taxonomy carries no logic error, and
1145
+ the refusal names a call missing its `code_verifier`, so it raises
1146
+ `InvalidArgumentError` — a malformed call.
1147
+ - **The assertion's default `typ` is suppressed.** The local JOSE library adds
1148
+ `typ: JWT` to a header that names none; upstream's builder adds nothing, so the local
1149
+ assertion is typed only when it names the issuer.
1150
+ - **The algorithms map names key types, not algorithm classes.** The local JOSE library
1151
+ selects an algorithm by its JWA name, so `JwsAlgorithms` maps each name onto what it
1152
+ demands of its key — the key type, and the curve for ECDSA — which is what
1153
+ `check_key` reads.
1154
+ - **DPoP is deferred, and the seams it lands through are already here.**
1155
+ Upstream's `OAuth2/` also carries a `DpopTokenType` — a per-origin nonce
1156
+ memory and a retry on `use_dpop_nonce` — and a `Dpop/DpopProofFactory`,
1157
+ minting a proof (`jti`/`htm`/`htu`/`iat`/`ath`, and the nonce the server
1158
+ answered) in ES256 by default, its thumbprint binding the token. Neither is
1159
+ landed, so every token this package asks for or challenges is a bearer one.
1160
+ The follow-up that lands them plugs into what the landed code already
1161
+ carries: a proof check is a `SenderConstraintInterface` — the async
1162
+ `check(request, access_token, claims)` weighs the request against the
1163
+ token's `cnf` before anything is loaded on the user's behalf, and the sync
1164
+ `get_challenge(error)` names the scheme and the parameters every challenge
1165
+ answers with, the access-token challenge builder letting the constraint's
1166
+ parameters override its own; the nonce retry is the access token type's
1167
+ `on_response(response, url)`, the one retry its contract names and the
1168
+ bearer type answers `False` to; the client asks for a bound token by
1169
+ holding the token type as a property — `OidcClient` keeps
1170
+ `BearerTokenType` there today, the upstream client's optional
1171
+ proof-factory parameter being the one seam not carried; and `dpop_jkt`
1172
+ stays on the OIDC login's managed authorization parameters, guarded so no
1173
+ configuration can set it, though nothing mints one yet.
1174
+ - **The ended-sessions key hashes the `sid` with SHA-256, not xxh128.** Upstream hashes the
1175
+ opaque `sid` with xxh128; the standard library offers no xxh128, and the family's keys —
1176
+ the discovery document's, the JWKS's — are SHA-256, so the ended-session key is too:
1177
+ `oidc_ended_session.<namespace>.<sha256(sid)>`. The scheme is the upstream one; only the
1178
+ digest differs, and no hashing dependency is added for it.
1179
+ - **The logout token's claim rules are hand-written.** The ID-token reader's recorded
1180
+ deviation extends here: the JOSE library's claims registry accepts a boolean as the
1181
+ timestamp `1` where the upstream check refuses it, dispatches per claim in the token's
1182
+ document order, and its terse errors would need translating regardless — so the logout
1183
+ token's rules are written out against the injected clock, the same set in the same fixed
1184
+ order (mandatory claims first, the family's order, where the upstream checker manager runs
1185
+ its checkers before the mandatory pass), with the same refusal of a non-numeric time
1186
+ claim.
1187
+ - **The end-session redirect goes through `HttpUtils`.** Upstream answers the logout with a
1188
+ bare redirect response; the local listener builds it through
1189
+ `create_redirect_response`, so a configured domain regexp weighs the provider's
1190
+ end-session endpoint too — a further restriction, never a widening, and the same
1191
+ convention the OIDC login's redirects follow. The `post_logout_redirect_uri` is written
1192
+ by the injected URI builder — the path itself by default, an absolute URI when the
1193
+ application's builder writes one — where upstream builds the absolute URI from the
1194
+ request; this stack has no routing component, and the builder is the application's own
1195
+ way to write absolute URIs.
1196
+ - **The OIDC listeners keep their client machinery as typing-only references.** The
1197
+ back-channel logout listener holds an `OidcEndedSessions` and the end-session listener an
1198
+ `OidcDiscovery`, both imported only for type checking: a runtime import would execute
1199
+ the `oidc` package's gate and pull the JOSE toolkit behind the `oidc` extra into every
1200
+ application importing this package, listener or no listener. The wiring that builds them
1201
+ resolves the classes where the extra is already required — the same shape as the token
1202
+ refresh listener's refresher.
1203
+ - **The token-generate command keeps no completion maps.** The upstream command's
1204
+ registration also collects the algorithms and issuers each firewall declared, to suggest
1205
+ them while the shell completes an option; the console here has no completion surface, so
1206
+ the registry the command takes carries only the firewall names and their generators.
1207
+ - **The token-generate command reads `--not-before` with the clock's own grammar.** The
1208
+ upstream command hands the option to its platform's date parser, which reads any English
1209
+ date phrase; the local command reads it with the [xtr-clock](../xtr-clock) modifier
1210
+ grammar — an offset like `+1 hour`, a keyword like `tomorrow`, an ISO-8601 datetime, a
1211
+ trailing zone — counted from the injected clock, and anything else is refused with the
1212
+ upstream message. An explicitly empty value is refused rather than quietly read as "no
1213
+ not-before", which is what the upstream truthiness happens to do with it.
1214
+ - **The token-generate command reports refusals itself.** Where the upstream command lets
1215
+ its refusals escape for the console to render, the local one writes each through the
1216
+ style's error block and fails the run — the family's command convention — keeping the
1217
+ upstream messages, with the upstream's "Please choose an firewall" article mended.
1218
+
1219
+ ### Access tokens
1220
+
1221
+ An **`AccessTokenExtractorInterface`** pulls a bearer token out of a request; an
1222
+ **`AccessTokenHandlerInterface`** turns a token string into a `UserBadge` with
1223
+ `get_user_badge_from(token)`, raising `InvalidAccessTokenError` when it cannot be trusted. The
1224
+ extractors:
1225
+
1226
+ | Extractor | Reads a token from |
1227
+ |---|---|
1228
+ | `HeaderAccessTokenExtractor(header_name="Authorization", token_type="Bearer")` | a header, after a scheme prefix |
1229
+ | `QueryAccessTokenExtractor(parameter_name="access_token")` | a query parameter |
1230
+ | `FormEncodedBodyExtractor(field_name="access_token")` | a form field |
1231
+ | `ChainAccessTokenExtractor(extractors)` | the first that finds one |
1232
+
1233
+ **`AccessTokenAuthenticator(handler, extractor, user_provider=None, ..., realm=None,
1234
+ resource_metadata_uri=None, sender_constraint=None)`** reads the token, validates it through
1235
+ the handler, and builds a `SelfValidatingPassport`; it copies the badge's granted scopes —
1236
+ read from its `scope` or `scp` attribute, the first that reads — onto the token as
1237
+ `oauth2_scope`. A `sender_constraint` (a `SenderConstraintInterface`) weighs the request
1238
+ against what the token says it is bound to before anything is loaded on the user's behalf,
1239
+ and names the scheme and the parameters every challenge answers with; a failed
1240
+ authentication is always answered — a `401` whose `WWW-Authenticate` names `invalid_token` —
1241
+ and the `resource_metadata_uri` (RFC 9728) is advertised in every challenge, a path
1242
+ resolved against the request.
1243
+
1244
+ ### OAuth2 introspection
1245
+
1246
+ **`Oauth2TokenHandler`** (in `access_token/oauth2/`) validates a bearer token the only way a
1247
+ resource server can trust one it did not issue: it asks the authorization server (RFC 7662).
1248
+ The endpoint is reached through the HTTP client the handler is given — a scoped client whose
1249
+ `base_uri` is the introspection endpoint and whose `auth_basic` carries this resource server's
1250
+ credentials, so the handler knows nothing of the transport:
1251
+
1252
+ ```python
1253
+ from xtr_http_client import HttpxHttpClient
1254
+ from xtr_security_http.access_token.oauth2 import Oauth2TokenHandler
1255
+
1256
+ handler = Oauth2TokenHandler(
1257
+ HttpxHttpClient().with_options(
1258
+ base_uri="https://as.example/introspect",
1259
+ auth_basic=("resource-server", "secret"),
1260
+ ),
1261
+ audiences="shop-api",
1262
+ issuer="https://as.example",
1263
+ )
1264
+ ```
1265
+
1266
+ The constructor takes the client, an optional logger, the `audiences` this resource server
1267
+ answers for, the `issuer` of the authorization server, the `claim` the user identifier is read
1268
+ from (`sub` with `username` falling back by default), a clock and the `allowed_time_drift` on
1269
+ the time members. Only `active`, `exp`, `nbf`, `iat`, `iss` and `aud` are read — the last two
1270
+ only when the handler was given the ones it expects — and the badge carries the identifier, a
1271
+ loader that builds the core `OAuth2User` the response describes, and the members themselves as
1272
+ the badge's attributes, so the access-token authenticator reads the granted scopes out of them.
1273
+ Every failure becomes an `InvalidAccessTokenError` whose public text is `Invalid credentials.`
1274
+ — the specific reason is logged and chained.
1275
+
1276
+ `enable_cache(cache, key_prefix, ttl=60)` keeps the responses of active tokens for at most
1277
+ `ttl` seconds — never past the `exp` the server reported, and an inactive response is not
1278
+ cached at all. Entries are keyed by the digest of the token, so a pool holds no usable
1279
+ credential. `enable_signed_response(algorithms, keyset=None, enforce=True)` asks for the
1280
+ response as a signed JWT (RFC 9701) and verifies it — the `typ` header, the signature against
1281
+ the key set under the algorithm allow-list, the mandatory top-level claims, and the RFC 7662
1282
+ members nested inside `token_introspection`; it needs joserfc, which the `oidc` extra carries.
1283
+ The verifying keys may also be read from the authorization server's metadata instead of a
1284
+ pinned key set: `enable_signed_response_discovery(cache, client, cache_key)` builds an
1285
+ `OidcDiscovery` over the RFC 8414 metadata URL derived from the handler's `issuer` (required
1286
+ for that path), keeps the document under `cache_key + ".document"` and the keys under the bare
1287
+ `cache_key` for what the response headers' freshness arithmetic leaves, and a `keyset` of
1288
+ `None` then resolves to those keys.
1289
+
1290
+ ### OAuth2 clients
1291
+
1292
+ The `oauth2/` folder at the package root is the **client** side of OAuth2 — what an
1293
+ application that *talks to* an authorization server builds its client from. It is a
1294
+ different folder and a different concern from `access_token/oauth2/`, which is the
1295
+ resource server validating the tokens that arrive: this one never reads a token it was
1296
+ not issued.
1297
+
1298
+ A **`ClientAuthenticationInterface`** is how a client authenticates itself at the token
1299
+ endpoint — the one method its registration carries, named by RFC 7591, Section 2 in its
1300
+ `token_endpoint_auth_method` metadata. `authenticate(client_id, token_endpoint, options)`
1301
+ returns the request options the token request is made with — a header, a body member, or
1302
+ nothing at all — and `get_method()` answers the registration name, compared exactly:
1303
+
1304
+ | Method | Does |
1305
+ |---|---|
1306
+ | `ClientSecretBasic(secret)` | sends the identifier and secret as Basic credentials, each percent-encoded |
1307
+ | `ClientSecretPost(secret)` | adds `client_secret` to the token request body |
1308
+ | `ClientSecretJwt(secret, algorithm="HS256", lifetime=60, clock=None, issuer=None)` | signs a jwt-bearer assertion (RFC 7523) with the secret as its HMAC key |
1309
+ | `PrivateKeyJwt(key, algorithm="RS256", lifetime=60, clock=None, issuer=None)` | signs the assertion with the client's private key |
1310
+ | `NoClientAuthentication()` | sends nothing — a public client, which refuses a code exchange without a PKCE verifier |
1311
+ | `TlsClientAuth()` / `SelfSignedTlsClientAuth()` | authenticates by the certificate the TLS handshake presented; the certificate itself is configured on the HTTP client (`local_cert`, `local_pk`, `passphrase`) |
1312
+
1313
+ The assertion pair builds on `AbstractClientAssertion`: the assertion names the client as
1314
+ its issuer and subject, the provider as its audience — the `issuer` given to the
1315
+ constructor, or the token endpoint — carries a single-use `jti`, an `iat` read from the
1316
+ clock and an `exp` a `lifetime` past it, and is typed `client-authentication+jwt` only
1317
+ when it names the issuer. `JwsAlgorithms` maps the JWA names onto what each demands of
1318
+ its key (`check_key`), and the allow-lists keep a shared secret out of `private_key_jwt`
1319
+ and an asymmetric key out of `client_secret_jwt`.
1320
+
1321
+ An **`AccessTokenTypeInterface`** is how the token the endpoint issued is presented:
1322
+ `get_token_type()`, `check_token_response(members)` — which refuses a token of another
1323
+ type — `prepare_token_request(url, options)`, `present_token(token, method, url,
1324
+ options)` and `on_response(response, url)`. **`BearerTokenType`** carries the token
1325
+ under the request's `auth_bearer`.
1326
+
1327
+ Everything in the folder is importable with nothing beyond the package's own
1328
+ dependencies, except the assertion pair — `ClientSecretJwt` and `PrivateKeyJwt`, over
1329
+ `AbstractClientAssertion` — which signs a JWT and needs joserfc, carried by the `oidc`
1330
+ extra: import them from `xtr_security_http.oauth2.client_authentication.client_secret_jwt`
1331
+ and `...private_key_jwt`, which name the extra when it is missing.
1332
+
1333
+ ### OIDC
1334
+
1335
+ The `oidc/` folder at the package root is the **client** side of OpenID Connect — what an
1336
+ application that *talks to* a provider builds its login flow from, next to `oauth2/` and a
1337
+ different concern from `access_token/oidc/`, which is the resource server verifying the tokens
1338
+ that arrive.
1339
+
1340
+ **`OidcDiscovery`** fetches and caches a provider's discovery document, over the HTTP client
1341
+ and cache contracts:
1342
+
1343
+ ```python
1344
+ from xtr_security_http.oidc import OidcDiscovery
1345
+
1346
+ discovery = OidcDiscovery(
1347
+ http_client,
1348
+ cache,
1349
+ issuer="https://issuer.example",
1350
+ checked_endpoints=("token_endpoint", "userinfo_endpoint"),
1351
+ )
1352
+ ```
1353
+
1354
+ The configuration URL defaults to the well-known path relative to the `issuer` (or the client's
1355
+ `base_uri` with no issuer); the issuer must be `https` — the loopback hosts local development
1356
+ serves on excepted, `is_secure_url` is the check — and the document must announce the same
1357
+ issuer, a trailing slash ignored on both sides. The endpoints named in `checked_endpoints` are
1358
+ weighed before anything is cached: one that would downgrade the `https` transport that carried
1359
+ the document is refused, and so is a missing one; `checked_mtls_aliases` weighs the
1360
+ `mtls_endpoint_aliases` entries the same way. The raw payload is cached (under `cache_key`, or
1361
+ a key derived from the URL) for `cache_ttl` seconds — `None` leaves the lifetime to the pool —
1362
+ and memoized per instance, so repeated reads within one authentication cost one fetch.
1363
+ `get_configuration()` returns the document, `get_secure_endpoint(member)` an announced
1364
+ `https` endpoint, `get_issuer()` the announced issuer (an expected one required, RFC 8414
1365
+ Section 3.3), `prefetch()` sends the request without waiting for its response, and `reset()`
1366
+ forgets the memo. Nothing is cached that exceeds 1 MiB.
1367
+
1368
+ **`OidcJwks`** is the static helper both OIDC entry points read keys through:
1369
+ `from_response(response, enforce_key_usage_verification=True, clock=None)` returns the
1370
+ filtered signing keys and the TTL the response's headers leave — `Cache-Control: max-age`,
1371
+ or `Expires` counted from the response's `Date` or the clock, minus the response's `Age`
1372
+ (RFC 9111 arithmetic), `None` when nothing is left — and `fetch_keys(http_client, jwks_uri,
1373
+ item, default_ttl=3600)` lands them in a cache item, capped at 30 days. Strict (the default)
1374
+ keeps only keys declared for signatures (`use: sig` or `key_ops` holding `sign`/`verify`);
1375
+ lax keeps keys declaring neither; a key without a `kty`, or with a `kid` that is not a
1376
+ string, is dropped either way.
1377
+
1378
+ **`OidcSignatureVerifier`** verifies an ID token's signature against the provider's JWKS —
1379
+ discovered from the `jwks_uri` of the discovery document, cached with the header TTL
1380
+ arithmetic above, refetched when a token names a `kid` the cached set lacks, throttled to
1381
+ once every 60 seconds. The `alg` header is checked first against the allow-list
1382
+ (`("RS256",)` by default) — **no MAC algorithm is ever accepted**, so a public key the
1383
+ provider publishes can never be turned into the shared secret of an `HS256` token — and the
1384
+ claims come back for `OidcIdToken.validate_claims` to confront.
1385
+
1386
+ **`OidcIdToken`** decodes a JWT without verifying its signature (`decode`) and validates the
1387
+ claims per OIDC Core 1.0, Section 3.1.3.7 (`validate_claims(claims, expected_issuer,
1388
+ expected_audience, expected_nonce=None, max_age=None)`): `iss`/`aud`/`exp`/`iat`/`sub` are
1389
+ mandatory, the time claims are read against the injected clock with the allowed drift,
1390
+ `azp` must name the client when present, the `nonce` must match when one was sent, and a
1391
+ requested `max_age` turns `auth_time` mandatory and must not be exceeded.
1392
+
1393
+ **`OidcClientInterface`** / **`OidcClient`** are the protocol operations the login flow needs:
1394
+ `request(endpoint, body, options)` makes a client-authenticated POST to any endpoint the
1395
+ provider announces (the `client_id` added to the form body, the registration's
1396
+ `ClientAuthenticationInterface` applied against the token endpoint whatever is called, no
1397
+ redirect followed), `exchange_code(code, redirect_uri, code_verifier=None)` runs the
1398
+ authorization-code grant, `refresh_token(refresh_token, scopes=())` the refresh grant — a
1399
+ 4xx `invalid_grant` raises `OidcInvalidGrantError`, the one error a caller has to tell apart —
1400
+ `fetch_user_info(access_token)` presents the token to the userinfo endpoint, and
1401
+ `get_client_authentication_method()` names the registration. With `certificate_options`
1402
+ (`local_cert`/`local_pk`/`passphrase`) the requests go to the `mtls_endpoint_aliases` the
1403
+ document announces (RFC 8705, Section 5). The access token type is the bearer one; a token
1404
+ bound to a key a proof factory signs for is not asked for here.
1405
+
1406
+ **`OidcTokenRefresher`** renews the tokens a security token carries, in place:
1407
+ `refresh_if_needed(token)` renews when the `oidc_access_token_expires_at` attribute is within
1408
+ `leeway` (30 seconds by default), `refresh(token)` renews whatever the expiry and stores the
1409
+ new `oidc_access_token`, `oidc_access_token_expires_at`, the rotated `oidc_refresh_token` and
1410
+ the refreshed `oidc_id_token` with its `oidc_sid` — a refreshed ID token is validated against
1411
+ the discovery document's issuer and the client id, and its `sub` must match the previous
1412
+ token's in constant time, and nothing is stored until the whole response has passed.
1413
+
1414
+ **`OidcTokenRefreshListener`** (`firewall/`) is the chain step that runs that renewal per
1415
+ request. Ordered — through `@FirewallListenerOrder`, at the priority the two share — after
1416
+ the context listener, it hands the token the context step restored from the session to the
1417
+ refresher, so what it renews is what the request reads and the context step's response half
1418
+ writes back into the session. `supports()` answers `None` always — the lazy join: the chain
1419
+ asks before the context step has restored anything, so the storage is the step's to read in
1420
+ `authenticate()`, not before. A request with no stored token is left alone; a token the
1421
+ refresher renews is renewed in place; a refresh token the provider no longer honors
1422
+ (`OidcInvalidGrantError`) clears the storage — the request goes on anonymous, and the context
1423
+ step's response half removes the session's copy — while any other renewal failure is warned
1424
+ about and left for the next request, the tokens untouched: a provider that cannot be reached
1425
+ costs a warning per request, not every user's login.
1426
+
1427
+ **`OidcLoginAuthenticator`** (`authenticator/`) lands the whole Authorization Code Flow on
1428
+ top of that machinery — the entry point, the callback and the token, one class:
1429
+
1430
+ ```python
1431
+ from xtr_security_http.authenticator import OidcLoginAuthenticator
1432
+
1433
+ authenticator = OidcLoginAuthenticator(
1434
+ http_utils,
1435
+ user_provider,
1436
+ oidc_client,
1437
+ discovery,
1438
+ id_token,
1439
+ "shop-api",
1440
+ success_handler,
1441
+ failure_handler,
1442
+ options={"check_path": "/oidc/callback", "scope": ("openid", "email")},
1443
+ signature_verifier=verifier,
1444
+ clock=clock,
1445
+ event_dispatcher=dispatcher,
1446
+ )
1447
+ ```
1448
+
1449
+ `start()` builds the authorization redirect: a `state` and a `nonce` of the flow's own, a
1450
+ PKCE code verifier (`S256` by default, `plain` on option, never off for a public client)
1451
+ with its challenge, the scopes — `openid` always, first — the configured `max_age` when one
1452
+ is given, and the extra `authorization_params`, after the managed-parameters guard refused
1453
+ any of `response_type`/`client_id`/`redirect_uri`/`scope`/`state`/`nonce`/`code_challenge`/
1454
+ `code_challenge_method`/`max_age`/`dpop_jkt` — the parameters the authenticator computes
1455
+ itself, so that no configuration can weaken them. Each pending attempt lives under its own
1456
+ session key (`_security.oidc_login.<firewall>.attempt.<state>`), capped at five, oldest
1457
+ dropped first. `start_re_authentication(request, token)` sends `prompt=login` with the
1458
+ previous ID token as `id_token_hint`, applied after the event so a listener cannot drop
1459
+ them. `authenticate()` consumes the callback (a POST reads the body, anything else the
1460
+ query): the `state` is matched in constant time and the attempt consumed whatever follows —
1461
+ a replayed callback fails, the other attempts survive — then the RFC 9207 `iss` parameter
1462
+ is checked against the announced issuer (mandatory when the provider announces
1463
+ `authorization_response_iss_parameter_supported`), the provider's `error` answered, the code
1464
+ exchanged with the PKCE verifier and the very `redirect_uri` the flow started on, the ID
1465
+ token's signature verified before its claims are read, the claims validated against the
1466
+ issuer, the client, the nonce and the `max_age`, and the user loaded from the claims the
1467
+ configured `user_data_source` names (`userinfo` — its `sub` bound to the ID token's in
1468
+ constant time — or `id_token`), with every claim handed to the loader as badge attributes.
1469
+ `create_token()` stamps the `oidc_*` attributes the refresher renews and the back-channel
1470
+ logout matches — `oidc_id_token`, `oidc_access_token`, `oidc_refresh_token`,
1471
+ `oidc_access_token_expires_at` (the clock's now plus `expires_in`, when the provider sent
1472
+ one), `oidc_acr`, `oidc_sid` — and the authentication proofs: the `amr` methods keyed to
1473
+ `min(auth_time, now)` — a provider whose clock runs ahead never extends the recency window —
1474
+ or to the login instant without an `auth_time`. A public client — one authenticating with
1475
+ `none` — is refused when PKCE is off or no signature verifier was given, at use time so the
1476
+ client can stay lazy. The login is interactive; a missing session is refused loudly.
1477
+
1478
+ **`OidcAuthorizationRequestEvent`** (`event/`) is dispatched while that redirect is being
1479
+ built, before anything is stored in the session: it carries the request, the firewall and
1480
+ the extra parameters, and a listener may compute them per request — `set_param`,
1481
+ `remove_param`, `set_params` — with the same managed-parameters guard refusing a listener
1482
+ that sets one of the flow's own.
1483
+
1484
+ The back-channel logout is the provider pushing the end of one of its sessions, and it is
1485
+ client-side machinery too — the login's tokens are what carry what it names:
1486
+
1487
+ - **`OidcLogoutToken`** validates the claims of a logout token — the security event token of
1488
+ RFC 8417 a provider pushes — per OpenID Connect Back-Channel Logout 1.0, Section 2.6:
1489
+ `iss`/`aud`/`iat`/`exp`/`jti`/`events` are mandatory, the time claims are read against the
1490
+ injected clock with the allowed drift, and the token must carry the
1491
+ `http://schemas.openid.net/event/backchannel-logout` member in an `events` object
1492
+ (`OidcLogoutToken.EVENT`). A `nonce` is refused — only an ID token carries one, which is
1493
+ what keeps an ID token from being replayed as a logout token — and the `sid` must be a
1494
+ non-empty string naming the session that ended, which `validate_claims(claims,
1495
+ expected_issuer, expected_audience)` returns. A `sub` is not required (a token naming a
1496
+ user rather than a session is refused by the `sid` rule), and a replayed `jti` is not
1497
+ remembered: `exp` is what bounds a captured token, Section 2.6 leaving the remembering
1498
+ optional. The signature is not verified here — the token arrives unauthenticated, from
1499
+ anyone able to reach the back-channel endpoint, so the signature verifier makes it the
1500
+ provider's and that check can never be skipped.
1501
+ - **`OidcEndedSessions`** remembers the provider sessions a back-channel logout ended, in a
1502
+ cache-item pool every server of the application shares — the logout token reaches any
1503
+ one of them, and the session it names was opened by another, so an adapter local to one
1504
+ process loses what the others recorded. `OidcEndedSessions(pool, namespace, lifetime)`
1505
+ namespaces by the firewall (two firewalls, two providers, never reading each other's
1506
+ entries) and keeps each end for `lifetime` seconds — at least how long a session can stay
1507
+ idle. `record(sid)` writes the end down (recording the same end twice is what it already
1508
+ says, which is what makes replaying a logout token harmless), `has(sid)` tells whether a
1509
+ session ended; the key is `oidc_ended_session.<namespace>.<hash(sid)>`, the opaque `sid`
1510
+ hashed out of the pool.
1511
+ - **`OidcBackChannelLogout`** is what the back-channel endpoint hands the pushed token to:
1512
+ `logout(logout_token)` verifies the signature first — nothing of the token is the
1513
+ provider's until its signature says so — and a verification failure is answered with a
1514
+ generic refusal, the reason logged rather than answered, because the endpoint is public
1515
+ and the verifier names what it verifies after the ID token it is built for. The claims
1516
+ are then validated against the issuer the discovery document announces and the client
1517
+ id, and the session the token names is recorded in the ended sessions — a token naming a
1518
+ session this application never opened is recorded all the same, and so is one naming a
1519
+ session that already ended.
1520
+ - **`OidcBackChannelLogoutListener`** (`event_listener/`) is where the recorded end is
1521
+ acted on: a plain callable on `CheckRefreshedUserEvent`, registered by the wiring on the
1522
+ dispatcher the refreshed-user check is announced on — one per firewall, over the ended
1523
+ sessions that firewall's provider pushed. A token minted on another firewall, or one
1524
+ naming no provider session (no `oidc_sid` attribute), is left untouched and costs no
1525
+ lookup; a token whose `sid` the ended sessions hold is deauthenticated with
1526
+ `OidcSessionEndedError` — the login stops working on the next request the browser makes.
1527
+ Deauthenticating is where it stops, which is what a password change does too: an
1528
+ application wanting its session invalidated as well reads the error off the
1529
+ deauthentication event.
1530
+ - **`OidcEndSessionListener`** (`event_listener/`) handles RP-Initiated Logout: at the
1531
+ logout event, priority 65 — ahead of the default logout listener's 64 — it redirects the
1532
+ caller to the `end_session_endpoint` the discovery document announces, carrying the ID
1533
+ token the login keeps as its `id_token_hint` and, when a
1534
+ `post_logout_redirect_path` is configured, the page to come back to as its
1535
+ `post_logout_redirect_uri`. A logout a listener answered first is left alone, and so is
1536
+ one whose login carries no OIDC ID token; a provider announcing no usable
1537
+ `end_session_endpoint` — or one that cannot be reached — falls back to a local logout,
1538
+ warned about: logging out of the application never depends on the provider being up.
1539
+
1540
+ With the `oidc` extra, the resource-server side keeps its own pieces:
1541
+ **`OidcTokenHandler`** (`access_token/oidc/`) verifies a third-party issuer's tokens against
1542
+ an `OidcKeySetProviderInterface` — `StaticOidcKeySetProvider(jwks)` for a fixed JWKS, or
1543
+ `DiscoveryOidcKeySetProvider(http_client, cache, base_uri=... | jwks_uri=...)`, rebuilt on the
1544
+ aligned discovery and the JWKS reader above: it discovers the `jwks_uri` through an
1545
+ `OidcDiscovery` of its own (the issuer checked for `https` and for the issuer the document
1546
+ announces, the `jwks_uri` weighed), keeps the keys in the injected cache for what the
1547
+ provider's headers leave — or `jwks_cache_ttl` seconds — and refetches on a `kid` the cached
1548
+ set lacks, no more than once every 60 seconds. The handler checks the signature against an
1549
+ explicit **algorithm allow-list** (refusing `none` and symmetric `HS*`), the `iss` against the
1550
+ trusted issuers, the `aud` against the audience, and the time claims against a clock with
1551
+ leeway. **`exp` is required** — a token that never expires is refused. A key set that cannot
1552
+ be fetched, discovered or read raises `OidcKeySetError`; every endpoint must be `https` —
1553
+ the loopback hosts excepted — unless `allow_insecure_http` opens the development door.
1554
+
1555
+ **`OidcUserInfoTokenHandler`** (`access_token/oidc/`) validates a token the other way round:
1556
+ it presents it to the issuer's userinfo endpoint as a bearer credential and takes the claims
1557
+ the endpoint answers with as the user — the one arrangement where the issuer's own answer,
1558
+ over the transport the client carries, is the whole of the validation:
1559
+
1560
+ ```python
1561
+ from xtr_security_http.access_token.oidc import OidcUserInfoTokenHandler
1562
+
1563
+ handler = OidcUserInfoTokenHandler(
1564
+ HttpxHttpClient().with_options(base_uri="https://issuer.example/userinfo"),
1565
+ claim="sub",
1566
+ )
1567
+ ```
1568
+
1569
+ The endpoint is the client's base URI, or the one the issuer's discovery document announces
1570
+ after `enable_discovery(cache, cache_key)` — which builds an `OidcDiscovery` over the
1571
+ handler's own client, the document kept under `cache_key + ".document"` with no lifetime of
1572
+ its own (the pool decides) and its `userinfo_endpoint` weighed before anything is cached.
1573
+ `reset()` forgets the memoized document. The identifier is read from `claim` (`sub` by
1574
+ default); the badge's loader is wrapped in a `FallbackUserLoader`, so the access-token
1575
+ authenticator replaces it with the firewall's user provider when one is configured, and
1576
+ without one the loader builds the core `OidcUser` the claims describe. Every failure becomes
1577
+ an `InvalidAccessTokenError` — a missing claim raises `MissingClaimError` on the chain, a bad
1578
+ signature names `InvalidSignatureError`.
1579
+
1580
+ **`OidcTokenGenerator`** (`access_token/oidc/`) signs the other side: an access token this
1581
+ application issues, as RFC 9068 profiles one — `typ: at+jwt` in the header, the user on `sub`
1582
+ (or the claim configured), the audience, the issuer, and `exp`/`nbf` from a `ttl` and a
1583
+ not-before:
1584
+
1585
+ ```python
1586
+ from xtr_security_http.access_token.oidc import OidcTokenGenerator
1587
+
1588
+ generator = OidcTokenGenerator(
1589
+ ("RS256",),
1590
+ keyset,
1591
+ "shop-api",
1592
+ ("https://this-application.example",),
1593
+ )
1594
+ token = generator.generate("alice", ttl=3600)
1595
+ ```
1596
+
1597
+ The algorithm and the issuer are each either named at every call or declared as the only one;
1598
+ the signing key is chosen from the key set — one whose declared use is signing and that fits
1599
+ the algorithm. **`OidcTrait`** is the mixin the OIDC handlers share: `create_user(claims)`
1600
+ builds the `OidcUser` the claims describe, and `check_discovered_endpoint(endpoint, key,
1601
+ discovery_url)` refuses an endpoint a discovery document must not announce.
1602
+
1603
+ **`OidcTokenGenerateCommand`** (`command/`) mints one from the console:
1604
+ `security:oidc:generate-token <user-identifier>` signs a token through the generator of a
1605
+ firewall — `--firewall` naming which when several are configured, `--algorithm` and
1606
+ `--issuer` naming the choice when several are declared, `--ttl` the seconds it lives,
1607
+ `--not-before` the moment it starts being valid (a date, or an offset like `-5 minutes`,
1608
+ read against the command's clock) — and prints the token, nothing else. The generators
1609
+ arrive one per firewall, keyed by the firewall's name — the registry the
1610
+ [xtr-security](../xtr-security) bundle fills as it wires the firewalls — and every
1611
+ refusal, the generator's included, is reported as the run's error. The command rides the
1612
+ `oidc` extra with its generator and runs on the application's console: the bundle loads
1613
+ it only when one is active.
1614
+
1615
+ ### OAuth2 scopes
1616
+
1617
+ A scope requirement is written as an access-control attribute. `oauth2_scope("books:read",
1618
+ "books:write")` builds the string `"OAUTH2_SCOPE(books:read books:write)"`;
1619
+ `OAuth2ScopeVoter.get_required_scopes(attribute)` reads the scopes back out — the static every
1620
+ reader shares, `parse_oauth2_scope(attribute)` its module-level spelling — or `None` for anything
1621
+ not shaped like it. **`OAuth2ScopeVoter`** grants when the token holds every asked-for scope,
1622
+ reading the `oauth2_scope` attribute an authenticator copied onto it, and supports an attribute
1623
+ by its `OAUTH2_SCOPE(` prefix alone; **`InsufficientScopeAccessDeniedHandler(responses, realm=None,
1624
+ inner=None, resource_metadata_uri=None, sender_constraint=None)`** answers a denied scope with a
1625
+ `403` whose empty body carries nothing — the RFC 6750 `insufficient_scope` challenge in its
1626
+ `WWW-Authenticate` header says it all: the realm, the fixed error description, the scopes every
1627
+ denied attribute asked for merged and deduplicated, the `resource_metadata` address (RFC 9728,
1628
+ Section 5.1 — a leading-slash path resolved against the request) and the scheme of the sender
1629
+ constraint when one is configured. A denial no scope took part in is not this handler's to
1630
+ answer: it goes to `inner`, which answering `None` hands back to the regular access-denied
1631
+ handling.
1632
+
1633
+ ### Security events
1634
+
1635
+ Ten events are raised around a request's authentication — `CheckPassportEvent`,
1636
+ `AuthenticationTokenCreatedEvent`, `InteractiveLoginEvent`, `LoginSuccessEvent`, `LoginFailureEvent`,
1637
+ `CheckRefreshedUserEvent`, `TokenDeauthenticatedEvent`, `LogoutEvent`, `SwitchUserEvent`,
1638
+ `OidcAuthorizationRequestEvent`.
1639
+ A subscriber listens to the event class itself (the dispatcher derives each event's name from its
1640
+ class) — except `InteractiveLoginEvent` and `SwitchUserEvent`, the two events that carry a name of
1641
+ their own, `SecurityEvents.INTERACTIVE_LOGIN` (`security.interactive_login`) and
1642
+ `SecurityEvents.SWITCH_USER` (`security.switch_user`), because an application listens for a user's
1643
+ own login or a user switch across the whole flow rather than at one authenticator.
1644
+ `SecurityEvents.ALIASES` maps every event class to the name it is dispatched under.
1645
+
1646
+ `CheckRefreshedUserEvent` and `TokenDeauthenticatedEvent` belong to the stateful half: the
1647
+ context listener announces the first while refreshing the user a session token carries —
1648
+ seeded with the verdict of its own comparison, for a listener to confirm, add a reason to,
1649
+ or clear — and the second when the refresh deauthenticates, carrying the original token,
1650
+ the reason, the providers that contributed and the error a listener gave.
1651
+
1652
+ ## Errors
1653
+
1654
+ Everything this library raises derives from `SecurityError` (from
1655
+ [xtr-security-core](../xtr-security-core)); authentication failures additionally derive from its
1656
+ `AuthenticationError`, so one `except AuthenticationError` catches the token failures.
1657
+
1658
+ | Error | Base (besides `SecurityError`) | Raised when |
1659
+ |---|---|---|
1660
+ | `InvalidAccessTokenError` | `AuthenticationError` | a bearer token was present but could not be trusted — answered with an `invalid_token` challenge |
1661
+ | `InvalidCsrfTokenError` | `AuthenticationError` | a passport's cross-site request forgery token did not check out — the login it guards is refused |
1662
+ | `InvalidLoginLinkError` | `RuntimeError` | a login link did not check out — a parameter missing or malformed, a hash verifying under no secret, a user the provider does not know; one of the `InvalidLoginLinkErrorInterface` family the authenticator catches |
1663
+ | `ExpiredLoginLinkError` | `ExpiredSignatureError` (core) | the link checked out except its moment, which has passed; also an `InvalidLoginLinkErrorInterface` |
1664
+ | `InvalidLoginLinkAuthenticationError` | `AuthenticationError` | a login through a link failed — the authentication-layer answer to every refusal the handler raises |
1665
+ | `OidcInvalidGrantError` | `AuthenticationError` | the provider refused a refresh grant — the one error a caller of the OIDC client has to tell apart |
1666
+ | `OidcSessionEndedError` | `AuthenticationError` | the OIDC provider ended the session this login belongs to; deauthenticates on the next request |
1667
+ | `MissingClaimError` | `AuthenticationError` | the claim the user identifier is read from was absent from what the issuer answered |
1668
+ | `ImpersonationError` | `RuntimeError` | an impersonation address was asked for and could not be built — no firewall with a switch, a forgery-asking switch without a manager, or a dedicated path configured as a route name |
1669
+ | `InvalidSessionStrategyError` | `RuntimeError` | a session strategy was asked to act under a name that is none of its own; carries `strategy` |
1670
+ | `LogoutError` | `RuntimeError` | a logout request failed its cross-site request forgery proof |
1671
+ | `MissingSessionError` | `RuntimeError` | a session strategy was asked to move a request whose security state carries no session |
1672
+ | `MissingStateBagError` | `RuntimeError` | a firewall chain was asked about a request carrying no state bag — the caller running the chain must park the kernel event's bag on the request it hands over |
1673
+ | `OidcKeySetError` | — | the keys a token verifies against could not be fetched, discovered or read |
1674
+ | `UnknownFirewallError` | `LookupError` | a firewall was asked for by a name none is registered under; carries `name` and `configured` |
1675
+
1676
+ ## Layout
1677
+
1678
+ ```
1679
+ xtr_security_http/
1680
+ ├── firewall_map.py FirewallMap, matched or fetched by name
1681
+ ├── firewall_context_interface.py one firewall's runtime pieces
1682
+ ├── firewall/ Firewall (the subscriber), AccessListener, ChannelListener,
1683
+ │ ContextListener, ExceptionListener, LogoutListener,
1684
+ │ OidcTokenRefreshListener, SwitchUserListener, RefreshUserResult,
1685
+ │ FirewallListenerInterface, AbstractListener
1686
+ ├── authentication/ AuthenticatorManager, handlers, ExposeSecurityLevel
1687
+ ├── authenticator/ AuthenticatorInterface, AccessTokenAuthenticator,
1688
+ │ OidcLoginAuthenticator, the pre-authenticated trio,
1689
+ │ passport/, token/
1690
+ ├── access_token/ extractors, handler interface, oauth2/ (Oauth2TokenHandler),
1691
+ │ oidc/ (OidcTokenHandler)
1692
+ ├── oidc/ the client suite: discovery, JWKS, signature verifier,
1693
+ │ ID token, client, token refresher, and the back-channel
1694
+ │ logout (logout token, ended sessions, the endpoint handler);
1695
+ │ DiscoveryOidcKeySetProvider for the resource server
1696
+ ├── oauth2/ the client side: JwsAlgorithms, BearerTokenType, the
1697
+ │ client-authentication methods (the assertion pair behind
1698
+ │ the oidc extra)
1699
+ ├── access_map.py the ordered access-control rules
1700
+ ├── request_matcher/ the callable escape hatch (the rest come from xtr-http-foundation)
1701
+ ├── authorization/ OAuth2ScopeVoter, oauth2_scope, the scope-denied handler
1702
+ ├── decorator/ IsGranted, CurrentUser, CurrentToken, IsCsrfTokenValid,
1703
+ │ FirewallListenerOrder
1704
+ ├── resolver/ the enforcement services behind the markers
1705
+ ├── entry_point/ AuthenticationEntryPointInterface
1706
+ ├── event/ the firewall events around a login, a logout and a switch
1707
+ ├── event_listener/ the listeners the bundle registers (incl. the timing guard)
1708
+ │ and the logout listeners answering LogoutEvent
1709
+ ├── impersonate/ ImpersonateUrlGenerator, ImpersonationForm, SwitchUserConfig,
1710
+ │ SwitchUserConfigLookupInterface — the switch addresses
1711
+ ├── rate_limiter/ DefaultLoginRateLimiter — the limits a login's failed attempts
1712
+ │ count under, keyed by the address and the account
1713
+ ├── login_link/ the signed links a login arrives by: details, handler,
1714
+ │ handler interface
1715
+ ├── logout/ LogoutUrlGenerator, LogoutForm — the logout addresses
1716
+ ├── remember_me/ the handlers, details, staged cookie and response pass of a
1717
+ │ login the user asked to outlive its session
1718
+ ├── session/ what a login does to the session it reattached
1719
+ ├── command/ the console command: security:oidc:generate-token
1720
+ └── exception/ the errors this package adds
1721
+ ```
1722
+
1723
+ ## In an application
1724
+
1725
+ This package works on its own and ships no bundle. The
1726
+ [`xtr-security`](../xtr-security#use-in-an-application) bundle wires it — the firewalls, the
1727
+ authenticators, the access map, the exception listener, the `Firewall` subscriber and the
1728
+ `IsGranted`/`CurrentUser` surface — into an application on [xtr-dependency-injection](../xtr-dependency-injection), with
1729
+ firewalls written as configuration. See that package's
1730
+ [Use in an application](../xtr-security#use-in-an-application).
1731
+
1732
+ ## Development
1733
+
1734
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
1735
+ `packages/xtr-security-http`; run the commands below from there. The `python-xtr-security-http`
1736
+ repository is a read-only copy, so send issues and pull requests to the monorepo.
1737
+
1738
+ ```sh
1739
+ uv sync --all-extras
1740
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
1741
+ ```
1742
+
1743
+ ## License
1744
+
1745
+ MIT — see [LICENSE](LICENSE).