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.
Files changed (30) hide show
  1. tba_app_backend_core/__init__.py +15 -0
  2. tba_app_backend_core/app.py +146 -0
  3. tba_app_backend_core/plugin.py +36 -0
  4. tba_app_backend_core/product.py +55 -0
  5. tba_app_backend_core/py.typed +0 -0
  6. tba_app_backend_core/surfaces/__init__.py +3 -0
  7. tba_app_backend_core/surfaces/admin_routes.py +353 -0
  8. tba_app_backend_core/surfaces/advanced_protocols.py +170 -0
  9. tba_app_backend_core/surfaces/auth_flows.py +16 -0
  10. tba_app_backend_core/surfaces/authorization_server_metadata_surface.py +37 -0
  11. tba_app_backend_core/surfaces/authorization_surface.py +15 -0
  12. tba_app_backend_core/surfaces/authz_surface.py +14 -0
  13. tba_app_backend_core/surfaces/client_registration_surface.py +366 -0
  14. tba_app_backend_core/surfaces/device_authorization_surface.py +44 -0
  15. tba_app_backend_core/surfaces/introspection_surface.py +247 -0
  16. tba_app_backend_core/surfaces/login_surface.py +25 -0
  17. tba_app_backend_core/surfaces/logout_surface.py +38 -0
  18. tba_app_backend_core/surfaces/oidc_discovery_surface.py +79 -0
  19. tba_app_backend_core/surfaces/par_surface.py +394 -0
  20. tba_app_backend_core/surfaces/protected_resource_metadata_surface.py +34 -0
  21. tba_app_backend_core/surfaces/resource_validation_metadata_surface.py +33 -0
  22. tba_app_backend_core/surfaces/revocation_surface.py +39 -0
  23. tba_app_backend_core/surfaces/standards_manifest.py +24 -0
  24. tba_app_backend_core/surfaces/surface_api.py +284 -0
  25. tba_app_backend_core/surfaces/token_exchange_surface.py +34 -0
  26. tba_app_backend_core/surfaces/token_surface.py +12 -0
  27. tba_app_backend_core/surfaces/userinfo_surface.py +34 -0
  28. tba_app_backend_core-0.4.0.dev2.dist-info/METADATA +60 -0
  29. tba_app_backend_core-0.4.0.dev2.dist-info/RECORD +30 -0
  30. 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
+ ]