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,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import asdict, is_dataclass
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tigrbl import TigrblApp, TigrblRouter
|
|
9
|
+
from tba_contracts_identity.attestation import AttestationEvidence
|
|
10
|
+
from tigrbl import Request
|
|
11
|
+
from tigrbl.runtime.status import HTTPException, status
|
|
12
|
+
|
|
13
|
+
api = TigrblRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _components(request: Request) -> Mapping[str, object]:
|
|
17
|
+
app = getattr(request, "app", None)
|
|
18
|
+
state = getattr(app, "state", None)
|
|
19
|
+
components = getattr(state, "tba_protocol_components", None)
|
|
20
|
+
return components if isinstance(components, Mapping) else {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _component(request: Request, name: str) -> object:
|
|
24
|
+
component = _components(request).get(name)
|
|
25
|
+
if component is None:
|
|
26
|
+
raise HTTPException(
|
|
27
|
+
status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
28
|
+
f"{name} protocol component is not configured",
|
|
29
|
+
)
|
|
30
|
+
return component
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _payload(request: Request) -> Mapping[str, Any]:
|
|
34
|
+
value = await request.json()
|
|
35
|
+
if not isinstance(value, Mapping):
|
|
36
|
+
raise HTTPException(
|
|
37
|
+
status.HTTP_400_BAD_REQUEST, "request body must be an object"
|
|
38
|
+
)
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _jsonable(value: object) -> object:
|
|
43
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
44
|
+
return _jsonable(asdict(value))
|
|
45
|
+
if isinstance(value, Mapping):
|
|
46
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
47
|
+
if isinstance(value, (tuple, list, set, frozenset)):
|
|
48
|
+
return [_jsonable(item) for item in value]
|
|
49
|
+
if isinstance(value, (datetime, date)):
|
|
50
|
+
return value.isoformat()
|
|
51
|
+
if isinstance(value, bytes):
|
|
52
|
+
return value.hex()
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@api.route("/credential", methods=["POST"], include_in_schema=True, tags=["oid4vci"])
|
|
57
|
+
async def issue_credential(request: Request) -> object:
|
|
58
|
+
payload = await _payload(request)
|
|
59
|
+
protocol = _component(request, "oid4vci")
|
|
60
|
+
try:
|
|
61
|
+
result = protocol.credential(
|
|
62
|
+
payload,
|
|
63
|
+
wallet_attestation=request.headers.get("OAuth-Client-Attestation"),
|
|
64
|
+
)
|
|
65
|
+
except (LookupError, PermissionError, ValueError) as exc:
|
|
66
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
|
67
|
+
return _jsonable(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@api.route(
|
|
71
|
+
"/presentation/verify", methods=["POST"], include_in_schema=True, tags=["oid4vp"]
|
|
72
|
+
)
|
|
73
|
+
async def verify_presentation(request: Request) -> object:
|
|
74
|
+
payload = await _payload(request)
|
|
75
|
+
holder, vp_token, authorization_request = (
|
|
76
|
+
payload.get("holder"),
|
|
77
|
+
payload.get("vp_token"),
|
|
78
|
+
payload.get("authorization_request"),
|
|
79
|
+
)
|
|
80
|
+
if (
|
|
81
|
+
not isinstance(holder, str)
|
|
82
|
+
or not isinstance(vp_token, (str, bytes))
|
|
83
|
+
or not isinstance(authorization_request, Mapping)
|
|
84
|
+
):
|
|
85
|
+
raise HTTPException(
|
|
86
|
+
status.HTTP_400_BAD_REQUEST,
|
|
87
|
+
"holder, vp_token, and authorization_request are required",
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
result = _component(request, "oid4vp").verify(
|
|
91
|
+
holder, vp_token, authorization_request
|
|
92
|
+
)
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
|
95
|
+
return _jsonable(result)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@api.route(
|
|
99
|
+
"/access/v1/evaluation", methods=["POST"], include_in_schema=True, tags=["authzen"]
|
|
100
|
+
)
|
|
101
|
+
async def evaluate_access(request: Request) -> object:
|
|
102
|
+
try:
|
|
103
|
+
return _component(request, "authzen").access_evaluation(await _payload(request))
|
|
104
|
+
except ValueError as exc:
|
|
105
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@api.route("/gnap/tx", methods=["POST"], include_in_schema=True, tags=["gnap"])
|
|
109
|
+
async def process_gnap(request: Request) -> object:
|
|
110
|
+
handler = _component(request, "gnap")
|
|
111
|
+
from tba_protocol_gnap import parse_gnap_request
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
parsed = parse_gnap_request(await _payload(request))
|
|
115
|
+
result = handler(parsed)
|
|
116
|
+
except ValueError as exc:
|
|
117
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
|
118
|
+
return _jsonable(result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@api.route(
|
|
122
|
+
"/security-events/receive", methods=["POST"], include_in_schema=True, tags=["set"]
|
|
123
|
+
)
|
|
124
|
+
async def receive_security_event(request: Request) -> object:
|
|
125
|
+
payload = await _payload(request)
|
|
126
|
+
token = payload.get("set")
|
|
127
|
+
if not isinstance(token, str):
|
|
128
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, "set token is required")
|
|
129
|
+
try:
|
|
130
|
+
event = _component(request, "security_events").receive(token)
|
|
131
|
+
except ValueError as exc:
|
|
132
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
|
133
|
+
return _jsonable(event)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@api.route(
|
|
137
|
+
"/attestations/appraise",
|
|
138
|
+
methods=["POST"],
|
|
139
|
+
include_in_schema=True,
|
|
140
|
+
tags=["attestation"],
|
|
141
|
+
)
|
|
142
|
+
async def appraise_attestation(request: Request) -> object:
|
|
143
|
+
payload = await _payload(request)
|
|
144
|
+
profile, claims, raw = (
|
|
145
|
+
payload.get("profile"),
|
|
146
|
+
payload.get("claims"),
|
|
147
|
+
payload.get("raw"),
|
|
148
|
+
)
|
|
149
|
+
if (
|
|
150
|
+
not isinstance(profile, str)
|
|
151
|
+
or not isinstance(claims, Mapping)
|
|
152
|
+
or (raw is not None and not isinstance(raw, str))
|
|
153
|
+
):
|
|
154
|
+
raise HTTPException(
|
|
155
|
+
status.HTTP_400_BAD_REQUEST, "profile and claims are required"
|
|
156
|
+
)
|
|
157
|
+
evidence = AttestationEvidence(profile, dict(claims), raw)
|
|
158
|
+
return _jsonable(await _component(request, "attestation").appraise(evidence))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def include_advanced_protocols(app: TigrblApp) -> None:
|
|
162
|
+
existing = {
|
|
163
|
+
getattr(route, "path", None) or getattr(route, "path_template", None)
|
|
164
|
+
for route in getattr(getattr(app, "router", app), "routes", ())
|
|
165
|
+
}
|
|
166
|
+
if "/credential" not in existing:
|
|
167
|
+
app.include_router(api)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = ["api", "include_advanced_protocols"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""OAuth/OIDC login and authorization-flow route composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tigrbl import TigrblRouter
|
|
6
|
+
|
|
7
|
+
from .authz_surface import router as authz_router
|
|
8
|
+
from .introspection_surface import router as introspection_router
|
|
9
|
+
from .login_surface import router as login_router
|
|
10
|
+
|
|
11
|
+
router = TigrblRouter()
|
|
12
|
+
router.include_router(login_router)
|
|
13
|
+
router.include_router(authz_router)
|
|
14
|
+
router.include_router(introspection_router)
|
|
15
|
+
|
|
16
|
+
__all__ = ["router"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Runtime composition for the RFC 8414 HTTP carrier."""
|
|
2
|
+
|
|
3
|
+
from tigrbl import TigrblApp
|
|
4
|
+
from tba_router_oauth_authorization_server_metadata import (
|
|
5
|
+
build_authorization_server_metadata_router,
|
|
6
|
+
include_authorization_server_metadata,
|
|
7
|
+
)
|
|
8
|
+
from tba_protocol_oauth.standards.authorization_server_metadata import (
|
|
9
|
+
RFC8414_METADATA_PATH,
|
|
10
|
+
)
|
|
11
|
+
from tba_runtime_identity.deployment import deployment_from_app
|
|
12
|
+
from tba_runtime_identity.settings import settings
|
|
13
|
+
|
|
14
|
+
from tba_server_identity.authorization_server_metadata_runtime import (
|
|
15
|
+
authorization_server_metadata,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
router = build_authorization_server_metadata_router(
|
|
20
|
+
authorization_server_metadata=authorization_server_metadata,
|
|
21
|
+
)
|
|
22
|
+
api = router
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def include_rfc8414(app: TigrblApp) -> None:
|
|
26
|
+
deployment = deployment_from_app(app, settings)
|
|
27
|
+
include_authorization_server_metadata(
|
|
28
|
+
app,
|
|
29
|
+
router,
|
|
30
|
+
enabled=(
|
|
31
|
+
bool(settings.enable_rfc8414)
|
|
32
|
+
and deployment.route_enabled(RFC8414_METADATA_PATH)
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["api", "include_rfc8414", "router"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Runtime composition for the OAuth/OIDC authorization HTTP carrier."""
|
|
2
|
+
|
|
3
|
+
from tba_router_oauth_authorization import build_authorization_router
|
|
4
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
5
|
+
|
|
6
|
+
from tba_server_identity.authorization_runtime import authorize_request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
router = build_authorization_router(
|
|
10
|
+
authorize_request=authorize_request,
|
|
11
|
+
get_db=get_db,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["authorize_request", "router"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Legacy authorization and token route composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tigrbl import TigrblRouter
|
|
6
|
+
|
|
7
|
+
from .authorization_surface import router as authorize_router
|
|
8
|
+
from .token_surface import router as token_router
|
|
9
|
+
|
|
10
|
+
router = TigrblRouter()
|
|
11
|
+
router.include_router(authorize_router)
|
|
12
|
+
router.include_router(token_router)
|
|
13
|
+
|
|
14
|
+
__all__ = ["router"]
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Runtime security, policy, and composition for RFC 7591 and RFC 7592."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import secrets
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from uuid import UUID, uuid4
|
|
9
|
+
|
|
10
|
+
from tigrbl import Request, TigrblApp
|
|
11
|
+
from tigrbl.runtime.status import HTTPException, status
|
|
12
|
+
from tba_router_oauth_registration import (
|
|
13
|
+
build_client_registration_router,
|
|
14
|
+
include_client_registration_endpoint as include_http_registration_endpoint,
|
|
15
|
+
)
|
|
16
|
+
from tba_protocol_oauth.schemas import (
|
|
17
|
+
DynamicClientRegistrationIn,
|
|
18
|
+
DynamicClientRegistrationManagementIn,
|
|
19
|
+
)
|
|
20
|
+
from tba_protocol_oauth.standards.client_registration_management import (
|
|
21
|
+
RFC7592ClientRegistrationManagementService,
|
|
22
|
+
)
|
|
23
|
+
from tba_protocol_oauth.standards.dynamic_client_registration import (
|
|
24
|
+
RFC7591DynamicClientRegistrationService,
|
|
25
|
+
)
|
|
26
|
+
from tba_protocol_oauth.standards.oauth_security_bcp import (
|
|
27
|
+
runtime_security_profile,
|
|
28
|
+
)
|
|
29
|
+
from tba_contracts_identity.oauth import (
|
|
30
|
+
ClientRegistrationCreateRequest,
|
|
31
|
+
ClientRegistrationRecord,
|
|
32
|
+
ClientRegistrationUpdateRequest,
|
|
33
|
+
)
|
|
34
|
+
from tba_primitives_identity.constant_time import text_equal
|
|
35
|
+
from tba_primitives_identity.digests import token_hash
|
|
36
|
+
from tba_provider_secret_hashing_bcrypt import hash_pw
|
|
37
|
+
from tba_runtime_identity.deployment import (
|
|
38
|
+
deployment_from_app,
|
|
39
|
+
deployment_from_request,
|
|
40
|
+
resolve_deployment,
|
|
41
|
+
)
|
|
42
|
+
from tba_runtime_identity.settings import settings
|
|
43
|
+
from tba_storage_core.tables import Tenant
|
|
44
|
+
from tba_runtime_identity_storage.engine import get_db
|
|
45
|
+
from tigrbl import (
|
|
46
|
+
create_record,
|
|
47
|
+
first_record,
|
|
48
|
+
read_record,
|
|
49
|
+
)
|
|
50
|
+
from tba_runtime_identity_storage.tenant_state import enabled_tenant_record
|
|
51
|
+
|
|
52
|
+
from tba_server_identity.security.client_registration import (
|
|
53
|
+
build_client_registration_capability,
|
|
54
|
+
)
|
|
55
|
+
from tba_server_identity.client_registration_support import (
|
|
56
|
+
bearer_token as _bearer_token,
|
|
57
|
+
merge_registration_metadata as _merge_registration_metadata,
|
|
58
|
+
registration_response as _registration_response,
|
|
59
|
+
validate_registration_metadata,
|
|
60
|
+
validate_token_endpoint_auth_method as _validated_token_endpoint_auth_method,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _tenant_from_operator_record(
|
|
65
|
+
record: dict[str, object], *, tenant_slug: str
|
|
66
|
+
) -> Tenant | None:
|
|
67
|
+
data = record.get("data") if isinstance(record.get("data"), dict) else {}
|
|
68
|
+
sql_tenant_id = record.get("sql_tenant_id") or data.get("sql_tenant_id")
|
|
69
|
+
if not sql_tenant_id:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
tenant_id = UUID(str(sql_tenant_id))
|
|
73
|
+
except ValueError:
|
|
74
|
+
return None
|
|
75
|
+
base_name = (
|
|
76
|
+
str(record.get("name") or data.get("name") or "Tenant").strip() or "Tenant"
|
|
77
|
+
)
|
|
78
|
+
return Tenant(
|
|
79
|
+
id=tenant_id,
|
|
80
|
+
slug=tenant_slug,
|
|
81
|
+
name=f"{base_name}-{tenant_slug}"[:120],
|
|
82
|
+
email=f"{tenant_slug}@tenant.local"[:120],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _resolve_registration_tenant(*, db, tenant_slug: str) -> Tenant:
|
|
87
|
+
tenant = await first_record(Tenant, db, {"slug": tenant_slug})
|
|
88
|
+
if tenant is not None:
|
|
89
|
+
return tenant
|
|
90
|
+
record = enabled_tenant_record(Path.cwd(), tenant_slug)
|
|
91
|
+
if record is None:
|
|
92
|
+
raise HTTPException(status.HTTP_404_NOT_FOUND, "tenant not found")
|
|
93
|
+
tenant = _tenant_from_operator_record(record, tenant_slug=tenant_slug)
|
|
94
|
+
if tenant is not None:
|
|
95
|
+
payload = {
|
|
96
|
+
"id": tenant.id,
|
|
97
|
+
"slug": tenant.slug,
|
|
98
|
+
"name": tenant.name,
|
|
99
|
+
"email": tenant.email,
|
|
100
|
+
}
|
|
101
|
+
else:
|
|
102
|
+
base_name = str(record.get("name") or "Tenant").strip() or "Tenant"
|
|
103
|
+
payload = {
|
|
104
|
+
"slug": tenant_slug,
|
|
105
|
+
"name": f"{base_name}-{tenant_slug}"[:120],
|
|
106
|
+
"email": f"{tenant_slug}@tenant.local"[:120],
|
|
107
|
+
}
|
|
108
|
+
try:
|
|
109
|
+
return await create_record(Tenant, db, payload)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
tenant = await first_record(Tenant, db, {"slug": tenant_slug})
|
|
112
|
+
if tenant is None:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status.HTTP_409_CONFLICT,
|
|
115
|
+
"tenant materialization conflict",
|
|
116
|
+
) from exc
|
|
117
|
+
return tenant
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _validated_registration_payload(
|
|
121
|
+
*,
|
|
122
|
+
db,
|
|
123
|
+
payload: DynamicClientRegistrationIn,
|
|
124
|
+
deployment=None,
|
|
125
|
+
) -> tuple[Tenant, dict[str, object]]:
|
|
126
|
+
deployment = deployment if deployment is not None else resolve_deployment(settings)
|
|
127
|
+
policy = runtime_security_profile(deployment)
|
|
128
|
+
tenant = await _resolve_registration_tenant(
|
|
129
|
+
db=db,
|
|
130
|
+
tenant_slug=payload.tenant_slug,
|
|
131
|
+
)
|
|
132
|
+
derived_metadata = validate_registration_metadata(payload, policy=policy)
|
|
133
|
+
return tenant, derived_metadata
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _deployment(request: Request):
|
|
137
|
+
if getattr(request, "app", None) is not None:
|
|
138
|
+
return deployment_from_request(request, settings)
|
|
139
|
+
return resolve_deployment(settings)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _rfc7591_service(request: Request, db):
|
|
143
|
+
deployment = _deployment(request)
|
|
144
|
+
return RFC7591DynamicClientRegistrationService(
|
|
145
|
+
build_client_registration_capability(db),
|
|
146
|
+
enabled=deployment.flag_enabled("enable_rfc7591"),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _rfc7592_service(request: Request, db):
|
|
151
|
+
deployment = _deployment(request)
|
|
152
|
+
return RFC7592ClientRegistrationManagementService(
|
|
153
|
+
build_client_registration_capability(db),
|
|
154
|
+
enabled=deployment.flag_enabled("enable_rfc7592"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def register_client(
|
|
159
|
+
request: Request,
|
|
160
|
+
db,
|
|
161
|
+
payload: DynamicClientRegistrationIn,
|
|
162
|
+
) -> dict[str, object]:
|
|
163
|
+
deployment = _deployment(request)
|
|
164
|
+
if not deployment.flag_enabled("enable_rfc7591"):
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status.HTTP_404_NOT_FOUND,
|
|
167
|
+
"dynamic client registration disabled",
|
|
168
|
+
)
|
|
169
|
+
tenant, derived_metadata = await _validated_registration_payload(
|
|
170
|
+
db=db,
|
|
171
|
+
payload=payload,
|
|
172
|
+
deployment=deployment,
|
|
173
|
+
)
|
|
174
|
+
policy = runtime_security_profile(deployment)
|
|
175
|
+
client_id = str(uuid4())
|
|
176
|
+
client_secret = secrets.token_urlsafe(32)
|
|
177
|
+
registration_access_token = None
|
|
178
|
+
registration_access_token_hash = None
|
|
179
|
+
registration_client_uri = None
|
|
180
|
+
if deployment.flag_enabled("enable_rfc7592"):
|
|
181
|
+
registration_access_token = secrets.token_urlsafe(32)
|
|
182
|
+
registration_access_token_hash = token_hash(registration_access_token)
|
|
183
|
+
registration_client_uri = (
|
|
184
|
+
f"{str(deployment.issuer or settings.issuer).rstrip('/')}"
|
|
185
|
+
f"/register/{client_id}"
|
|
186
|
+
)
|
|
187
|
+
metadata = _merge_registration_metadata(
|
|
188
|
+
payload,
|
|
189
|
+
derived_metadata=derived_metadata,
|
|
190
|
+
)
|
|
191
|
+
record = await _rfc7591_service(request, db).register(
|
|
192
|
+
ClientRegistrationCreateRequest(
|
|
193
|
+
client_id=client_id,
|
|
194
|
+
tenant_id=str(tenant.id),
|
|
195
|
+
client_secret_hash=hash_pw(client_secret),
|
|
196
|
+
redirect_uris=tuple(payload.redirect_uris),
|
|
197
|
+
grant_types=tuple(payload.grant_types),
|
|
198
|
+
response_types=tuple(payload.response_types),
|
|
199
|
+
metadata=metadata,
|
|
200
|
+
contacts=tuple(str(item) for item in payload.contacts or ()),
|
|
201
|
+
software_id=payload.software_id,
|
|
202
|
+
software_version=payload.software_version,
|
|
203
|
+
registration_access_token_hash=registration_access_token_hash,
|
|
204
|
+
registration_client_uri=registration_client_uri,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
commit = getattr(db, "commit", None)
|
|
208
|
+
if callable(commit):
|
|
209
|
+
committed = commit()
|
|
210
|
+
if inspect.isawaitable(committed):
|
|
211
|
+
await committed
|
|
212
|
+
suppress_secret = policy.fapi_mode and (
|
|
213
|
+
payload.token_endpoint_auth_method in set(policy.allowed_client_auth_methods)
|
|
214
|
+
)
|
|
215
|
+
return _registration_response(
|
|
216
|
+
record,
|
|
217
|
+
registration_access_token=registration_access_token,
|
|
218
|
+
client_secret=None if suppress_secret else client_secret,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async def _require_registration_access(
|
|
223
|
+
request: Request,
|
|
224
|
+
db,
|
|
225
|
+
client_id: str,
|
|
226
|
+
) -> tuple[ClientRegistrationRecord, str]:
|
|
227
|
+
try:
|
|
228
|
+
normalized_client_id = str(UUID(str(client_id)))
|
|
229
|
+
except ValueError as exc:
|
|
230
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, "invalid client_id") from exc
|
|
231
|
+
service = _rfc7592_service(request, db)
|
|
232
|
+
if not service.enabled:
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status.HTTP_404_NOT_FOUND,
|
|
235
|
+
"dynamic client registration management disabled",
|
|
236
|
+
)
|
|
237
|
+
record = await service.get(normalized_client_id)
|
|
238
|
+
if record is None or record.disabled_at is not None or not record.client_active:
|
|
239
|
+
raise HTTPException(
|
|
240
|
+
status.HTTP_404_NOT_FOUND,
|
|
241
|
+
"client registration not found",
|
|
242
|
+
)
|
|
243
|
+
bearer = _bearer_token(request)
|
|
244
|
+
if not bearer:
|
|
245
|
+
raise HTTPException(
|
|
246
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
247
|
+
{
|
|
248
|
+
"error": "invalid_token",
|
|
249
|
+
"error_description": "registration access token required",
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
presented_hash = token_hash(bearer)
|
|
253
|
+
if not record.registration_access_token_hash or not text_equal(
|
|
254
|
+
presented_hash,
|
|
255
|
+
record.registration_access_token_hash,
|
|
256
|
+
):
|
|
257
|
+
raise HTTPException(
|
|
258
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
259
|
+
{
|
|
260
|
+
"error": "invalid_token",
|
|
261
|
+
"error_description": "invalid registration access token",
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
return record, bearer
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def get_registered_client(
|
|
268
|
+
request: Request,
|
|
269
|
+
db,
|
|
270
|
+
client_id: str,
|
|
271
|
+
) -> dict[str, object]:
|
|
272
|
+
record, bearer = await _require_registration_access(request, db, client_id)
|
|
273
|
+
return _registration_response(record, registration_access_token=bearer)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def update_registered_client(
|
|
277
|
+
request: Request,
|
|
278
|
+
db,
|
|
279
|
+
client_id: str,
|
|
280
|
+
payload: DynamicClientRegistrationManagementIn,
|
|
281
|
+
) -> dict[str, object]:
|
|
282
|
+
record, bearer = await _require_registration_access(request, db, client_id)
|
|
283
|
+
current = dict(record.metadata)
|
|
284
|
+
tenant = await read_record(Tenant, db, record.tenant_id)
|
|
285
|
+
current.setdefault("tenant_slug", tenant.slug if tenant is not None else "public")
|
|
286
|
+
current.setdefault("redirect_uris", list(record.redirect_uris))
|
|
287
|
+
current.setdefault("grant_types", list(record.grant_types))
|
|
288
|
+
current.setdefault("response_types", list(record.response_types))
|
|
289
|
+
current.setdefault("token_endpoint_auth_method", "client_secret_basic")
|
|
290
|
+
incoming = payload.model_dump(exclude_none=True)
|
|
291
|
+
current.update(incoming)
|
|
292
|
+
validated = DynamicClientRegistrationIn(**current)
|
|
293
|
+
_, derived_metadata = await _validated_registration_payload(
|
|
294
|
+
db=db,
|
|
295
|
+
payload=validated,
|
|
296
|
+
deployment=_deployment(request),
|
|
297
|
+
)
|
|
298
|
+
metadata = _merge_registration_metadata(
|
|
299
|
+
validated,
|
|
300
|
+
derived_metadata=derived_metadata,
|
|
301
|
+
)
|
|
302
|
+
changed = await _rfc7592_service(request, db).update(
|
|
303
|
+
ClientRegistrationUpdateRequest(
|
|
304
|
+
client_id=record.client_id,
|
|
305
|
+
redirect_uris=tuple(validated.redirect_uris),
|
|
306
|
+
grant_types=tuple(validated.grant_types),
|
|
307
|
+
response_types=tuple(validated.response_types),
|
|
308
|
+
metadata=metadata,
|
|
309
|
+
contacts=tuple(str(item) for item in validated.contacts or ()),
|
|
310
|
+
software_id=validated.software_id,
|
|
311
|
+
software_version=validated.software_version,
|
|
312
|
+
updated_fields=tuple(sorted(incoming)),
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return _registration_response(changed, registration_access_token=bearer)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def delete_registered_client(
|
|
319
|
+
request: Request,
|
|
320
|
+
db,
|
|
321
|
+
client_id: str,
|
|
322
|
+
) -> dict[str, object]:
|
|
323
|
+
record, _ = await _require_registration_access(request, db, client_id)
|
|
324
|
+
disabled = await _rfc7592_service(request, db).delete(record.client_id)
|
|
325
|
+
return {"status": "deleted", "client_id": disabled.client_id}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
router = build_client_registration_router(
|
|
329
|
+
register_client=register_client,
|
|
330
|
+
get_registration=get_registered_client,
|
|
331
|
+
update_registration=update_registered_client,
|
|
332
|
+
delete_registration=delete_registered_client,
|
|
333
|
+
get_db=get_db,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def include_client_registration_endpoint(app: TigrblApp) -> None:
|
|
338
|
+
deployment = deployment_from_app(app, settings)
|
|
339
|
+
include_http_registration_endpoint(
|
|
340
|
+
app,
|
|
341
|
+
router,
|
|
342
|
+
registration_enabled=deployment.flag_enabled("enable_rfc7591"),
|
|
343
|
+
management_enabled=deployment.flag_enabled("enable_rfc7592"),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
include_rfc7591 = include_client_registration_endpoint
|
|
348
|
+
include_rfc7592 = include_client_registration_endpoint
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
__all__ = [
|
|
352
|
+
"_bearer_token",
|
|
353
|
+
"_merge_registration_metadata",
|
|
354
|
+
"_registration_response",
|
|
355
|
+
"_require_registration_access",
|
|
356
|
+
"_validated_registration_payload",
|
|
357
|
+
"_validated_token_endpoint_auth_method",
|
|
358
|
+
"delete_registered_client",
|
|
359
|
+
"get_registered_client",
|
|
360
|
+
"include_client_registration_endpoint",
|
|
361
|
+
"include_rfc7591",
|
|
362
|
+
"include_rfc7592",
|
|
363
|
+
"register_client",
|
|
364
|
+
"router",
|
|
365
|
+
"update_registered_client",
|
|
366
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Runtime composition for the RFC 8628 device-authorization carrier."""
|
|
2
|
+
|
|
3
|
+
from tigrbl import TigrblApp
|
|
4
|
+
from tba_router_oauth_device_authorization import (
|
|
5
|
+
build_device_authorization_router,
|
|
6
|
+
include_device_authorization_endpoint as include_device_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.device_authorization_runtime import (
|
|
13
|
+
device_authorization,
|
|
14
|
+
device_authorization_request,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
router = build_device_authorization_router(
|
|
19
|
+
device_authorization_request=device_authorization,
|
|
20
|
+
get_db=get_db,
|
|
21
|
+
)
|
|
22
|
+
api = router
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def include_device_authorization_endpoint(app: TigrblApp) -> None:
|
|
26
|
+
deployment = deployment_from_app(app, settings)
|
|
27
|
+
include_device_carrier(
|
|
28
|
+
app,
|
|
29
|
+
router,
|
|
30
|
+
enabled=deployment.route_enabled("/device_authorization"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
include_rfc8628 = include_device_authorization_endpoint
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"api",
|
|
39
|
+
"device_authorization",
|
|
40
|
+
"device_authorization_request",
|
|
41
|
+
"include_device_authorization_endpoint",
|
|
42
|
+
"include_rfc8628",
|
|
43
|
+
"router",
|
|
44
|
+
]
|