tba-app-backend-core 0.4.0.dev2__py3-none-any.whl
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.
- tba_app_backend_core/__init__.py +15 -0
- tba_app_backend_core/app.py +146 -0
- tba_app_backend_core/plugin.py +36 -0
- tba_app_backend_core/product.py +55 -0
- tba_app_backend_core/py.typed +0 -0
- tba_app_backend_core/surfaces/__init__.py +3 -0
- tba_app_backend_core/surfaces/admin_routes.py +353 -0
- tba_app_backend_core/surfaces/advanced_protocols.py +170 -0
- tba_app_backend_core/surfaces/auth_flows.py +16 -0
- tba_app_backend_core/surfaces/authorization_server_metadata_surface.py +37 -0
- tba_app_backend_core/surfaces/authorization_surface.py +15 -0
- tba_app_backend_core/surfaces/authz_surface.py +14 -0
- tba_app_backend_core/surfaces/client_registration_surface.py +366 -0
- tba_app_backend_core/surfaces/device_authorization_surface.py +44 -0
- tba_app_backend_core/surfaces/introspection_surface.py +247 -0
- tba_app_backend_core/surfaces/login_surface.py +25 -0
- tba_app_backend_core/surfaces/logout_surface.py +38 -0
- tba_app_backend_core/surfaces/oidc_discovery_surface.py +79 -0
- tba_app_backend_core/surfaces/par_surface.py +394 -0
- tba_app_backend_core/surfaces/protected_resource_metadata_surface.py +34 -0
- tba_app_backend_core/surfaces/resource_validation_metadata_surface.py +33 -0
- tba_app_backend_core/surfaces/revocation_surface.py +39 -0
- tba_app_backend_core/surfaces/standards_manifest.py +24 -0
- tba_app_backend_core/surfaces/surface_api.py +284 -0
- tba_app_backend_core/surfaces/token_exchange_surface.py +34 -0
- tba_app_backend_core/surfaces/token_surface.py +12 -0
- tba_app_backend_core/surfaces/userinfo_surface.py +34 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/METADATA +60 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/RECORD +30 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Runtime collaborators and composition for the RFC 7662 HTTP carrier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from tigrbl import Request, TigrblApp
|
|
11
|
+
from tigrbl.engine import HybridSession as AsyncSession
|
|
12
|
+
from tigrbl.runtime.status import HTTPException
|
|
13
|
+
from tba_router_oauth_introspection import (
|
|
14
|
+
build_introspection_router,
|
|
15
|
+
include_introspection_endpoint as include_http_introspection_endpoint,
|
|
16
|
+
)
|
|
17
|
+
from tba_protocol_oauth.standards._introspection_activity import header
|
|
18
|
+
from tba_protocol_oauth.standards.jwt_client_auth import (
|
|
19
|
+
PRIVATE_KEY_JWT_AUTH_METHOD,
|
|
20
|
+
authenticate_client_assertion,
|
|
21
|
+
)
|
|
22
|
+
from tba_protocol_oauth.standards.mutual_tls_client_authentication import (
|
|
23
|
+
SUPPORTED_MTLS_AUTH_METHODS,
|
|
24
|
+
authenticate_mtls_client,
|
|
25
|
+
presented_certificate_pem,
|
|
26
|
+
presented_certificate_thumbprint,
|
|
27
|
+
)
|
|
28
|
+
from tba_protocol_oauth.standards.resource_verifier_contract import (
|
|
29
|
+
build_protected_resource_verifier_contract,
|
|
30
|
+
)
|
|
31
|
+
from tba_runtime_identity.deployment import deployment_from_request
|
|
32
|
+
from tba_runtime_identity.settings import settings
|
|
33
|
+
from tba_storage_oauth.tables import Client
|
|
34
|
+
from tba_storage_oauth.tables import ClientRegistration
|
|
35
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
36
|
+
from tba_runtime_identity_storage.ops.clients import lookup_client
|
|
37
|
+
from tigrbl import (
|
|
38
|
+
first_table_record,
|
|
39
|
+
read_table_record,
|
|
40
|
+
)
|
|
41
|
+
from tba_capability_client_secret_authentication import (
|
|
42
|
+
ClientSecretAuthenticationCapability,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from tba_server_identity.security.token_introspection import (
|
|
46
|
+
build_rfc7662_introspection_service,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_basic"
|
|
51
|
+
client_secret_authentication = ClientSecretAuthenticationCapability(lookup_client)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_transport(request: Request) -> None:
|
|
55
|
+
scope = getattr(request, "scope", {})
|
|
56
|
+
scheme = scope.get("scheme") if isinstance(scope, dict) else None
|
|
57
|
+
if not scheme:
|
|
58
|
+
try:
|
|
59
|
+
url = request.url
|
|
60
|
+
scheme = url.scheme if hasattr(url, "scheme") else str(url).split(":", 1)[0]
|
|
61
|
+
except Exception:
|
|
62
|
+
scheme = "http"
|
|
63
|
+
deployment = deployment_from_request(request, settings)
|
|
64
|
+
if deployment.flag_enabled("require_tls") and scheme != "https":
|
|
65
|
+
raise HTTPException(HTTPStatus.BAD_REQUEST, {"error": "tls_required"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _optional_registration_unavailable(exc: Exception) -> bool:
|
|
69
|
+
message = f"{exc.__class__.__module__}.{exc.__class__.__name__}: {exc}".lower()
|
|
70
|
+
if "has no attribute 'execute'" in message:
|
|
71
|
+
return True
|
|
72
|
+
return any(
|
|
73
|
+
marker in message
|
|
74
|
+
for marker in ("undefinedtable", "no such table", "missing table")
|
|
75
|
+
) or ("does not exist" in message and ("relation" in message or "table" in message))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _load_client(
|
|
79
|
+
db: Any, client_id: str
|
|
80
|
+
) -> tuple[Client | None, ClientRegistration | None]:
|
|
81
|
+
try:
|
|
82
|
+
client_key: UUID | str = UUID(str(client_id))
|
|
83
|
+
except ValueError:
|
|
84
|
+
client_key = client_id
|
|
85
|
+
client = await read_table_record(Client, db, client_key)
|
|
86
|
+
registration = None
|
|
87
|
+
if client is not None:
|
|
88
|
+
try:
|
|
89
|
+
registration = await first_table_record(
|
|
90
|
+
ClientRegistration, db, {"client_id": client.id}
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
if not _optional_registration_unavailable(exc):
|
|
94
|
+
raise
|
|
95
|
+
return client, registration
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _registered_auth_method(registration: ClientRegistration | None) -> str:
|
|
99
|
+
raw = getattr(registration, "registration_metadata", None)
|
|
100
|
+
metadata = raw if isinstance(raw, dict) else {}
|
|
101
|
+
return str(
|
|
102
|
+
metadata.get("token_endpoint_auth_method") or DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _protected_resource_verifier_contract(request: Any):
|
|
107
|
+
return build_protected_resource_verifier_contract(
|
|
108
|
+
deployment_from_request(request, settings)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _introspection_endpoint_audiences(request: Any) -> set[str]:
|
|
113
|
+
deployment = deployment_from_request(request, settings)
|
|
114
|
+
issuer = str(getattr(deployment, "issuer", None) or settings.issuer).rstrip("/")
|
|
115
|
+
return {issuer, f"{issuer}/introspect", f"{issuer}/token"} - {""}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def authorize_introspection_caller(
|
|
119
|
+
request: Request,
|
|
120
|
+
form_data: Mapping[str, object],
|
|
121
|
+
db: AsyncSession,
|
|
122
|
+
) -> None:
|
|
123
|
+
contract = _protected_resource_verifier_contract(request)
|
|
124
|
+
allowed_auth_methods = set(contract.introspection_auth_methods)
|
|
125
|
+
authorization = header(request, "Authorization") or ""
|
|
126
|
+
client_assertion = str(form_data.get("client_assertion") or "").strip()
|
|
127
|
+
client_assertion_type = str(form_data.get("client_assertion_type") or "").strip()
|
|
128
|
+
client_id: str | None = None
|
|
129
|
+
client_secret: str | None = None
|
|
130
|
+
|
|
131
|
+
if authorization.startswith("Basic "):
|
|
132
|
+
try:
|
|
133
|
+
decoded = base64.b64decode(authorization.split()[1]).decode()
|
|
134
|
+
client_id, client_secret = decoded.split(":", 1)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client") from exc
|
|
137
|
+
else:
|
|
138
|
+
client_id = str(form_data.get("client_id") or "").strip() or None
|
|
139
|
+
client_secret = str(form_data.get("client_secret") or "").strip() or None
|
|
140
|
+
|
|
141
|
+
if client_assertion and not client_id:
|
|
142
|
+
try:
|
|
143
|
+
claims = authenticate_client_assertion(
|
|
144
|
+
client_assertion_type=client_assertion_type,
|
|
145
|
+
client_assertion=client_assertion,
|
|
146
|
+
audience=_introspection_endpoint_audiences(request),
|
|
147
|
+
client_id=None,
|
|
148
|
+
)
|
|
149
|
+
except ValueError as exc:
|
|
150
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client") from exc
|
|
151
|
+
client_id = str(claims.get("iss") or "").strip() or None
|
|
152
|
+
|
|
153
|
+
if not client_id:
|
|
154
|
+
raise HTTPException(
|
|
155
|
+
HTTPStatus.UNAUTHORIZED,
|
|
156
|
+
"authenticated caller required",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
client, registration = await _load_client(db, client_id)
|
|
160
|
+
if client is None:
|
|
161
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client")
|
|
162
|
+
|
|
163
|
+
registered_auth_method = _registered_auth_method(registration)
|
|
164
|
+
raw_metadata = getattr(registration, "registration_metadata", None)
|
|
165
|
+
registration_metadata = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
166
|
+
|
|
167
|
+
if client_assertion:
|
|
168
|
+
if (
|
|
169
|
+
PRIVATE_KEY_JWT_AUTH_METHOD not in allowed_auth_methods
|
|
170
|
+
or registered_auth_method != PRIVATE_KEY_JWT_AUTH_METHOD
|
|
171
|
+
):
|
|
172
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client")
|
|
173
|
+
try:
|
|
174
|
+
authenticate_client_assertion(
|
|
175
|
+
client_assertion_type=client_assertion_type,
|
|
176
|
+
client_assertion=client_assertion,
|
|
177
|
+
audience=_introspection_endpoint_audiences(request),
|
|
178
|
+
client_id=str(client.id),
|
|
179
|
+
token_endpoint_auth_method=registered_auth_method,
|
|
180
|
+
)
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client") from exc
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
if registered_auth_method in SUPPORTED_MTLS_AUTH_METHODS:
|
|
186
|
+
if registered_auth_method not in allowed_auth_methods:
|
|
187
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client")
|
|
188
|
+
try:
|
|
189
|
+
authenticate_mtls_client(
|
|
190
|
+
registration_metadata,
|
|
191
|
+
presented_certificate_thumbprint(request),
|
|
192
|
+
presented_certificate_pem=presented_certificate_pem(request),
|
|
193
|
+
token_endpoint_auth_method=registered_auth_method,
|
|
194
|
+
)
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client") from exc
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
if registered_auth_method == "client_secret_post":
|
|
200
|
+
valid_shape = registered_auth_method in allowed_auth_methods and bool(
|
|
201
|
+
client_secret
|
|
202
|
+
)
|
|
203
|
+
elif registered_auth_method == "client_secret_basic":
|
|
204
|
+
valid_shape = (
|
|
205
|
+
registered_auth_method in allowed_auth_methods
|
|
206
|
+
and authorization.startswith("Basic ")
|
|
207
|
+
and bool(client_secret)
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
valid_shape = False
|
|
211
|
+
if not valid_shape:
|
|
212
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client")
|
|
213
|
+
|
|
214
|
+
result = client_secret_authentication.verify_client_record(client, client_secret)
|
|
215
|
+
if not result.authenticated:
|
|
216
|
+
raise HTTPException(HTTPStatus.UNAUTHORIZED, "invalid_client")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _service_for_request(request: Request):
|
|
220
|
+
return build_rfc7662_introspection_service(settings)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
router = build_introspection_router(
|
|
224
|
+
service_for_request=_service_for_request,
|
|
225
|
+
authorize_caller=authorize_introspection_caller,
|
|
226
|
+
require_transport=_require_transport,
|
|
227
|
+
get_db=get_db,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def include_introspection_endpoint(app: TigrblApp) -> None:
|
|
232
|
+
include_http_introspection_endpoint(
|
|
233
|
+
app,
|
|
234
|
+
router,
|
|
235
|
+
enabled=bool(settings.enable_rfc7662),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
include_rfc7662 = include_introspection_endpoint
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = [
|
|
243
|
+
"authorize_introspection_caller",
|
|
244
|
+
"include_introspection_endpoint",
|
|
245
|
+
"include_rfc7662",
|
|
246
|
+
"router",
|
|
247
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Runtime composition for the interactive session-login HTTP carrier."""
|
|
2
|
+
|
|
3
|
+
from tba_router_session_login import build_login_router
|
|
4
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
5
|
+
|
|
6
|
+
from tba_server_identity.login_runtime import (
|
|
7
|
+
change_required_password,
|
|
8
|
+
login_user,
|
|
9
|
+
password_authentication,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
router = build_login_router(
|
|
14
|
+
login_request=login_user,
|
|
15
|
+
get_db=get_db,
|
|
16
|
+
required_password_change_request=change_required_password,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"change_required_password",
|
|
22
|
+
"login_user",
|
|
23
|
+
"password_authentication",
|
|
24
|
+
"router",
|
|
25
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Runtime composition for the OpenID Connect logout HTTP carrier."""
|
|
2
|
+
|
|
3
|
+
from tigrbl import TigrblApp
|
|
4
|
+
from tba_router_oidc_logout import (
|
|
5
|
+
build_logout_router,
|
|
6
|
+
include_logout_endpoint as include_logout_carrier,
|
|
7
|
+
)
|
|
8
|
+
from tba_runtime_identity.deployment import deployment_from_app
|
|
9
|
+
from tba_runtime_identity.settings import settings
|
|
10
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
11
|
+
|
|
12
|
+
from tba_server_identity.logout_runtime import logout, logout_request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
router = build_logout_router(logout_request=logout, get_db=get_db)
|
|
16
|
+
api = router
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def include_logout_endpoint(app: TigrblApp) -> None:
|
|
20
|
+
deployment = deployment_from_app(app, settings)
|
|
21
|
+
include_logout_carrier(
|
|
22
|
+
app,
|
|
23
|
+
router,
|
|
24
|
+
enabled=deployment.route_enabled("/logout"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
include_oidc_rp_initiated_logout = include_logout_endpoint
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"api",
|
|
33
|
+
"include_logout_endpoint",
|
|
34
|
+
"include_oidc_rp_initiated_logout",
|
|
35
|
+
"logout",
|
|
36
|
+
"logout_request",
|
|
37
|
+
"router",
|
|
38
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Runtime composition for OIDC discovery and JWKS HTTP carriers."""
|
|
2
|
+
|
|
3
|
+
from tigrbl import TigrblApp, TigrblRouter
|
|
4
|
+
from tba_router_oidc_discovery import (
|
|
5
|
+
build_oidc_discovery_routers,
|
|
6
|
+
include_jwks as include_jwks_carrier,
|
|
7
|
+
include_oidc_discovery as include_oidc_discovery_carrier,
|
|
8
|
+
include_openid_configuration as include_openid_configuration_carrier,
|
|
9
|
+
)
|
|
10
|
+
from tba_protocol_oauth.standards.authorization_server_metadata import (
|
|
11
|
+
JWKS_PATH,
|
|
12
|
+
)
|
|
13
|
+
from tba_runtime_identity.deployment import deployment_from_app
|
|
14
|
+
from tba_runtime_identity.settings import settings
|
|
15
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
16
|
+
|
|
17
|
+
from tba_server_identity.oidc_discovery_runtime import (
|
|
18
|
+
jwks,
|
|
19
|
+
openid_configuration,
|
|
20
|
+
realm_jwks,
|
|
21
|
+
realm_openid_configuration,
|
|
22
|
+
tenant_jwks,
|
|
23
|
+
tenant_openid_configuration,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
discovery_api, jwks_api = build_oidc_discovery_routers(
|
|
28
|
+
openid_configuration=openid_configuration,
|
|
29
|
+
tenant_openid_configuration=tenant_openid_configuration,
|
|
30
|
+
realm_openid_configuration=realm_openid_configuration,
|
|
31
|
+
jwks=jwks,
|
|
32
|
+
tenant_jwks=tenant_jwks,
|
|
33
|
+
realm_jwks=realm_jwks,
|
|
34
|
+
get_db=get_db,
|
|
35
|
+
)
|
|
36
|
+
api = TigrblRouter()
|
|
37
|
+
api.include_router(discovery_api)
|
|
38
|
+
api.include_router(jwks_api)
|
|
39
|
+
router = api
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def include_openid_configuration(app: TigrblApp) -> None:
|
|
43
|
+
deployment = deployment_from_app(app, settings)
|
|
44
|
+
include_openid_configuration_carrier(
|
|
45
|
+
app,
|
|
46
|
+
discovery_api,
|
|
47
|
+
enabled=deployment.route_enabled("/.well-known/openid-configuration"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def include_jwks(app: TigrblApp) -> None:
|
|
52
|
+
deployment = deployment_from_app(app, settings)
|
|
53
|
+
include_jwks_carrier(
|
|
54
|
+
app,
|
|
55
|
+
jwks_api,
|
|
56
|
+
enabled=deployment.route_enabled(JWKS_PATH),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def include_oidc_discovery(app: TigrblApp) -> None:
|
|
61
|
+
deployment = deployment_from_app(app, settings)
|
|
62
|
+
include_oidc_discovery_carrier(
|
|
63
|
+
app,
|
|
64
|
+
discovery_api,
|
|
65
|
+
jwks_api,
|
|
66
|
+
discovery_enabled=deployment.route_enabled("/.well-known/openid-configuration"),
|
|
67
|
+
jwks_enabled=deployment.route_enabled(JWKS_PATH),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"api",
|
|
73
|
+
"discovery_api",
|
|
74
|
+
"include_jwks",
|
|
75
|
+
"include_oidc_discovery",
|
|
76
|
+
"include_openid_configuration",
|
|
77
|
+
"jwks_api",
|
|
78
|
+
"router",
|
|
79
|
+
]
|