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.
- xtr_security_http-5.0.0/PKG-INFO +1745 -0
- xtr_security_http-5.0.0/README.md +1706 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/pyproject.toml +24 -13
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/pyproject.toml.orig +34 -10
- xtr_security_http-5.0.0/src/xtr_security_http/__init__.py +363 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/_challenge.py +47 -7
- xtr_security_http-5.0.0/src/xtr_security_http/_parked_session.py +44 -0
- xtr_security_http-5.0.0/src/xtr_security_http/_request_parameters.py +95 -0
- xtr_security_http-5.0.0/src/xtr_security_http/_security_state.py +137 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_map.py +74 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_map_interface.py +39 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/__init__.py +2 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/access_token_extractor_interface.py +24 -0
- {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
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/form_encoded_body_extractor.py +85 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/header_access_token_extractor.py +68 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/__init__.py +14 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_introspected_user.py +70 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_introspection_claims.py +193 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_signed_response.py +141 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/_signed_response_discovery.py +149 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oauth2/oauth2_token_handler.py +471 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/access_token/oidc/__init__.py +19 -8
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/__init__.py +9 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/invalid_signature_error.py +23 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/exception/missing_claim_error.py +23 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_token_generator.py +190 -0
- {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
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_trait.py +90 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/oidc/oidc_user_info_token_handler.py +218 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/query_access_token_extractor.py +45 -0
- xtr_security_http-5.0.0/src/xtr_security_http/access_token/sender_constraint_interface.py +69 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/__init__.py +12 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/_authentication_outcome.py +191 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/_manager_reporting.py +48 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/_referer.py +67 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/_supports_pass.py +96 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/authentication_failure_handler_interface.py +10 -10
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/authentication_success_handler_interface.py +3 -5
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/authentication_utils.py +119 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/authenticator_manager.py +381 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/authenticator_manager_interface.py +55 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/custom_authentication_failure_handler.py +66 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/custom_authentication_success_handler.py +81 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/default_authentication_failure_handler.py +175 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/default_authentication_success_handler.py +173 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authentication/user_authenticator_interface.py +54 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/__init__.py +45 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_access_token_challenge.py +96 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_access_token_scopes.py +40 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_form_login_credentials.py +75 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_attempts.py +115 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_authorization.py +148 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_callback.py +102 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_claims.py +149 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_options.py +136 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_public_client.py +55 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/_oidc_login_token.py +83 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/abstract_authenticator.py +7 -8
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/abstract_login_form_authenticator.py +137 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/abstract_pre_authenticated_authenticator.py +248 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/access_token_authenticator.py +285 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/authenticator_interface.py +7 -8
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/__init__.py +13 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/traceable_authenticator.py +168 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/traceable_authenticator_manager_listener.py +101 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/debug/unsupported_reasons.py +41 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/fallback_user_loader.py +52 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/form_login_authenticator.py +264 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/http_basic_authenticator.py +183 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/interactive_authenticator_interface.py +32 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/json_login_authenticator.py +254 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/login_link_authenticator.py +216 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/oidc_login_authenticator.py +379 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/__init__.py +6 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/authentication_method_badge.py +48 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/csrf_token_badge.py +60 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/badge/remember_me_badge.py +60 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/user_badge.py +14 -16
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/custom_credentials.py +17 -6
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/password_credentials.py +4 -1
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/passport.py +121 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/passport/self_validating_passport.py +53 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/remember_me_authenticator.py +209 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/remote_user_authenticator.py +76 -0
- xtr_security_http-5.0.0/src/xtr_security_http/authenticator/x509_authenticator.py +111 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/access_denied_handler_interface.py +4 -3
- xtr_security_http-5.0.0/src/xtr_security_http/authorization/insufficient_scope_access_denied_handler.py +136 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/oauth2_scope_voter.py +43 -23
- xtr_security_http-5.0.0/src/xtr_security_http/command/__init__.py +7 -0
- xtr_security_http-5.0.0/src/xtr_security_http/command/_registry.py +16 -0
- xtr_security_http-5.0.0/src/xtr_security_http/command/oidc_token_generate_command.py +152 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/__init__.py +41 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/_methods.py +24 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/current_token.py +56 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/current_user.py +82 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/firewall_listener_order.py +79 -0
- xtr_security_http-5.0.0/src/xtr_security_http/decorator/is_csrf_token_valid.py +119 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/decorator/is_granted.py +56 -53
- xtr_security_http-5.0.0/src/xtr_security_http/entry_point/__init__.py +15 -0
- {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
- xtr_security_http-5.0.0/src/xtr_security_http/entry_point/fallback_authentication_entry_point_interface.py +54 -0
- xtr_security_http-5.0.0/src/xtr_security_http/entry_point/re_authentication_entry_point_interface.py +50 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/__init__.py +35 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/check_passport_event.py +10 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/check_refreshed_user_event.py +102 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/interactive_login_event.py +47 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/login_failure_event.py +36 -16
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/login_success_event.py +41 -10
- xtr_security_http-5.0.0/src/xtr_security_http/event/logout_event.py +68 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/oidc_authorization_request_event.py +87 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/switch_user_event.py +77 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event/token_deauthenticated_event.py +63 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/__init__.py +62 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/authentication_proofs_listener.py +112 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/check_credentials_listener.py +10 -9
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/check_remember_me_conditions_listener.py +148 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/clear_site_data_listener.py +74 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/cleared_cookie.py +33 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/cookie_clearing_listener.py +88 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/csrf_protection_listener.py +97 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/csrf_token_clearing_listener.py +66 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/default_logout_listener.py +69 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/login_throttling_listener.py +191 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/oidc_back_channel_logout_listener.py +98 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/oidc_end_session_listener.py +122 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/password_migrating_listener.py +42 -12
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/remember_me_listener.py +103 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/save_session_listener.py +129 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/session_logout_listener.py +69 -0
- xtr_security_http-5.0.0/src/xtr_security_http/event_listener/session_strategy_listener.py +111 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/user_checker_listener.py +28 -7
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event_listener/user_provider_listener.py +22 -11
- xtr_security_http-5.0.0/src/xtr_security_http/exception/__init__.py +43 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/expired_login_link_error.py +19 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/impersonation_error.py +18 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_csrf_token_error.py +22 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_authentication_error.py +21 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_error.py +20 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_login_link_error_interface.py +22 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/invalid_session_strategy_error.py +33 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/logout_error.py +17 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/missing_session_error.py +20 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/missing_state_bag_error.py +20 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/oidc_invalid_grant_error.py +22 -0
- xtr_security_http-5.0.0/src/xtr_security_http/exception/oidc_session_ended_error.py +21 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/__init__.py +48 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_exception_answers.py +89 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_re_authentication.py +63 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_load.py +55 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_request.py +48 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_switch_uri.py +33 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/_user_refresh.py +197 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/abstract_listener.py +30 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/access_listener.py +200 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/authenticator_manager_listener.py +98 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/channel_listener.py +179 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/context_listener.py +262 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/exception_listener.py +387 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/firewall.py +201 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/firewall_listener_interface.py +71 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/logout_listener.py +179 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/oidc_token_refresh_listener.py +125 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/refresh_user_result.py +42 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/switch_user_listener.py +384 -0
- xtr_security_http-5.0.0/src/xtr_security_http/firewall/user_changed.py +123 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_context_interface.py +26 -12
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_map.py +42 -14
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/firewall_map_interface.py +29 -5
- xtr_security_http-5.0.0/src/xtr_security_http/http_utils.py +190 -0
- xtr_security_http-5.0.0/src/xtr_security_http/impersonate/__init__.py +15 -0
- xtr_security_http-5.0.0/src/xtr_security_http/impersonate/impersonate_url_generator.py +265 -0
- xtr_security_http-5.0.0/src/xtr_security_http/impersonate/impersonation_form.py +32 -0
- xtr_security_http-5.0.0/src/xtr_security_http/impersonate/switch_user_config.py +32 -0
- xtr_security_http-5.0.0/src/xtr_security_http/impersonate/switch_user_config_lookup_interface.py +38 -0
- xtr_security_http-5.0.0/src/xtr_security_http/login_link/__init__.py +20 -0
- xtr_security_http-5.0.0/src/xtr_security_http/login_link/_presented_link.py +103 -0
- xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_details.py +52 -0
- xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_handler.py +286 -0
- xtr_security_http-5.0.0/src/xtr_security_http/login_link/login_link_handler_interface.py +67 -0
- xtr_security_http-5.0.0/src/xtr_security_http/logout/__init__.py +8 -0
- xtr_security_http-5.0.0/src/xtr_security_http/logout/logout_form.py +30 -0
- xtr_security_http-5.0.0/src/xtr_security_http/logout/logout_url_generator.py +298 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/__init__.py +43 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/__init__.py +14 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/access_token_type_interface.py +115 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/access_token_type/bearer_token_type.py +119 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/__init__.py +29 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/abstract_client_assertion.py +258 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/abstract_tls_client_auth.py +57 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_authentication_interface.py +64 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_basic.py +73 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_jwt.py +107 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/client_secret_post.py +74 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/no_client_authentication.py +85 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/private_key_jwt.py +83 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/self_signed_tls_client_auth.py +29 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/client_authentication/tls_client_auth.py +29 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oauth2/jws_algorithms.py +113 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/__init__.py +44 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/discovery_oidc_key_set_provider.py +258 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_back_channel_logout.py +109 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_client.py +339 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_client_interface.py +117 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_discovery.py +376 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_ended_sessions.py +83 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_id_token.py +219 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_jwks.py +276 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_logout_token.py +181 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_signature_verifier.py +227 -0
- xtr_security_http-5.0.0/src/xtr_security_http/oidc/oidc_token_refresher.py +218 -0
- xtr_security_http-5.0.0/src/xtr_security_http/rate_limiter/__init__.py +16 -0
- xtr_security_http-5.0.0/src/xtr_security_http/rate_limiter/default_login_rate_limiter.py +158 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/__init__.py +29 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/_signature_digest.py +126 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/abstract_remember_me_handler.py +189 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/persistent_remember_me_handler.py +269 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_cookie.py +81 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_details.py +168 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/remember_me_handler_interface.py +63 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/response_listener.py +58 -0
- xtr_security_http-5.0.0/src/xtr_security_http/remember_me/signature_remember_me_handler.py +139 -0
- xtr_security_http-5.0.0/src/xtr_security_http/resolver/__init__.py +21 -0
- xtr_security_http-5.0.0/src/xtr_security_http/resolver/csrf_token_checker.py +65 -0
- xtr_security_http-5.0.0/src/xtr_security_http/resolver/is_granted_checker.py +124 -0
- xtr_security_http-5.0.0/src/xtr_security_http/resolver/security_token_resolver.py +57 -0
- xtr_security_http-5.0.0/src/xtr_security_http/resolver/user_value_resolver.py +82 -0
- xtr_security_http-5.0.0/src/xtr_security_http/security_events.py +75 -0
- xtr_security_http-5.0.0/src/xtr_security_http/security_request_attributes.py +122 -0
- xtr_security_http-5.0.0/src/xtr_security_http/session/__init__.py +11 -0
- xtr_security_http-5.0.0/src/xtr_security_http/session/session_authentication_strategy.py +140 -0
- xtr_security_http-5.0.0/src/xtr_security_http/session/session_authentication_strategy_interface.py +38 -0
- xtr_security_http-5.0.0/src/xtr_security_http/util/__init__.py +7 -0
- xtr_security_http-5.0.0/src/xtr_security_http/util/target_path.py +65 -0
- xtr_security_http-4.0.4/PKG-INFO +0 -429
- xtr_security_http-4.0.4/README.md +0 -396
- xtr_security_http-4.0.4/src/xtr_security_http/__init__.py +0 -173
- xtr_security_http-4.0.4/src/xtr_security_http/_runner.py +0 -151
- xtr_security_http-4.0.4/src/xtr_security_http/_state.py +0 -58
- xtr_security_http-4.0.4/src/xtr_security_http/access_map.py +0 -52
- xtr_security_http-4.0.4/src/xtr_security_http/access_map_interface.py +0 -24
- xtr_security_http-4.0.4/src/xtr_security_http/access_token/access_token_extractor_interface.py +0 -31
- xtr_security_http-4.0.4/src/xtr_security_http/access_token/form_encoded_body_extractor.py +0 -120
- xtr_security_http-4.0.4/src/xtr_security_http/access_token/header_access_token_extractor.py +0 -80
- xtr_security_http-4.0.4/src/xtr_security_http/access_token/oidc/exception/__init__.py +0 -7
- xtr_security_http-4.0.4/src/xtr_security_http/access_token/query_access_token_extractor.py +0 -51
- xtr_security_http-4.0.4/src/xtr_security_http/authentication/authenticator_manager.py +0 -232
- xtr_security_http-4.0.4/src/xtr_security_http/authentication/authenticator_manager_interface.py +0 -40
- xtr_security_http-4.0.4/src/xtr_security_http/authenticator/__init__.py +0 -9
- xtr_security_http-4.0.4/src/xtr_security_http/authenticator/access_token_authenticator.py +0 -209
- xtr_security_http-4.0.4/src/xtr_security_http/authenticator/passport/passport.py +0 -113
- xtr_security_http-4.0.4/src/xtr_security_http/authenticator/passport/self_validating_passport.py +0 -42
- xtr_security_http-4.0.4/src/xtr_security_http/authorization/insufficient_scope_access_denied_handler.py +0 -71
- xtr_security_http-4.0.4/src/xtr_security_http/decorator/__init__.py +0 -16
- xtr_security_http-4.0.4/src/xtr_security_http/decorator/current_user.py +0 -82
- xtr_security_http-4.0.4/src/xtr_security_http/entry_point/__init__.py +0 -7
- xtr_security_http-4.0.4/src/xtr_security_http/event/__init__.py +0 -21
- xtr_security_http-4.0.4/src/xtr_security_http/event_listener/__init__.py +0 -21
- xtr_security_http-4.0.4/src/xtr_security_http/exception/__init__.py +0 -18
- xtr_security_http-4.0.4/src/xtr_security_http/exception/firewall_not_booted_error.py +0 -18
- xtr_security_http-4.0.4/src/xtr_security_http/firewall/__init__.py +0 -15
- xtr_security_http-4.0.4/src/xtr_security_http/firewall/access_listener.py +0 -66
- xtr_security_http-4.0.4/src/xtr_security_http/firewall/exception_listener.py +0 -156
- xtr_security_http-4.0.4/src/xtr_security_http/firewall/firewall.py +0 -142
- xtr_security_http-4.0.4/src/xtr_security_http/firewall_scheme.py +0 -114
- xtr_security_http-4.0.4/src/xtr_security_http/firewall_scheme_registry.py +0 -83
- xtr_security_http-4.0.4/src/xtr_security_http/oidc/__init__.py +0 -7
- xtr_security_http-4.0.4/src/xtr_security_http/oidc/oidc_discovery.py +0 -318
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/__init__.py +0 -28
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/_pattern.py +0 -27
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/callable_request_matcher.py +0 -37
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/chain_request_matcher.py +0 -37
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/host_request_matcher.py +0 -51
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/ip_request_matcher.py +0 -61
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/method_request_matcher.py +0 -32
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/path_request_matcher.py +0 -43
- xtr_security_http-4.0.4/src/xtr_security_http/request_matcher/request_matcher_interface.py +0 -25
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/LICENSE +0 -0
- {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
- {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
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/_sensitive.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authentication/expose_security_level.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/oidc/__init__.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/oidc/oidc_jwks.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/__init__.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/badge/badge_interface.py +0 -0
- {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
- {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
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/__init__.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/passport/credentials/credentials_interface.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/token/__init__.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authenticator/token/post_authentication_token.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/authorization/__init__.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/decorator/is_granted_context.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/event/authentication_token_created_event.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/exception/invalid_access_token_error.py +0 -0
- {xtr_security_http-4.0.4 → xtr_security_http-5.0.0}/src/xtr_security_http/exception/unknown_firewall_error.py +0 -0
- {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).
|