tba-app-backend-public 0.4.0.dev2__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.
- tba_app_backend_public-0.4.0.dev2/PKG-INFO +75 -0
- tba_app_backend_public-0.4.0.dev2/README.md +40 -0
- tba_app_backend_public-0.4.0.dev2/pyproject.toml +38 -0
- tba_app_backend_public-0.4.0.dev2/src/tba_app_backend_public/__init__.py +14 -0
- tba_app_backend_public-0.4.0.dev2/src/tba_app_backend_public/app.py +64 -0
- tba_app_backend_public-0.4.0.dev2/src/tba_app_backend_public/contract.py +110 -0
- tba_app_backend_public-0.4.0.dev2/src/tba_app_backend_public/openapi.py +350 -0
- tba_app_backend_public-0.4.0.dev2/src/tba_app_backend_public/py.typed +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-app-backend-public
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary: Public OAuth/OIDC and human-login backend application for TBA.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10,<3.15
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: tba-app-backend-core (==0.4.0.dev2)
|
|
14
|
+
Requires-Dist: tba-auth (==0.4.0.dev2)
|
|
15
|
+
Requires-Dist: tba-router-oauth-authorization (==0.4.0.dev2)
|
|
16
|
+
Requires-Dist: tba-router-oauth-authorization-server-metadata (==0.4.0.dev2)
|
|
17
|
+
Requires-Dist: tba-router-oauth-device-authorization (==0.4.0.dev2)
|
|
18
|
+
Requires-Dist: tba-router-oauth-introspection (==0.4.0.dev2)
|
|
19
|
+
Requires-Dist: tba-router-oauth-protected-resource-metadata (==0.4.0.dev2)
|
|
20
|
+
Requires-Dist: tba-router-oauth-pushed-authorization-request (==0.4.0.dev2)
|
|
21
|
+
Requires-Dist: tba-router-oauth-registration (==0.4.0.dev2)
|
|
22
|
+
Requires-Dist: tba-router-oauth-revocation (==0.4.0.dev2)
|
|
23
|
+
Requires-Dist: tba-router-oauth-token (==0.4.0.dev2)
|
|
24
|
+
Requires-Dist: tba-router-oauth-token-exchange (==0.4.0.dev2)
|
|
25
|
+
Requires-Dist: tba-router-oidc-discovery (==0.4.0.dev2)
|
|
26
|
+
Requires-Dist: tba-router-oidc-logout (==0.4.0.dev2)
|
|
27
|
+
Requires-Dist: tba-router-oidc-userinfo (==0.4.0.dev2)
|
|
28
|
+
Requires-Dist: tba-router-session-login (==0.4.0.dev2)
|
|
29
|
+
Requires-Dist: tba-router-webauthn (==0.4.0.dev2)
|
|
30
|
+
Project-URL: Homepage, https://github.com/tigrbl/tigrbl_auth
|
|
31
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl_auth
|
|
32
|
+
Project-URL: Source, https://github.com/tigrbl/tigrbl_auth/tree/master/pkgs/90-backend-apps/tba-app-backend-public
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# tba-app-backend-public
|
|
36
|
+
|
|
37
|
+
Public OAuth/OIDC and white-label login API front door.
|
|
38
|
+
|
|
39
|
+
## Surface
|
|
40
|
+
|
|
41
|
+
| Property | Value |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Product surface | `public-app` |
|
|
44
|
+
| Intended UIX | `@tba/ui-public` |
|
|
45
|
+
| Exposes | Login, authorize, token, logout, registration, discovery, JWKS, and public OAuth/OIDC helpers |
|
|
46
|
+
| Excludes | `/admin/*`, `/rpc`, diagnostics, tenant lifecycle administration |
|
|
47
|
+
|
|
48
|
+
## Entrypoint
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from tba_app_backend_public import app, build_app
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Docker dev deployment
|
|
55
|
+
|
|
56
|
+
From the repository root:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
docker compose -f docker/docker-compose.public-app.yml up -d --build
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The dev deployment publishes the public API on `http://localhost:8013`.
|
|
63
|
+
It runs the `production` profile with TLS disabled for local development so
|
|
64
|
+
registration, login, authorization, token, discovery, and JWKS routes are
|
|
65
|
+
available on the same localhost issuer.
|
|
66
|
+
|
|
67
|
+
Useful checks:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl http://localhost:8013/.well-known/openid-configuration
|
|
71
|
+
curl http://localhost:8013/.well-known/jwks.json
|
|
72
|
+
curl -i http://localhost:8013/tenant
|
|
73
|
+
curl -i http://localhost:8013/diagnostics
|
|
74
|
+
```
|
|
75
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# tba-app-backend-public
|
|
2
|
+
|
|
3
|
+
Public OAuth/OIDC and white-label login API front door.
|
|
4
|
+
|
|
5
|
+
## Surface
|
|
6
|
+
|
|
7
|
+
| Property | Value |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Product surface | `public-app` |
|
|
10
|
+
| Intended UIX | `@tba/ui-public` |
|
|
11
|
+
| Exposes | Login, authorize, token, logout, registration, discovery, JWKS, and public OAuth/OIDC helpers |
|
|
12
|
+
| Excludes | `/admin/*`, `/rpc`, diagnostics, tenant lifecycle administration |
|
|
13
|
+
|
|
14
|
+
## Entrypoint
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from tba_app_backend_public import app, build_app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Docker dev deployment
|
|
21
|
+
|
|
22
|
+
From the repository root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
docker compose -f docker/docker-compose.public-app.yml up -d --build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The dev deployment publishes the public API on `http://localhost:8013`.
|
|
29
|
+
It runs the `production` profile with TLS disabled for local development so
|
|
30
|
+
registration, login, authorization, token, discovery, and JWKS routes are
|
|
31
|
+
available on the same localhost issuer.
|
|
32
|
+
|
|
33
|
+
Useful checks:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl http://localhost:8013/.well-known/openid-configuration
|
|
37
|
+
curl http://localhost:8013/.well-known/jwks.json
|
|
38
|
+
curl -i http://localhost:8013/tenant
|
|
39
|
+
curl -i http://localhost:8013/diagnostics
|
|
40
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tba-app-backend-public"
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
|
+
description = "Public OAuth/OIDC and human-login backend application for TBA."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.15"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"tba-auth==0.4.0.dev2",
|
|
10
|
+
"tba-app-backend-core==0.4.0.dev2",
|
|
11
|
+
"tba-router-oauth-authorization==0.4.0.dev2",
|
|
12
|
+
"tba-router-oauth-authorization-server-metadata==0.4.0.dev2",
|
|
13
|
+
"tba-router-oauth-device-authorization==0.4.0.dev2",
|
|
14
|
+
"tba-router-oauth-introspection==0.4.0.dev2",
|
|
15
|
+
"tba-router-oauth-pushed-authorization-request==0.4.0.dev2",
|
|
16
|
+
"tba-router-oauth-protected-resource-metadata==0.4.0.dev2",
|
|
17
|
+
"tba-router-oauth-registration==0.4.0.dev2",
|
|
18
|
+
"tba-router-oauth-revocation==0.4.0.dev2",
|
|
19
|
+
"tba-router-oauth-token==0.4.0.dev2",
|
|
20
|
+
"tba-router-oauth-token-exchange==0.4.0.dev2",
|
|
21
|
+
"tba-router-oidc-discovery==0.4.0.dev2",
|
|
22
|
+
"tba-router-oidc-logout==0.4.0.dev2",
|
|
23
|
+
"tba-router-oidc-userinfo==0.4.0.dev2",
|
|
24
|
+
"tba-router-session-login==0.4.0.dev2",
|
|
25
|
+
"tba-router-webauthn==0.4.0.dev2",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/tigrbl/tigrbl_auth"
|
|
30
|
+
Repository = "https://github.com/tigrbl/tigrbl_auth"
|
|
31
|
+
Source = "https://github.com/tigrbl/tigrbl_auth/tree/master/pkgs/90-backend-apps/tba-app-backend-public"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["poetry-core>=1.0.0"]
|
|
35
|
+
build-backend = "poetry.core.masonry.api"
|
|
36
|
+
|
|
37
|
+
[tool.poetry]
|
|
38
|
+
packages = [{ include = "tba_app_backend_public", from = "src" }]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Public backend application package for TBA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .app import PRODUCT_SURFACE, app, build_app
|
|
6
|
+
from .contract import PUBLIC_BACKEND_APP_CONTRACT, PublicBackendAppContract
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"PRODUCT_SURFACE",
|
|
10
|
+
"PUBLIC_BACKEND_APP_CONTRACT",
|
|
11
|
+
"PublicBackendAppContract",
|
|
12
|
+
"app",
|
|
13
|
+
"build_app",
|
|
14
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""ASGI entrypoint for the public TBA backend application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from tba_app_backend_core import build_product_app
|
|
9
|
+
from tba_app_backend_public.contract import PUBLIC_BACKEND_APP_CONTRACT
|
|
10
|
+
from tba_auth.runtime import LazyASGIApplication
|
|
11
|
+
from tba_app_backend_public.openapi import patch_public_openapi
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from tigrbl import TigrblApp
|
|
15
|
+
|
|
16
|
+
PRODUCT_SURFACE = "public-app"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_settings() -> object:
|
|
20
|
+
from tba_auth.config.settings import Settings
|
|
21
|
+
|
|
22
|
+
settings = Settings()
|
|
23
|
+
env_overrides = {
|
|
24
|
+
"issuer": os.environ.get("AUTHN_ISSUER"),
|
|
25
|
+
"protected_resource_identifier": os.environ.get(
|
|
26
|
+
"TBA_AUTH_PROTECTED_RESOURCE_IDENTIFIER"
|
|
27
|
+
),
|
|
28
|
+
"deployment_profile": os.environ.get("TBA_AUTH_PROFILE"),
|
|
29
|
+
"require_tls": os.environ.get("TBA_AUTH_REQUIRE_TLS"),
|
|
30
|
+
"session_cookie_force_secure": os.environ.get(
|
|
31
|
+
"TBA_AUTH_SESSION_COOKIE_FORCE_SECURE"
|
|
32
|
+
),
|
|
33
|
+
"enable_rfc9207": os.environ.get("TBA_AUTH_ENABLE_RFC9207"),
|
|
34
|
+
}
|
|
35
|
+
for name, value in env_overrides.items():
|
|
36
|
+
if value in {None, ""}:
|
|
37
|
+
continue
|
|
38
|
+
current = getattr(settings, name, None)
|
|
39
|
+
if isinstance(current, bool):
|
|
40
|
+
value = str(value).lower() in {"1", "true", "yes", "on"}
|
|
41
|
+
setattr(settings, name, value)
|
|
42
|
+
return settings
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _package_default_profile(settings_obj: object | None = None) -> str:
|
|
46
|
+
resolved = settings_obj if settings_obj is not None else _default_settings()
|
|
47
|
+
configured = str(getattr(resolved, "deployment_profile", "baseline") or "baseline")
|
|
48
|
+
return configured if configured != "baseline" else "production"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_app(settings_obj: object | None = None) -> "TigrblApp":
|
|
52
|
+
if settings_obj is None:
|
|
53
|
+
settings_obj = _default_settings()
|
|
54
|
+
settings_obj.deployment_profile = _package_default_profile(settings_obj)
|
|
55
|
+
return patch_public_openapi(
|
|
56
|
+
build_product_app(
|
|
57
|
+
PRODUCT_SURFACE, settings_obj, contract=PUBLIC_BACKEND_APP_CONTRACT
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
app = LazyASGIApplication(build_app)
|
|
63
|
+
|
|
64
|
+
__all__ = ["PRODUCT_SURFACE", "_package_default_profile", "app", "build_app"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Public backend-app contract for TBA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class PublicBackendAppContract:
|
|
10
|
+
"""Machine-readable boundary for the public OAuth/OIDC backend app."""
|
|
11
|
+
|
|
12
|
+
product_surface: str
|
|
13
|
+
intended_uix: str
|
|
14
|
+
baseline_capabilities: tuple[str, ...]
|
|
15
|
+
production_capabilities: tuple[str, ...]
|
|
16
|
+
forbidden_route_prefixes: tuple[str, ...]
|
|
17
|
+
forbidden_exact_routes: tuple[str, ...]
|
|
18
|
+
consumed_packages: tuple[str, ...]
|
|
19
|
+
mounted_router_packages: tuple[str, ...]
|
|
20
|
+
app_local_route_groups: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PUBLIC_BACKEND_APP_CONTRACT = PublicBackendAppContract(
|
|
24
|
+
product_surface="public-app",
|
|
25
|
+
intended_uix="@tba/ui-public",
|
|
26
|
+
baseline_capabilities=(
|
|
27
|
+
"login",
|
|
28
|
+
"authorize",
|
|
29
|
+
"token",
|
|
30
|
+
"rest-only",
|
|
31
|
+
"openid-configuration",
|
|
32
|
+
"tenant-openid-configuration",
|
|
33
|
+
"oauth-authorization-server-metadata",
|
|
34
|
+
"jwks",
|
|
35
|
+
"tenant-jwks",
|
|
36
|
+
"standards-manifest",
|
|
37
|
+
"oid4vci-credential",
|
|
38
|
+
"oid4vp-verification",
|
|
39
|
+
"authzen-evaluation",
|
|
40
|
+
"gnap-transaction",
|
|
41
|
+
"set-receiver",
|
|
42
|
+
"attestation-appraisal",
|
|
43
|
+
),
|
|
44
|
+
production_capabilities=(
|
|
45
|
+
"login",
|
|
46
|
+
"authorize",
|
|
47
|
+
"token",
|
|
48
|
+
"userinfo",
|
|
49
|
+
"introspection",
|
|
50
|
+
"register",
|
|
51
|
+
"register-management",
|
|
52
|
+
"revoke",
|
|
53
|
+
"logout",
|
|
54
|
+
"openid-configuration",
|
|
55
|
+
"tenant-openid-configuration",
|
|
56
|
+
"oauth-authorization-server-metadata",
|
|
57
|
+
"oauth-protected-resource-metadata",
|
|
58
|
+
"jwks",
|
|
59
|
+
"tenant-jwks",
|
|
60
|
+
"standards-manifest",
|
|
61
|
+
"oid4vci-credential",
|
|
62
|
+
"oid4vp-verification",
|
|
63
|
+
"authzen-evaluation",
|
|
64
|
+
"gnap-transaction",
|
|
65
|
+
"set-receiver",
|
|
66
|
+
"attestation-appraisal",
|
|
67
|
+
),
|
|
68
|
+
forbidden_route_prefixes=(
|
|
69
|
+
"/admin",
|
|
70
|
+
"/cryptokey",
|
|
71
|
+
"/cryptokeyversion",
|
|
72
|
+
"/principalkeybinding",
|
|
73
|
+
"/keyenvelope",
|
|
74
|
+
"/keyattestationevidence",
|
|
75
|
+
"/tenant/",
|
|
76
|
+
"/diagnostics",
|
|
77
|
+
),
|
|
78
|
+
forbidden_exact_routes=("/tenant",),
|
|
79
|
+
consumed_packages=(
|
|
80
|
+
"tba-auth",
|
|
81
|
+
"tba-server-identity",
|
|
82
|
+
"tba-runtime-identity",
|
|
83
|
+
"tba-protocol-oauth",
|
|
84
|
+
"tba-protocol-oidc",
|
|
85
|
+
"tba-provider-identity-jose",
|
|
86
|
+
"tba-protocol-oid4vci",
|
|
87
|
+
"tba-protocol-oid4vp",
|
|
88
|
+
"tba-profile-haip",
|
|
89
|
+
),
|
|
90
|
+
mounted_router_packages=(
|
|
91
|
+
"tba-router-oauth-authorization",
|
|
92
|
+
"tba-router-oauth-authorization-server-metadata",
|
|
93
|
+
"tba-router-oauth-device-authorization",
|
|
94
|
+
"tba-router-oauth-introspection",
|
|
95
|
+
"tba-router-oauth-pushed-authorization-request",
|
|
96
|
+
"tba-router-oauth-protected-resource-metadata",
|
|
97
|
+
"tba-router-oauth-registration",
|
|
98
|
+
"tba-router-oauth-revocation",
|
|
99
|
+
"tba-router-oauth-token",
|
|
100
|
+
"tba-router-oauth-token-exchange",
|
|
101
|
+
"tba-router-oidc-discovery",
|
|
102
|
+
"tba-router-oidc-logout",
|
|
103
|
+
"tba-router-oidc-userinfo",
|
|
104
|
+
"tba-router-session-login",
|
|
105
|
+
"tba-router-webauthn",
|
|
106
|
+
),
|
|
107
|
+
app_local_route_groups=(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
__all__ = ["PUBLIC_BACKEND_APP_CONTRACT", "PublicBackendAppContract"]
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""OpenAPI affordances for the public backend app.
|
|
2
|
+
|
|
3
|
+
The shared runtime intentionally hand-parses several OAuth/OIDC endpoints so it
|
|
4
|
+
can accept exact protocol wire formats. Tigrbl cannot infer request bodies from
|
|
5
|
+
those handlers, so this package overlays the public front-door documentation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
from dataclasses import replace
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_LOGIN_BODY = {
|
|
16
|
+
"required": True,
|
|
17
|
+
"content": {
|
|
18
|
+
"application/json": {
|
|
19
|
+
"schema": {"$ref": "#/components/schemas/CredsIn"},
|
|
20
|
+
"example": {"identifier": "admin", "password": "AdminPass123!"},
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_REGISTER_BODY = {
|
|
26
|
+
"required": True,
|
|
27
|
+
"content": {
|
|
28
|
+
"application/json": {
|
|
29
|
+
"schema": {"$ref": "#/components/schemas/DynamicClientRegistrationIn"},
|
|
30
|
+
"example": {
|
|
31
|
+
"tenant_slug": "public",
|
|
32
|
+
"redirect_uris": ["http://127.0.0.1:8080/callback"],
|
|
33
|
+
"grant_types": ["authorization_code", "refresh_token", "password"],
|
|
34
|
+
"response_types": ["code"],
|
|
35
|
+
"token_endpoint_auth_method": "client_secret_post",
|
|
36
|
+
"client_name": "Local dev client",
|
|
37
|
+
"scope": "openid profile email",
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_TOKEN_BODY = {
|
|
44
|
+
"required": True,
|
|
45
|
+
"content": {
|
|
46
|
+
"application/x-www-form-urlencoded": {
|
|
47
|
+
"schema": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["grant_type", "client_id"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"grant_type": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"enum": [
|
|
54
|
+
"authorization_code",
|
|
55
|
+
"client_credentials",
|
|
56
|
+
"password",
|
|
57
|
+
"refresh_token",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
"client_id": {"type": "string"},
|
|
61
|
+
"client_secret": {"type": "string"},
|
|
62
|
+
"username": {"type": "string"},
|
|
63
|
+
"password": {"type": "string", "format": "password"},
|
|
64
|
+
"scope": {"type": "string"},
|
|
65
|
+
"code": {"type": "string"},
|
|
66
|
+
"redirect_uri": {"type": "string", "format": "uri"},
|
|
67
|
+
"code_verifier": {"type": "string"},
|
|
68
|
+
"refresh_token": {"type": "string"},
|
|
69
|
+
"resource": {"type": "string"},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"examples": {
|
|
73
|
+
"password": {
|
|
74
|
+
"summary": "Password grant after dynamic client registration",
|
|
75
|
+
"value": {
|
|
76
|
+
"grant_type": "password",
|
|
77
|
+
"client_id": "<registered-client-id>",
|
|
78
|
+
"client_secret": "<registered-client-secret>",
|
|
79
|
+
"username": "admin",
|
|
80
|
+
"password": "AdminPass123!",
|
|
81
|
+
"scope": "openid profile email",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
"authorization_code": {
|
|
85
|
+
"summary": "Authorization code exchange",
|
|
86
|
+
"value": {
|
|
87
|
+
"grant_type": "authorization_code",
|
|
88
|
+
"client_id": "<registered-client-id>",
|
|
89
|
+
"client_secret": "<registered-client-secret>",
|
|
90
|
+
"code": "<authorization-code>",
|
|
91
|
+
"redirect_uri": "http://127.0.0.1:8080/callback",
|
|
92
|
+
"code_verifier": "<pkce-code-verifier>",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_LOGOUT_BODY = {
|
|
101
|
+
"required": False,
|
|
102
|
+
"content": {
|
|
103
|
+
"application/json": {
|
|
104
|
+
"schema": {"$ref": "#/components/schemas/LogoutIn"},
|
|
105
|
+
"example": {"client_id": "<registered-client-id>", "state": "dev-state"},
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_TOKEN_REFERENCE_BODY = {
|
|
111
|
+
"required": True,
|
|
112
|
+
"content": {
|
|
113
|
+
"application/x-www-form-urlencoded": {
|
|
114
|
+
"schema": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"required": ["token"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"token": {"type": "string"},
|
|
119
|
+
"token_type_hint": {"type": "string"},
|
|
120
|
+
"client_id": {"type": "string"},
|
|
121
|
+
"client_secret": {"type": "string"},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_AUTHORIZE_PARAMS = [
|
|
129
|
+
{
|
|
130
|
+
"name": "response_type",
|
|
131
|
+
"in": "query",
|
|
132
|
+
"required": True,
|
|
133
|
+
"schema": {"type": "string", "default": "code"},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "client_id",
|
|
137
|
+
"in": "query",
|
|
138
|
+
"required": True,
|
|
139
|
+
"schema": {"type": "string"},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "redirect_uri",
|
|
143
|
+
"in": "query",
|
|
144
|
+
"required": True,
|
|
145
|
+
"schema": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"format": "uri",
|
|
148
|
+
"default": "http://127.0.0.1:8080/callback",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "scope",
|
|
153
|
+
"in": "query",
|
|
154
|
+
"required": False,
|
|
155
|
+
"schema": {"type": "string", "default": "openid profile email"},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "state",
|
|
159
|
+
"in": "query",
|
|
160
|
+
"required": False,
|
|
161
|
+
"schema": {"type": "string", "default": "dev-state"},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "nonce",
|
|
165
|
+
"in": "query",
|
|
166
|
+
"required": False,
|
|
167
|
+
"schema": {"type": "string", "default": "dev-nonce"},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "code_challenge",
|
|
171
|
+
"in": "query",
|
|
172
|
+
"required": True,
|
|
173
|
+
"schema": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"default": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "code_challenge_method",
|
|
180
|
+
"in": "query",
|
|
181
|
+
"required": True,
|
|
182
|
+
"schema": {"type": "string", "enum": ["S256"], "default": "S256"},
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _ensure_schema(openapi: dict[str, Any], name: str, schema: dict[str, Any]) -> None:
|
|
188
|
+
components = openapi.setdefault("components", {})
|
|
189
|
+
schemas = components.setdefault("schemas", {})
|
|
190
|
+
schemas.setdefault(name, schema)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _operation(openapi: dict[str, Any], path: str, method: str) -> dict[str, Any]:
|
|
194
|
+
return openapi.setdefault("paths", {}).setdefault(path, {}).setdefault(method, {})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _replace_params(operation: dict[str, Any], params: list[dict[str, Any]]) -> None:
|
|
198
|
+
existing = operation.get("parameters") or []
|
|
199
|
+
preserved = [
|
|
200
|
+
param
|
|
201
|
+
for param in existing
|
|
202
|
+
if (param.get("in"), param.get("name"))
|
|
203
|
+
not in {(item["in"], item["name"]) for item in params}
|
|
204
|
+
]
|
|
205
|
+
operation["parameters"] = preserved + deepcopy(params)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def patch_public_openapi(app: object) -> object:
|
|
209
|
+
"""Patch generated OpenAPI for public protocol endpoints."""
|
|
210
|
+
target = getattr(app, "app", app)
|
|
211
|
+
original_openapi: Callable[[], dict[str, Any]] = target.openapi
|
|
212
|
+
|
|
213
|
+
def public_openapi() -> dict[str, Any]:
|
|
214
|
+
cached = getattr(target, "openapi_schema", None)
|
|
215
|
+
if cached:
|
|
216
|
+
return cached
|
|
217
|
+
|
|
218
|
+
openapi = original_openapi()
|
|
219
|
+
_ensure_schema(
|
|
220
|
+
openapi,
|
|
221
|
+
"CredsIn",
|
|
222
|
+
{
|
|
223
|
+
"type": "object",
|
|
224
|
+
"required": ["identifier", "password"],
|
|
225
|
+
"properties": {
|
|
226
|
+
"identifier": {"type": "string", "minLength": 3},
|
|
227
|
+
"password": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"format": "password",
|
|
230
|
+
"minLength": 8,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
_ensure_schema(
|
|
236
|
+
openapi,
|
|
237
|
+
"DynamicClientRegistrationIn",
|
|
238
|
+
{
|
|
239
|
+
"type": "object",
|
|
240
|
+
"required": ["redirect_uris"],
|
|
241
|
+
"properties": {
|
|
242
|
+
"tenant_slug": {"type": "string", "default": "public"},
|
|
243
|
+
"redirect_uris": {
|
|
244
|
+
"type": "array",
|
|
245
|
+
"items": {"type": "string", "format": "uri"},
|
|
246
|
+
},
|
|
247
|
+
"grant_types": {
|
|
248
|
+
"type": "array",
|
|
249
|
+
"items": {"type": "string"},
|
|
250
|
+
"default": ["authorization_code"],
|
|
251
|
+
},
|
|
252
|
+
"response_types": {
|
|
253
|
+
"type": "array",
|
|
254
|
+
"items": {"type": "string"},
|
|
255
|
+
"default": ["code"],
|
|
256
|
+
},
|
|
257
|
+
"token_endpoint_auth_method": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"default": "client_secret_basic",
|
|
260
|
+
},
|
|
261
|
+
"scope": {"type": "string"},
|
|
262
|
+
"client_name": {"type": "string"},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
_ensure_schema(
|
|
267
|
+
openapi,
|
|
268
|
+
"LogoutIn",
|
|
269
|
+
{
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"id_token_hint": {"type": "string"},
|
|
273
|
+
"post_logout_redirect_uri": {"type": "string", "format": "uri"},
|
|
274
|
+
"state": {"type": "string"},
|
|
275
|
+
"sid": {"type": "string"},
|
|
276
|
+
"client_id": {"type": "string"},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
_operation(openapi, "/login", "post")["requestBody"] = deepcopy(_LOGIN_BODY)
|
|
282
|
+
_operation(openapi, "/register", "post")["requestBody"] = deepcopy(
|
|
283
|
+
_REGISTER_BODY
|
|
284
|
+
)
|
|
285
|
+
_operation(openapi, "/token", "post")["requestBody"] = deepcopy(_TOKEN_BODY)
|
|
286
|
+
_operation(openapi, "/logout", "post")["requestBody"] = deepcopy(_LOGOUT_BODY)
|
|
287
|
+
_operation(openapi, "/revoke", "post")["requestBody"] = deepcopy(
|
|
288
|
+
_TOKEN_REFERENCE_BODY
|
|
289
|
+
)
|
|
290
|
+
if "/introspect" in openapi.get("paths", {}):
|
|
291
|
+
_operation(openapi, "/introspect", "post")["requestBody"] = deepcopy(
|
|
292
|
+
_TOKEN_REFERENCE_BODY
|
|
293
|
+
)
|
|
294
|
+
if "/authorize" in openapi.get("paths", {}):
|
|
295
|
+
_replace_params(_operation(openapi, "/authorize", "get"), _AUTHORIZE_PARAMS)
|
|
296
|
+
|
|
297
|
+
setattr(target, "openapi_schema", openapi)
|
|
298
|
+
return openapi
|
|
299
|
+
|
|
300
|
+
target.openapi = public_openapi
|
|
301
|
+
if target is not app:
|
|
302
|
+
app.openapi = public_openapi
|
|
303
|
+
|
|
304
|
+
routes = getattr(target, "_routes", None)
|
|
305
|
+
if isinstance(routes, list):
|
|
306
|
+
from tigrbl_concrete._concrete._response import Response
|
|
307
|
+
|
|
308
|
+
def _openapi_handler(request: Any) -> Response:
|
|
309
|
+
return Response.json(public_openapi())
|
|
310
|
+
|
|
311
|
+
patched_routes = []
|
|
312
|
+
for route in routes:
|
|
313
|
+
path = str(
|
|
314
|
+
getattr(route, "path_template", None) or getattr(route, "path", "")
|
|
315
|
+
)
|
|
316
|
+
methods = set(getattr(route, "methods", ()) or ())
|
|
317
|
+
if path == "/openapi.json" and "GET" in methods:
|
|
318
|
+
patched_routes.append(replace(route, handler=_openapi_handler))
|
|
319
|
+
else:
|
|
320
|
+
patched_routes.append(route)
|
|
321
|
+
routes[:] = patched_routes
|
|
322
|
+
return PublicOpenAPIGate(app)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
__all__ = ["patch_public_openapi"]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class PublicOpenAPIGate:
|
|
329
|
+
"""ASGI wrapper that serves the patched public OpenAPI document."""
|
|
330
|
+
|
|
331
|
+
def __init__(self, app: object) -> None:
|
|
332
|
+
self.app = app
|
|
333
|
+
|
|
334
|
+
def __getattr__(self, name: str) -> Any:
|
|
335
|
+
return getattr(self.app, name)
|
|
336
|
+
|
|
337
|
+
def openapi(self) -> dict[str, Any]:
|
|
338
|
+
return self.app.openapi()
|
|
339
|
+
|
|
340
|
+
async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
341
|
+
if (
|
|
342
|
+
scope.get("type") == "http"
|
|
343
|
+
and str(scope.get("method", "")).upper() == "GET"
|
|
344
|
+
and scope.get("path") == "/openapi.json"
|
|
345
|
+
):
|
|
346
|
+
from tigrbl_concrete._concrete._response import Response
|
|
347
|
+
|
|
348
|
+
await Response.json(self.openapi())(scope, receive, send)
|
|
349
|
+
return
|
|
350
|
+
await self.app(scope, receive, send)
|
|
File without changes
|