xtr-security-http 3.0.0__tar.gz → 4.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/LICENSE +1 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/PKG-INFO +14 -12
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/README.md +6 -6
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/pyproject.toml +8 -8
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/pyproject.toml.orig +8 -8
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/__init__.py +30 -0
- xtr_security_http-4.0.2/src/xtr_security_http/_challenge.py +64 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/_runner.py +23 -6
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/_state.py +4 -3
- xtr_security_http-4.0.2/src/xtr_security_http/access_token/form_encoded_body_extractor.py +120 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/oidc/oidc_token_handler.py +4 -4
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/access_token_authenticator.py +13 -3
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/passport.py +11 -2
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/token/post_authentication_token.py +10 -2
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authorization/insufficient_scope_access_denied_handler.py +9 -4
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authorization/oauth2_scope_voter.py +11 -6
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/decorator/current_user.py +1 -2
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event/__init__.py +2 -1
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event_listener/check_credentials_listener.py +10 -21
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall/exception_listener.py +7 -2
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall/firewall.py +1 -1
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall_context_interface.py +2 -5
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall_scheme_registry.py +0 -4
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/oidc/oidc_discovery.py +100 -20
- xtr_security_http-4.0.2/src/xtr_security_http/request_matcher/host_request_matcher.py +51 -0
- xtr_security_http-4.0.2/src/xtr_security_http/request_matcher/ip_request_matcher.py +61 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/path_request_matcher.py +3 -1
- xtr_security_http-3.0.0/src/xtr_security_http/access_token/form_encoded_body_extractor.py +0 -57
- xtr_security_http-3.0.0/src/xtr_security_http/request_matcher/host_request_matcher.py +0 -43
- xtr_security_http-3.0.0/src/xtr_security_http/request_matcher/ip_request_matcher.py +0 -37
- xtr_security_http-3.0.0/src/xtr_security_http/security_events.py +0 -41
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_map.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_map_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/access_token_extractor_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/access_token_handler_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/chain_access_token_extractor.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/header_access_token_extractor.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/oidc/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/oidc/exception/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/oidc/exception/oidc_key_set_error.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/access_token/query_access_token_extractor.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/_sensitive.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/authentication_failure_handler_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/authentication_success_handler_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/authenticator_manager.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/authenticator_manager_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authentication/expose_security_level.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/abstract_authenticator.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/authenticator_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/oidc/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/oidc/oidc_jwks.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/badge/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/badge/badge_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/badge/password_upgrade_badge.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/badge/pre_authenticated_user_badge.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/badge/user_badge.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/credentials/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/credentials/credentials_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/credentials/custom_credentials.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/credentials/password_credentials.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/passport/self_validating_passport.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authenticator/token/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authorization/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/authorization/access_denied_handler_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/decorator/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/decorator/is_granted.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/decorator/is_granted_context.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/entry_point/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/entry_point/authentication_entry_point_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event/authentication_token_created_event.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event/check_passport_event.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event/login_failure_event.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event/login_success_event.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event_listener/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event_listener/password_migrating_listener.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event_listener/user_checker_listener.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/event_listener/user_provider_listener.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/exception/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/exception/firewall_not_booted_error.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/exception/invalid_access_token_error.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/exception/unknown_firewall_error.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall/access_listener.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall_map.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall_map_interface.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/firewall_scheme.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/oidc/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/py.typed +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/__init__.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/_pattern.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/callable_request_matcher.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/chain_request_matcher.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/method_request_matcher.py +0 -0
- {xtr_security_http-3.0.0 → xtr_security_http-4.0.2}/src/xtr_security_http/request_matcher/request_matcher_interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtr-security-http
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.2
|
|
4
4
|
Summary: The HTTP edge of xtr security: firewalls, authenticators, bearer access tokens and the request surface.
|
|
5
5
|
Keywords: security,authentication,firewall,fastapi,oauth2
|
|
6
6
|
Author: Xterr
|
|
@@ -14,11 +14,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
|
-
Requires-Dist: xtr-security-core>=
|
|
18
|
-
Requires-Dist: xtr-http-kernel>=
|
|
19
|
-
Requires-Dist: xtr-
|
|
20
|
-
Requires-Dist: xtr-
|
|
21
|
-
Requires-Dist: xtr-
|
|
17
|
+
Requires-Dist: xtr-security-core>=4.0,<5
|
|
18
|
+
Requires-Dist: xtr-http-kernel>=4.0,<5
|
|
19
|
+
Requires-Dist: xtr-dependency-injection[fastapi]>=4.0,<5
|
|
20
|
+
Requires-Dist: xtr-event-dispatcher>=4.0,<5
|
|
21
|
+
Requires-Dist: xtr-event-dispatcher-contracts>=4.0,<5
|
|
22
|
+
Requires-Dist: xtr-logging-contracts>=4.0,<5
|
|
23
|
+
Requires-Dist: xtr-password-hasher>=4.0,<5
|
|
22
24
|
Requires-Dist: fastapi>=0.123
|
|
23
25
|
Requires-Dist: starlette>=0.40
|
|
24
26
|
Requires-Dist: anyio>=4.0
|
|
@@ -182,7 +184,7 @@ routes. See [xtr-security's Quick start](../xtr-security#quick-start) for the wh
|
|
|
182
184
|
### Firewalls and the map
|
|
183
185
|
|
|
184
186
|
A **`FirewallContextInterface`** is one firewall's runtime pieces: its `name`, its
|
|
185
|
-
`authenticator_manager`, its `access_listener`,
|
|
187
|
+
`authenticator_manager`, its `access_listener`, the FastAPI `scheme`
|
|
186
188
|
it shows in OpenAPI, whether it has `security` at all, and the optional `entry_point`,
|
|
187
189
|
`access_denied_handler` and `scope_denied_handler` that answer a failure. A **`FirewallMap`**
|
|
188
190
|
holds those contexts: `match(request)` returns the first whose matcher claims the request,
|
|
@@ -363,9 +365,10 @@ answers a denied scope with a `403` carrying the RFC 6750 `insufficient_scope` c
|
|
|
363
365
|
|
|
364
366
|
### Security events
|
|
365
367
|
|
|
366
|
-
|
|
367
|
-
`
|
|
368
|
-
|
|
368
|
+
Four events are raised around a request's authentication — `CheckPassportEvent`,
|
|
369
|
+
`AuthenticationTokenCreatedEvent`, `LoginSuccessEvent`, `LoginFailureEvent`. A subscriber listens
|
|
370
|
+
to the event class itself (the dispatcher derives each event's name from its class), so there is no
|
|
371
|
+
separate table of name constants to keep in step.
|
|
369
372
|
|
|
370
373
|
## Errors
|
|
371
374
|
|
|
@@ -394,10 +397,9 @@ xtr_security_http/
|
|
|
394
397
|
├── access_map.py the ordered access-control rules
|
|
395
398
|
├── request_matcher/ path, host, method, ip, chain, callable
|
|
396
399
|
├── authorization/ OAuth2ScopeVoter, oauth2_scope, the scope-denied handler
|
|
397
|
-
├── decorator/
|
|
400
|
+
├── decorator/ IsGranted, CurrentUser, IsGrantedContext
|
|
398
401
|
├── entry_point/ AuthenticationEntryPointInterface
|
|
399
402
|
├── event/ the four firewall events
|
|
400
|
-
├── security_events.py the name each is dispatched under
|
|
401
403
|
├── event_listener/ the listeners the bundle registers (incl. the timing guard)
|
|
402
404
|
└── exception/ the errors this package adds
|
|
403
405
|
```
|
|
@@ -151,7 +151,7 @@ routes. See [xtr-security's Quick start](../xtr-security#quick-start) for the wh
|
|
|
151
151
|
### Firewalls and the map
|
|
152
152
|
|
|
153
153
|
A **`FirewallContextInterface`** is one firewall's runtime pieces: its `name`, its
|
|
154
|
-
`authenticator_manager`, its `access_listener`,
|
|
154
|
+
`authenticator_manager`, its `access_listener`, the FastAPI `scheme`
|
|
155
155
|
it shows in OpenAPI, whether it has `security` at all, and the optional `entry_point`,
|
|
156
156
|
`access_denied_handler` and `scope_denied_handler` that answer a failure. A **`FirewallMap`**
|
|
157
157
|
holds those contexts: `match(request)` returns the first whose matcher claims the request,
|
|
@@ -332,9 +332,10 @@ answers a denied scope with a `403` carrying the RFC 6750 `insufficient_scope` c
|
|
|
332
332
|
|
|
333
333
|
### Security events
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
`
|
|
337
|
-
|
|
335
|
+
Four events are raised around a request's authentication — `CheckPassportEvent`,
|
|
336
|
+
`AuthenticationTokenCreatedEvent`, `LoginSuccessEvent`, `LoginFailureEvent`. A subscriber listens
|
|
337
|
+
to the event class itself (the dispatcher derives each event's name from its class), so there is no
|
|
338
|
+
separate table of name constants to keep in step.
|
|
338
339
|
|
|
339
340
|
## Errors
|
|
340
341
|
|
|
@@ -363,10 +364,9 @@ xtr_security_http/
|
|
|
363
364
|
├── access_map.py the ordered access-control rules
|
|
364
365
|
├── request_matcher/ path, host, method, ip, chain, callable
|
|
365
366
|
├── authorization/ OAuth2ScopeVoter, oauth2_scope, the scope-denied handler
|
|
366
|
-
├── decorator/
|
|
367
|
+
├── decorator/ IsGranted, CurrentUser, IsGrantedContext
|
|
367
368
|
├── entry_point/ AuthenticationEntryPointInterface
|
|
368
369
|
├── event/ the four firewall events
|
|
369
|
-
├── security_events.py the name each is dispatched under
|
|
370
370
|
├── event_listener/ the listeners the bundle registers (incl. the timing guard)
|
|
371
371
|
└── exception/ the errors this package adds
|
|
372
372
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-security-http"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.2"
|
|
4
4
|
description = "The HTTP edge of xtr security: firewalls, authenticators, bearer access tokens and the request surface."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -23,11 +23,13 @@ classifiers = [
|
|
|
23
23
|
"Typing :: Typed",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"xtr-security-core>=
|
|
27
|
-
"xtr-http-kernel>=
|
|
28
|
-
"xtr-
|
|
29
|
-
"xtr-
|
|
30
|
-
"xtr-
|
|
26
|
+
"xtr-security-core>=4.0,<5",
|
|
27
|
+
"xtr-http-kernel>=4.0,<5",
|
|
28
|
+
"xtr-dependency-injection[fastapi]>=4.0,<5",
|
|
29
|
+
"xtr-event-dispatcher>=4.0,<5",
|
|
30
|
+
"xtr-event-dispatcher-contracts>=4.0,<5",
|
|
31
|
+
"xtr-logging-contracts>=4.0,<5",
|
|
32
|
+
"xtr-password-hasher>=4.0,<5",
|
|
31
33
|
"fastapi>=0.123",
|
|
32
34
|
"starlette>=0.40",
|
|
33
35
|
"anyio>=4.0",
|
|
@@ -52,8 +54,6 @@ dev = [
|
|
|
52
54
|
"pytest-cov>=5",
|
|
53
55
|
"anyio>=4.0",
|
|
54
56
|
"ty>=0.0.83",
|
|
55
|
-
"xtr-dependency-injection>=3.0,<4",
|
|
56
|
-
"xtr-event-dispatcher>=3.0,<4",
|
|
57
57
|
"httpx>=0.27",
|
|
58
58
|
"joserfc>=1.7",
|
|
59
59
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-security-http"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.2"
|
|
4
4
|
description = "The HTTP edge of xtr security: firewalls, authenticators, bearer access tokens and the request surface."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -20,11 +20,13 @@ classifiers = [
|
|
|
20
20
|
"Typing :: Typed",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"xtr-security-core>=
|
|
24
|
-
"xtr-http-kernel>=
|
|
25
|
-
"xtr-
|
|
26
|
-
"xtr-
|
|
27
|
-
"xtr-
|
|
23
|
+
"xtr-security-core>=4.0,<5",
|
|
24
|
+
"xtr-http-kernel>=4.0,<5",
|
|
25
|
+
"xtr-dependency-injection[fastapi]>=4.0,<5",
|
|
26
|
+
"xtr-event-dispatcher>=4.0,<5",
|
|
27
|
+
"xtr-event-dispatcher-contracts>=4.0,<5",
|
|
28
|
+
"xtr-logging-contracts>=4.0,<5",
|
|
29
|
+
"xtr-password-hasher>=4.0,<5",
|
|
28
30
|
"fastapi>=0.123",
|
|
29
31
|
"starlette>=0.40",
|
|
30
32
|
"anyio>=4.0",
|
|
@@ -45,8 +47,6 @@ dev = [
|
|
|
45
47
|
"pytest-cov>=5",
|
|
46
48
|
"anyio>=4.0",
|
|
47
49
|
"ty>=0.0.83",
|
|
48
|
-
"xtr-dependency-injection>=3.0,<4",
|
|
49
|
-
"xtr-event-dispatcher>=3.0,<4",
|
|
50
50
|
"httpx>=0.27",
|
|
51
51
|
"joserfc>=1.7",
|
|
52
52
|
]
|
|
@@ -9,6 +9,8 @@ and xtr-http-kernel.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
13
|
+
|
|
12
14
|
from .access_map import AccessMap
|
|
13
15
|
from .access_map_interface import AccessMapInterface
|
|
14
16
|
from .access_token import (
|
|
@@ -40,6 +42,7 @@ from .authenticator.passport.badge import (
|
|
|
40
42
|
PreAuthenticatedUserBadge,
|
|
41
43
|
UserBadge,
|
|
42
44
|
)
|
|
45
|
+
from .authenticator.passport.badge.user_badge import MAX_USERNAME_LENGTH
|
|
43
46
|
from .authenticator.passport.credentials import (
|
|
44
47
|
CredentialsInterface,
|
|
45
48
|
CustomCredentials,
|
|
@@ -51,6 +54,7 @@ from .authorization import (
|
|
|
51
54
|
InsufficientScopeAccessDeniedHandler,
|
|
52
55
|
OAuth2ScopeVoter,
|
|
53
56
|
oauth2_scope,
|
|
57
|
+
parse_oauth2_scope,
|
|
54
58
|
)
|
|
55
59
|
from .decorator.current_user import CurrentUser
|
|
56
60
|
from .decorator.is_granted import IsGranted
|
|
@@ -82,8 +86,18 @@ from .firewall_scheme_registry import (
|
|
|
82
86
|
FirewallSchemeRegistry,
|
|
83
87
|
active_firewall_schemes,
|
|
84
88
|
)
|
|
89
|
+
from .request_matcher import (
|
|
90
|
+
CallableRequestMatcher,
|
|
91
|
+
ChainRequestMatcher,
|
|
92
|
+
HostRequestMatcher,
|
|
93
|
+
IpRequestMatcher,
|
|
94
|
+
MethodRequestMatcher,
|
|
95
|
+
PathRequestMatcher,
|
|
96
|
+
RequestMatcherInterface,
|
|
97
|
+
)
|
|
85
98
|
|
|
86
99
|
__all__ = [
|
|
100
|
+
"MAX_USERNAME_LENGTH",
|
|
87
101
|
"AbstractAuthenticator",
|
|
88
102
|
"AccessDeniedHandlerInterface",
|
|
89
103
|
"AccessListener",
|
|
@@ -100,7 +114,9 @@ __all__ = [
|
|
|
100
114
|
"AuthenticatorManager",
|
|
101
115
|
"AuthenticatorManagerInterface",
|
|
102
116
|
"BadgeInterface",
|
|
117
|
+
"CallableRequestMatcher",
|
|
103
118
|
"ChainAccessTokenExtractor",
|
|
119
|
+
"ChainRequestMatcher",
|
|
104
120
|
"CheckCredentialsListener",
|
|
105
121
|
"CheckPassportEvent",
|
|
106
122
|
"CredentialsInterface",
|
|
@@ -117,27 +133,41 @@ __all__ = [
|
|
|
117
133
|
"FirewallSchemeRegistry",
|
|
118
134
|
"FormEncodedBodyExtractor",
|
|
119
135
|
"HeaderAccessTokenExtractor",
|
|
136
|
+
"HostRequestMatcher",
|
|
120
137
|
"InsufficientScopeAccessDeniedHandler",
|
|
121
138
|
"InvalidAccessTokenError",
|
|
139
|
+
"IpRequestMatcher",
|
|
122
140
|
"IsGranted",
|
|
123
141
|
"IsGrantedContext",
|
|
124
142
|
"LoginFailureEvent",
|
|
125
143
|
"LoginSuccessEvent",
|
|
144
|
+
"MethodRequestMatcher",
|
|
126
145
|
"OAuth2ScopeVoter",
|
|
127
146
|
"Passport",
|
|
128
147
|
"PasswordCredentials",
|
|
129
148
|
"PasswordMigratingListener",
|
|
130
149
|
"PasswordUpgradeBadge",
|
|
150
|
+
"PathRequestMatcher",
|
|
131
151
|
"PostAuthenticationToken",
|
|
132
152
|
"PreAuthenticatedUserBadge",
|
|
133
153
|
"QueryAccessTokenExtractor",
|
|
154
|
+
"RequestMatcherInterface",
|
|
134
155
|
"SelfValidatingPassport",
|
|
135
156
|
"UnknownFirewallError",
|
|
136
157
|
"UserBadge",
|
|
137
158
|
"UserCheckerListener",
|
|
138
159
|
"UserProviderListener",
|
|
160
|
+
"__version__",
|
|
139
161
|
"active_firewall_schemes",
|
|
140
162
|
"is_sensitive",
|
|
141
163
|
"mask",
|
|
142
164
|
"oauth2_scope",
|
|
165
|
+
"parse_oauth2_scope",
|
|
143
166
|
]
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
__version__ = version("xtr-security-http")
|
|
170
|
+
except PackageNotFoundError: # pragma: no cover
|
|
171
|
+
# Running from a source tree with no installed metadata to read; having no
|
|
172
|
+
# version is better than refusing to import.
|
|
173
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
r"""Writing a ``WWW-Authenticate`` challenge: quoting and cache headers.
|
|
2
|
+
|
|
3
|
+
A challenge's ``realm``, ``error_description`` and ``scope`` are written as
|
|
4
|
+
RFC 7235 ``quoted-string`` values; a ``\`` or a ``"`` inside one must be
|
|
5
|
+
backslash-escaped or the header is malformed. Both the access-token
|
|
6
|
+
authenticator's challenge and the insufficient-scope handler's challenge quote
|
|
7
|
+
their values through here, so the escaping is written once.
|
|
8
|
+
|
|
9
|
+
Every challenge response also carries the no-store cache directives
|
|
10
|
+
:func:`no_store_headers` adds, so a ``401`` or ``403`` is never kept by a shared
|
|
11
|
+
cache or a browser and replayed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Final
|
|
18
|
+
|
|
19
|
+
from xtr_security_core.exception import InvalidArgumentError
|
|
20
|
+
|
|
21
|
+
__all__ = ["no_store_headers", "quote_auth_param"]
|
|
22
|
+
|
|
23
|
+
#: The control characters a ``quoted-string`` cannot carry. Its ``qdtext``
|
|
24
|
+
#: admits HTAB and SP; every other character below ``%x20``, and ``%x7f``, has
|
|
25
|
+
#: no spelling in the grammar — not even as a ``quoted-pair``.
|
|
26
|
+
_REFUSED_CONTROLS: Final = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
|
|
27
|
+
|
|
28
|
+
#: The cache directives a challenge response carries, so neither a shared cache
|
|
29
|
+
#: nor a browser keeps a ``401``/``403`` and replays it to another caller or
|
|
30
|
+
#: after the credential changed. ``no-store`` is the HTTP/1.1 directive;
|
|
31
|
+
#: ``Pragma: no-cache`` is the HTTP/1.0 belt-and-braces an old intermediary
|
|
32
|
+
#: still reads.
|
|
33
|
+
_NO_STORE: Final = {"Cache-Control": "no-store", "Pragma": "no-cache"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def quote_auth_param(value: str) -> str:
|
|
37
|
+
r"""Return ``value`` as an RFC 7235 ``quoted-string``, escaping ``\`` and ``"``.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
InvalidArgumentError: When ``value`` carries a control character. A
|
|
41
|
+
``quoted-string`` has no way to write one, so the challenge would be
|
|
42
|
+
malformed — and a CR or an LF would split the header. Every value
|
|
43
|
+
quoted here is declared by the application, so this is a mistake to
|
|
44
|
+
report where it is made, not a request to answer.
|
|
45
|
+
"""
|
|
46
|
+
refused = _REFUSED_CONTROLS.search(value)
|
|
47
|
+
if refused is not None:
|
|
48
|
+
raise InvalidArgumentError(
|
|
49
|
+
f"An auth-param value cannot carry the control character {refused.group()!r}.",
|
|
50
|
+
)
|
|
51
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
52
|
+
return f'"{escaped}"'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def no_store_headers(**headers: str) -> dict[str, str]:
|
|
56
|
+
"""Return ``headers`` with the no-store cache directives a challenge needs.
|
|
57
|
+
|
|
58
|
+
A challenge response must not be cached: a shared cache that kept a ``401``
|
|
59
|
+
would answer another caller's request with it, and a browser that kept one
|
|
60
|
+
would replay it after the credential changed. Every challenge this package
|
|
61
|
+
writes merges its own headers — a ``WWW-Authenticate`` — through here so the
|
|
62
|
+
directives are set in one place.
|
|
63
|
+
"""
|
|
64
|
+
return {**_NO_STORE, **headers}
|
|
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
|
|
17
17
|
from xtr_security_core.authentication.token.null_token import NullToken
|
|
18
18
|
from xtr_security_core.authorization.access_decision import AccessDecision
|
|
19
|
-
from xtr_security_core.exception import AccessDeniedError
|
|
19
|
+
from xtr_security_core.exception import AccessDeniedError
|
|
20
20
|
|
|
21
21
|
from ._state import (
|
|
22
22
|
AUTHENTICATED_FIREWALLS_KEY,
|
|
@@ -63,6 +63,10 @@ async def run_firewall( # noqa: PLR0913 -- the firewall's request, scopes and i
|
|
|
63
63
|
"""
|
|
64
64
|
context = _resolve(firewall_map, name, request)
|
|
65
65
|
if context is None or not context.security:
|
|
66
|
+
if scopes:
|
|
67
|
+
raise AccessDeniedError(
|
|
68
|
+
"A scoped resource was reached with no firewall to authenticate the caller.",
|
|
69
|
+
)
|
|
66
70
|
return
|
|
67
71
|
|
|
68
72
|
setattr(request.state, FIREWALL_CONTEXT_KEY, context)
|
|
@@ -86,25 +90,38 @@ def _resolve(
|
|
|
86
90
|
async def _authenticate_once(context: FirewallContextInterface, request: Request) -> None:
|
|
87
91
|
"""Authenticate the request under ``context`` at most once, memoised by name.
|
|
88
92
|
|
|
93
|
+
Only a settled outcome is memoised: ``None`` for a pass that authenticated,
|
|
94
|
+
the exception for one that did not. Recording a crash as a pass would let a
|
|
95
|
+
later call over the same request skip authentication and go on with a null
|
|
96
|
+
token, so whatever the authenticators raised — a failure the handlers own or
|
|
97
|
+
a bug they do not — is recorded and raised again by every later call. A later
|
|
98
|
+
call raises a fresh instance of the recorded error's type and message rather
|
|
99
|
+
than the stored instance itself, so re-raising it does not splice this
|
|
100
|
+
request's traceback onto the one the first call already unwound, nor let a
|
|
101
|
+
caller that mutates the caught exception change what a later call raises.
|
|
102
|
+
|
|
89
103
|
Raises:
|
|
90
104
|
AuthenticationError: When authentication fails, on this call and every
|
|
91
105
|
later one this request.
|
|
106
|
+
BaseException: Whatever else authentication raised, re-raised by every
|
|
107
|
+
later call this request rather than passing it off as authenticated.
|
|
92
108
|
CarriedResponse: When a handler produced a response to send.
|
|
93
109
|
"""
|
|
94
110
|
memo: dict[str, BaseException | None] = getattr(request.state, AUTHENTICATED_FIREWALLS_KEY, {})
|
|
95
111
|
if not memo:
|
|
96
112
|
setattr(request.state, AUTHENTICATED_FIREWALLS_KEY, memo)
|
|
97
113
|
if context.name in memo:
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
raise
|
|
114
|
+
recorded = memo[context.name]
|
|
115
|
+
if recorded is not None:
|
|
116
|
+
raise type(recorded)(str(recorded))
|
|
101
117
|
return
|
|
102
118
|
try:
|
|
103
119
|
response = await context.authenticator_manager.authenticate_request(request)
|
|
104
|
-
except
|
|
120
|
+
except BaseException as error:
|
|
105
121
|
memo[context.name] = error
|
|
106
122
|
raise
|
|
107
|
-
|
|
123
|
+
else:
|
|
124
|
+
memo[context.name] = None
|
|
108
125
|
if response is not None:
|
|
109
126
|
raise CarriedResponse(response)
|
|
110
127
|
|
|
@@ -22,9 +22,10 @@ __all__ = [
|
|
|
22
22
|
|
|
23
23
|
_PREFIX: Final = "_xtr_security_"
|
|
24
24
|
|
|
25
|
-
#: Maps a firewall name to the outcome of authenticating this request
|
|
26
|
-
#: the
|
|
27
|
-
#: authentication stays one pass per firewall.
|
|
25
|
+
#: Maps a firewall name to the settled outcome of authenticating this request
|
|
26
|
+
#: under it — the exception authentication raised, or ``None`` for a pass that
|
|
27
|
+
#: authenticated — so authentication stays one pass per firewall. A name is
|
|
28
|
+
#: absent until an outcome settles, so no unfinished pass reads as authenticated.
|
|
28
29
|
AUTHENTICATED_FIREWALLS_KEY: Final = f"{_PREFIX}authenticated_firewalls"
|
|
29
30
|
|
|
30
31
|
#: The firewall context in charge of the request, stashed by the firewall so the
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""An extractor that reads a bearer token from a form-encoded body."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
from urllib.parse import parse_qs
|
|
7
|
+
|
|
8
|
+
from fastapi.security import HTTPBearer
|
|
9
|
+
from typing_extensions import override
|
|
10
|
+
|
|
11
|
+
from .access_token_extractor_interface import AccessTokenExtractorInterface
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastapi.security.base import SecurityBase
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
|
|
17
|
+
__all__ = ["FormEncodedBodyExtractor"]
|
|
18
|
+
|
|
19
|
+
_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded"
|
|
20
|
+
|
|
21
|
+
#: The largest form body read, so a bearer token in a body cannot be a memory
|
|
22
|
+
#: exhaustion vector; a larger declared or streamed body yields no token.
|
|
23
|
+
_MAX_BODY_SIZE = 64 * 1024
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@final
|
|
27
|
+
class FormEncodedBodyExtractor(AccessTokenExtractorInterface):
|
|
28
|
+
"""Reads a token from a form field of an ``x-www-form-urlencoded`` body.
|
|
29
|
+
|
|
30
|
+
RFC 6750 allows a bearer token in a form body; this reads the
|
|
31
|
+
``access_token`` field of one, when the request carries that content type.
|
|
32
|
+
The body is parsed directly, so no form-parsing dependency is needed. There
|
|
33
|
+
is no dedicated OpenAPI scheme for a body-carried token, so it documents
|
|
34
|
+
itself as the standard bearer scheme.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
field_name: The form field the token is read from.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__slots__ = ("_bearer", "_field_name")
|
|
41
|
+
|
|
42
|
+
def __init__(self, field_name: str = "access_token") -> None:
|
|
43
|
+
"""Build the extractor for the ``field_name`` form field."""
|
|
44
|
+
self._field_name = field_name
|
|
45
|
+
self._bearer = HTTPBearer(auto_error=False, bearerFormat="JWT", scheme_name="Bearer")
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
async def extract_access_token(self, request: Request) -> str | None:
|
|
49
|
+
"""Return the token in the form body, or ``None`` when it is absent.
|
|
50
|
+
|
|
51
|
+
A body whose ``content-length`` is above the 64 KiB cap — or, with no
|
|
52
|
+
usable ``content-length``, one that streams past the cap — yields
|
|
53
|
+
``None``, as does a body that is not valid UTF-8; an endpoint behind the
|
|
54
|
+
firewall then challenges rather than failing on the oversize or
|
|
55
|
+
undecodable body.
|
|
56
|
+
"""
|
|
57
|
+
content_type = request.headers.get("content-type", "")
|
|
58
|
+
if not content_type.lower().startswith(_FORM_CONTENT_TYPE):
|
|
59
|
+
return None
|
|
60
|
+
body = await _read_capped(request)
|
|
61
|
+
if body is None:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
text = body.decode("utf-8", errors="strict")
|
|
65
|
+
except UnicodeDecodeError:
|
|
66
|
+
return None
|
|
67
|
+
fields = parse_qs(text)
|
|
68
|
+
values = fields.get(self._field_name)
|
|
69
|
+
return values[0] if values else None
|
|
70
|
+
|
|
71
|
+
@override
|
|
72
|
+
def scheme(self) -> SecurityBase:
|
|
73
|
+
"""Return the bearer scheme this extractor documents by."""
|
|
74
|
+
return self._bearer
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def _read_capped(request: Request) -> bytes | None:
|
|
78
|
+
"""Read the body of ``request`` up to the cap, or ``None`` when it is exceeded.
|
|
79
|
+
|
|
80
|
+
A ``content-length`` the sender declares is only believed when it is a run
|
|
81
|
+
of ASCII digits, which is all RFC 9110 allows: ``int`` would also take
|
|
82
|
+
``-1``, ``+5`` or a padded ``" 10 "``, and reading the body on the strength
|
|
83
|
+
of such a header would leave the cap unenforced. A header is decoded as
|
|
84
|
+
latin-1, so it can even carry a superscript ``²``, which counts as a digit
|
|
85
|
+
yet is no number ``int`` can read. Anything else — absent, padded, signed or
|
|
86
|
+
not a number — is read by streaming, which caps on what actually arrives
|
|
87
|
+
rather than on what was promised. A body read by streaming is cached back on
|
|
88
|
+
the request through :func:`_cache_body`, so an endpoint behind the firewall
|
|
89
|
+
can still read its own body — ``await request.body()`` or a form parse —
|
|
90
|
+
after the extractor has looked for a token.
|
|
91
|
+
"""
|
|
92
|
+
declared = request.headers.get("content-length")
|
|
93
|
+
if declared is not None and declared.isascii() and declared.isdigit():
|
|
94
|
+
if int(declared) > _MAX_BODY_SIZE:
|
|
95
|
+
return None
|
|
96
|
+
return await request.body()
|
|
97
|
+
collected = bytearray()
|
|
98
|
+
async for chunk in request.stream():
|
|
99
|
+
collected.extend(chunk)
|
|
100
|
+
if len(collected) > _MAX_BODY_SIZE:
|
|
101
|
+
return None
|
|
102
|
+
body = bytes(collected)
|
|
103
|
+
_cache_body(request, body)
|
|
104
|
+
return body
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cache_body(request: Request, body: bytes) -> None:
|
|
108
|
+
"""Hand the streamed ``body`` back to Starlette as the request's cached body.
|
|
109
|
+
|
|
110
|
+
``request.stream()`` consumes the body once; without this, a later
|
|
111
|
+
``await request.body()`` from the endpoint raises ``RuntimeError: Stream
|
|
112
|
+
consumed``. Starlette's own ``body()`` caches into the ``_body`` attribute
|
|
113
|
+
and every later ``stream()``/``body()`` serves from it, so setting that
|
|
114
|
+
attribute is how a value read ahead of the route is made available to it.
|
|
115
|
+
This is the one private attribute that mechanism exposes; it is set in this
|
|
116
|
+
single documented place.
|
|
117
|
+
"""
|
|
118
|
+
# SLF001/reportPrivateUsage: Starlette's own body() caches into this same
|
|
119
|
+
# attribute; setting it is the supported re-read path. See the docstring.
|
|
120
|
+
request._body = body # noqa: SLF001 # pyright: ignore[reportPrivateUsage]
|
|
@@ -50,7 +50,8 @@ class OidcTokenHandler:
|
|
|
50
50
|
|
|
51
51
|
The identifier is read from ``claim`` (``sub`` by default). With no provider
|
|
52
52
|
the claims are the user — an
|
|
53
|
-
:class:`~xtr_security_core.user.oidc_user.OidcUser
|
|
53
|
+
:class:`~xtr_security_core.user.oidc_user.OidcUser` built from every claim,
|
|
54
|
+
so a ``roles`` claim grants the roles it holds; with one, its
|
|
54
55
|
identifier loads the user, the claims handed alongside to a provider that
|
|
55
56
|
reads them. The badge carries the granted ``scope`` (from the ``scope``
|
|
56
57
|
string or the ``scp`` list), the ``client_id``, the ``jti`` and every claim.
|
|
@@ -158,8 +159,8 @@ class OidcTokenHandler:
|
|
|
158
159
|
"iss": {"essential": True, "values": list(self._issuers)},
|
|
159
160
|
"aud": {"essential": True, "value": self._audience},
|
|
160
161
|
"exp": {"essential": True},
|
|
161
|
-
self._claim: {"essential": True},
|
|
162
162
|
}
|
|
163
|
+
options[self._claim] = {**options.get(self._claim, {}), "essential": True}
|
|
163
164
|
registry = JWTClaimsRegistry(now=self._clock(), leeway=self._leeway, **options)
|
|
164
165
|
try:
|
|
165
166
|
registry.validate(dict(claims))
|
|
@@ -192,12 +193,11 @@ class OidcTokenHandler:
|
|
|
192
193
|
the claims travel to it; with a plain one only the identifier does.
|
|
193
194
|
"""
|
|
194
195
|
provider = self._user_provider
|
|
195
|
-
claim = self._claim
|
|
196
196
|
if provider is None:
|
|
197
197
|
|
|
198
198
|
def load_from_claims(identifier: str) -> UserInterface:
|
|
199
199
|
del identifier
|
|
200
|
-
return OidcUser(claims
|
|
200
|
+
return OidcUser.from_claims(claims)
|
|
201
201
|
|
|
202
202
|
return load_from_claims
|
|
203
203
|
if AttributesBasedUserProviderInterface in type(provider).__mro__:
|
|
@@ -8,6 +8,7 @@ from starlette.responses import JSONResponse
|
|
|
8
8
|
from typing_extensions import override
|
|
9
9
|
from xtr_security_core.exception import AuthenticationError, BadCredentialsError
|
|
10
10
|
|
|
11
|
+
from xtr_security_http._challenge import no_store_headers, quote_auth_param
|
|
11
12
|
from xtr_security_http.authenticator.passport.self_validating_passport import SelfValidatingPassport
|
|
12
13
|
from xtr_security_http.authorization.oauth2_scope_voter import OAuth2ScopeVoter
|
|
13
14
|
from xtr_security_http.entry_point.authentication_entry_point_interface import (
|
|
@@ -95,6 +96,11 @@ class AccessTokenAuthenticator(AbstractAuthenticator, AuthenticationEntryPointIn
|
|
|
95
96
|
self._failure_handler = failure_handler
|
|
96
97
|
self._realm = realm
|
|
97
98
|
|
|
99
|
+
@property
|
|
100
|
+
def extractor(self) -> AccessTokenExtractorInterface:
|
|
101
|
+
"""Return the extractor that reads the token out of a request."""
|
|
102
|
+
return self._extractor
|
|
103
|
+
|
|
98
104
|
@override
|
|
99
105
|
def supports(self, request: Request) -> bool | None:
|
|
100
106
|
"""Handle the request only when it carries a token this extractor reads.
|
|
@@ -180,14 +186,18 @@ class AccessTokenAuthenticator(AbstractAuthenticator, AuthenticationEntryPointIn
|
|
|
180
186
|
del request
|
|
181
187
|
parts = ["Bearer"]
|
|
182
188
|
if self._realm is not None:
|
|
183
|
-
parts.append(f
|
|
189
|
+
parts.append(f"realm={quote_auth_param(self._realm)}")
|
|
184
190
|
body: dict[str, str] = {}
|
|
185
191
|
if isinstance(error, InvalidAccessTokenError):
|
|
186
192
|
parts.append('error="invalid_token"')
|
|
187
|
-
parts.append(f
|
|
193
|
+
parts.append(f"error_description={quote_auth_param(error.get_message_key())}")
|
|
188
194
|
body = {"error": "invalid_token"}
|
|
189
195
|
challenge = parts[0] + (" " + ", ".join(parts[1:]) if len(parts) > 1 else "")
|
|
190
|
-
return JSONResponse(
|
|
196
|
+
return JSONResponse(
|
|
197
|
+
body,
|
|
198
|
+
status_code=401,
|
|
199
|
+
headers=no_store_headers(**{"WWW-Authenticate": challenge}),
|
|
200
|
+
)
|
|
191
201
|
|
|
192
202
|
def _normalize_scope(self, scope: object) -> list[str]:
|
|
193
203
|
"""Turn a scope list or space-separated string into a list of scopes."""
|
|
@@ -50,7 +50,12 @@ class Passport:
|
|
|
50
50
|
self._attributes: dict[str, object] = dict(attributes) if attributes is not None else {}
|
|
51
51
|
|
|
52
52
|
def add_badge(self, badge: BadgeInterface) -> Passport:
|
|
53
|
-
"""Attach ``badge``, replacing any badge of its class, and return self.
|
|
53
|
+
"""Attach ``badge``, replacing any badge of its exact class, and return self.
|
|
54
|
+
|
|
55
|
+
Badges are keyed by ``type(badge)`` — the exact runtime class, not a base
|
|
56
|
+
class — so a subclass of a badge does not replace, and is not found by,
|
|
57
|
+
the base class it derives from.
|
|
58
|
+
"""
|
|
54
59
|
self._badges[type(badge)] = badge
|
|
55
60
|
return self
|
|
56
61
|
|
|
@@ -59,7 +64,11 @@ class Passport:
|
|
|
59
64
|
return badge_class in self._badges
|
|
60
65
|
|
|
61
66
|
def get_badge(self, badge_class: type[_BadgeT]) -> _BadgeT | None:
|
|
62
|
-
"""Return the badge of ``badge_class``, or ``None`` when none is attached.
|
|
67
|
+
"""Return the badge of ``badge_class``, or ``None`` when none is attached.
|
|
68
|
+
|
|
69
|
+
The lookup is by exact class: a badge is found only under the class it was
|
|
70
|
+
attached as, never under a base class of it.
|
|
71
|
+
"""
|
|
63
72
|
badge = self._badges.get(badge_class)
|
|
64
73
|
return badge if isinstance(badge, badge_class) else None
|
|
65
74
|
|