fayda-mcp 0.1.0__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.
- fayda_mcp-0.1.0/.gitignore +11 -0
- fayda_mcp-0.1.0/LICENSE +17 -0
- fayda_mcp-0.1.0/PKG-INFO +134 -0
- fayda_mcp-0.1.0/README.md +94 -0
- fayda_mcp-0.1.0/docs/architecture.md +22 -0
- fayda_mcp-0.1.0/docs/callback_wiring.md +65 -0
- fayda_mcp-0.1.0/docs/cleanup_and_lifecycle.md +56 -0
- fayda_mcp-0.1.0/docs/configuration.md +67 -0
- fayda_mcp-0.1.0/docs/credentials.md +70 -0
- fayda_mcp-0.1.0/examples/fastapi_server.py +69 -0
- fayda_mcp-0.1.0/examples/remote_http_server.py +104 -0
- fayda_mcp-0.1.0/examples/sandbox_flow.py +152 -0
- fayda_mcp-0.1.0/examples/stdio_server.py +70 -0
- fayda_mcp-0.1.0/guide.pdf +0 -0
- fayda_mcp-0.1.0/pyproject.toml +82 -0
- fayda_mcp-0.1.0/pyrightconfig.json +6 -0
- fayda_mcp-0.1.0/src/fayda_mcp/__init__.py +62 -0
- fayda_mcp-0.1.0/src/fayda_mcp/claims.py +85 -0
- fayda_mcp-0.1.0/src/fayda_mcp/config.py +147 -0
- fayda_mcp-0.1.0/src/fayda_mcp/context.py +95 -0
- fayda_mcp-0.1.0/src/fayda_mcp/exceptions.py +90 -0
- fayda_mcp-0.1.0/src/fayda_mcp/integrations/__init__.py +2 -0
- fayda_mcp-0.1.0/src/fayda_mcp/integrations/fastapi.py +223 -0
- fayda_mcp-0.1.0/src/fayda_mcp/mcp/__init__.py +4 -0
- fayda_mcp-0.1.0/src/fayda_mcp/mcp/factory.py +40 -0
- fayda_mcp-0.1.0/src/fayda_mcp/mcp/tools.py +118 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/__init__.py +4 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/assertions.py +38 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/authorization.py +59 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/client.py +90 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/jwks.py +75 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/mapping.py +109 -0
- fayda_mcp-0.1.0/src/fayda_mcp/oidc/tokens.py +126 -0
- fayda_mcp-0.1.0/src/fayda_mcp/policy.py +65 -0
- fayda_mcp-0.1.0/src/fayda_mcp/schemas.py +75 -0
- fayda_mcp-0.1.0/src/fayda_mcp/secrets/__init__.py +6 -0
- fayda_mcp-0.1.0/src/fayda_mcp/secrets/file.py +18 -0
- fayda_mcp-0.1.0/src/fayda_mcp/secrets/keys.py +85 -0
- fayda_mcp-0.1.0/src/fayda_mcp/secrets/protocols.py +12 -0
- fayda_mcp-0.1.0/src/fayda_mcp/service.py +365 -0
- fayda_mcp-0.1.0/src/fayda_mcp/storage/__init__.py +20 -0
- fayda_mcp-0.1.0/src/fayda_mcp/storage/memory.py +187 -0
- fayda_mcp-0.1.0/src/fayda_mcp/storage/postgres.py +41 -0
- fayda_mcp-0.1.0/src/fayda_mcp/storage/protocols.py +84 -0
- fayda_mcp-0.1.0/src/fayda_mcp/storage/redis.py +50 -0
- fayda_mcp-0.1.0/tests/integration/__init__.py +1 -0
- fayda_mcp-0.1.0/tests/integration/test_sandbox_flow.py +104 -0
- fayda_mcp-0.1.0/tests/unit/test_claims_results.py +261 -0
- fayda_mcp-0.1.0/tests/unit/test_code_exchange_jwt.py +215 -0
- fayda_mcp-0.1.0/tests/unit/test_fastapi_integration.py +303 -0
- fayda_mcp-0.1.0/tests/unit/test_foundation.py +122 -0
- fayda_mcp-0.1.0/tests/unit/test_mcp_factory.py +317 -0
- fayda_mcp-0.1.0/tests/unit/test_oidc_authorization.py +190 -0
- fayda_mcp-0.1.0/tests/unit/test_optional_extras.py +46 -0
- fayda_mcp-0.1.0/tests/unit/test_release_packaging.py +103 -0
- fayda_mcp-0.1.0/tests/unit/test_storage_contracts.py +249 -0
fayda_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Fayda MCP Contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
fayda_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fayda-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fayda eSignet identity verification tools for Model Context Protocol (MCP)
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: cryptography>=43.0.0
|
|
9
|
+
Requires-Dist: fastmcp>=4.0.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.5.0
|
|
12
|
+
Requires-Dist: pydantic>=2.9.0
|
|
13
|
+
Requires-Dist: pyjwt>=2.9.0
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'all'
|
|
16
|
+
Requires-Dist: greenlet>=3.0.0; extra == 'all'
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'all'
|
|
18
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
19
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'all'
|
|
20
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'all'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: greenlet>=3.0.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: redis>=5.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'fastapi'
|
|
32
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'fastapi'
|
|
33
|
+
Provides-Extra: postgres
|
|
34
|
+
Requires-Dist: greenlet>=3.0.0; extra == 'postgres'
|
|
35
|
+
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'postgres'
|
|
36
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == 'postgres'
|
|
37
|
+
Provides-Extra: redis
|
|
38
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Fayda MCP Python Library (`fayda-mcp`)
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/fayda-mcp/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
|
|
46
|
+
An importable, headless Python library providing Model Context Protocol (MCP) tools and verification services for Ethiopian National ID (Fayda eSignet).
|
|
47
|
+
|
|
48
|
+
## Key Capabilities
|
|
49
|
+
|
|
50
|
+
- **Explicit FastMCP Tools**: `start_verification`, `get_verification_status`, `get_verification_result`, `cancel_verification`.
|
|
51
|
+
- **Zero Raw Demographic / Biometric Leaks**: Outputs minimal boolean predicates (`identity_verified`, `age_over_18`), strictly forbidding citizen biometrics, OTPs, or demographic dumps from entering LLM contexts.
|
|
52
|
+
- **Cryptographic Security**: OIDC PKCE S256, RFC 7523 `private_key_jwt` client assertions, and strict JWT signature/nonce validation.
|
|
53
|
+
- **Pluggable Architecture**: Zero mandatory database or web framework runtime dependencies in core; optional extras for FastAPI, Redis, and Neon/PostgreSQL.
|
|
54
|
+
- **Multi-Tenant Caller Isolation**: Built-in tenant and principal boundaries ensuring Caller A cannot access Caller B verification records.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Core library (headless FastMCP + Fayda OIDC verification engine)
|
|
62
|
+
pip install fayda-mcp
|
|
63
|
+
|
|
64
|
+
# With optional FastAPI integration
|
|
65
|
+
pip install "fayda-mcp[fastapi]"
|
|
66
|
+
|
|
67
|
+
# Full bundle with Redis and Neon/Postgres adapters
|
|
68
|
+
pip install "fayda-mcp[fastapi,redis,postgres]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start (Sandbox)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fayda_mcp import FaydaConfig, FaydaVerificationService
|
|
77
|
+
from fayda_mcp.storage.memory import MemorySessionStore, MemoryResultRepository
|
|
78
|
+
from fayda_mcp.mcp.factory import create_mcp_server
|
|
79
|
+
|
|
80
|
+
# 1. Configure with official Fayda sandbox preset
|
|
81
|
+
config = FaydaConfig.sandbox(
|
|
82
|
+
client_id="your_registered_client_id",
|
|
83
|
+
redirect_uri="http://localhost:8000/auth/fayda/callback",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 2. Initialize verification service
|
|
87
|
+
service = FaydaVerificationService(
|
|
88
|
+
config=config,
|
|
89
|
+
sessions=MemorySessionStore(),
|
|
90
|
+
results=MemoryResultRepository(),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 3. Create FastMCP server
|
|
94
|
+
server = create_mcp_server(service=service)
|
|
95
|
+
|
|
96
|
+
# 4. Run over stdio (e.g. for Claude Desktop or Cursor)
|
|
97
|
+
server.run(transport="stdio")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Running the Complete Sandbox Flow
|
|
103
|
+
|
|
104
|
+
Run the included standalone sandbox walkthrough:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python examples/sandbox_flow.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This runs the entire end-to-end lifecycle (starts verification -> builds authorization link -> simulates citizen callback -> queries status -> retrieves privacy-preserving boolean result -> verifies caller isolation) without editing library internals.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
- **Local Standard I/O (stdio)**: [examples/stdio_server.py](examples/stdio_server.py) (Desktop LLM clients, Claude Desktop, Cursor)
|
|
117
|
+
- **Remote HTTP Server**: [examples/remote_http_server.py](examples/remote_http_server.py) (Hosted streamable-HTTP / SSE MCP endpoints with FastAPI)
|
|
118
|
+
- **Full Sandbox Walkthrough**: [examples/sandbox_flow.py](examples/sandbox_flow.py)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Documentation
|
|
123
|
+
|
|
124
|
+
- [Architecture & Responsibilities](docs/architecture.md)
|
|
125
|
+
- [Configuration Guide & Environment Variables](docs/configuration.md)
|
|
126
|
+
- [Callback Wiring & Session Binding](docs/callback_wiring.md)
|
|
127
|
+
- [Credential Ownership & Cryptography](docs/credentials.md)
|
|
128
|
+
- [Cleanup & Lifecycle Management](docs/cleanup_and_lifecycle.md)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Fayda MCP Python Library (`fayda-mcp`)
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fayda-mcp/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
An importable, headless Python library providing Model Context Protocol (MCP) tools and verification services for Ethiopian National ID (Fayda eSignet).
|
|
7
|
+
|
|
8
|
+
## Key Capabilities
|
|
9
|
+
|
|
10
|
+
- **Explicit FastMCP Tools**: `start_verification`, `get_verification_status`, `get_verification_result`, `cancel_verification`.
|
|
11
|
+
- **Zero Raw Demographic / Biometric Leaks**: Outputs minimal boolean predicates (`identity_verified`, `age_over_18`), strictly forbidding citizen biometrics, OTPs, or demographic dumps from entering LLM contexts.
|
|
12
|
+
- **Cryptographic Security**: OIDC PKCE S256, RFC 7523 `private_key_jwt` client assertions, and strict JWT signature/nonce validation.
|
|
13
|
+
- **Pluggable Architecture**: Zero mandatory database or web framework runtime dependencies in core; optional extras for FastAPI, Redis, and Neon/PostgreSQL.
|
|
14
|
+
- **Multi-Tenant Caller Isolation**: Built-in tenant and principal boundaries ensuring Caller A cannot access Caller B verification records.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Core library (headless FastMCP + Fayda OIDC verification engine)
|
|
22
|
+
pip install fayda-mcp
|
|
23
|
+
|
|
24
|
+
# With optional FastAPI integration
|
|
25
|
+
pip install "fayda-mcp[fastapi]"
|
|
26
|
+
|
|
27
|
+
# Full bundle with Redis and Neon/Postgres adapters
|
|
28
|
+
pip install "fayda-mcp[fastapi,redis,postgres]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start (Sandbox)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from fayda_mcp import FaydaConfig, FaydaVerificationService
|
|
37
|
+
from fayda_mcp.storage.memory import MemorySessionStore, MemoryResultRepository
|
|
38
|
+
from fayda_mcp.mcp.factory import create_mcp_server
|
|
39
|
+
|
|
40
|
+
# 1. Configure with official Fayda sandbox preset
|
|
41
|
+
config = FaydaConfig.sandbox(
|
|
42
|
+
client_id="your_registered_client_id",
|
|
43
|
+
redirect_uri="http://localhost:8000/auth/fayda/callback",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# 2. Initialize verification service
|
|
47
|
+
service = FaydaVerificationService(
|
|
48
|
+
config=config,
|
|
49
|
+
sessions=MemorySessionStore(),
|
|
50
|
+
results=MemoryResultRepository(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 3. Create FastMCP server
|
|
54
|
+
server = create_mcp_server(service=service)
|
|
55
|
+
|
|
56
|
+
# 4. Run over stdio (e.g. for Claude Desktop or Cursor)
|
|
57
|
+
server.run(transport="stdio")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Running the Complete Sandbox Flow
|
|
63
|
+
|
|
64
|
+
Run the included standalone sandbox walkthrough:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python examples/sandbox_flow.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This runs the entire end-to-end lifecycle (starts verification -> builds authorization link -> simulates citizen callback -> queries status -> retrieves privacy-preserving boolean result -> verifies caller isolation) without editing library internals.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
- **Local Standard I/O (stdio)**: [examples/stdio_server.py](examples/stdio_server.py) (Desktop LLM clients, Claude Desktop, Cursor)
|
|
77
|
+
- **Remote HTTP Server**: [examples/remote_http_server.py](examples/remote_http_server.py) (Hosted streamable-HTTP / SSE MCP endpoints with FastAPI)
|
|
78
|
+
- **Full Sandbox Walkthrough**: [examples/sandbox_flow.py](examples/sandbox_flow.py)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- [Architecture & Responsibilities](docs/architecture.md)
|
|
85
|
+
- [Configuration Guide & Environment Variables](docs/configuration.md)
|
|
86
|
+
- [Callback Wiring & Session Binding](docs/callback_wiring.md)
|
|
87
|
+
- [Credential Ownership & Cryptography](docs/credentials.md)
|
|
88
|
+
- [Cleanup & Lifecycle Management](docs/cleanup_and_lifecycle.md)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Fayda MCP Architecture
|
|
2
|
+
|
|
3
|
+
## Delivery Model
|
|
4
|
+
|
|
5
|
+
`fayda-mcp` is an importable, headless Python library providing:
|
|
6
|
+
1. Typed FastMCP tools and schemas (`start_verification`, `get_verification_status`, `get_verification_result`, `cancel_verification`).
|
|
7
|
+
2. OIDC PKCE generation and private_key_jwt client assertion support.
|
|
8
|
+
3. Cryptographic JWT and JWKS validation for Fayda eSignet.
|
|
9
|
+
4. Pluggable storage protocols for sessions, results, and audit trails.
|
|
10
|
+
|
|
11
|
+
## Responsibilities
|
|
12
|
+
|
|
13
|
+
| Component | Owner |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Typed MCP tools & schemas | `fayda-mcp` library |
|
|
16
|
+
| OIDC / PKCE & client assertions | `fayda-mcp` library |
|
|
17
|
+
| JWT validation & claim evaluation | `fayda-mcp` library |
|
|
18
|
+
| Storage protocols & memory adapter | `fayda-mcp` library |
|
|
19
|
+
| MCP hosting & OAuth transport security | Integrating application |
|
|
20
|
+
| Fayda RP registration & private key | Integrating application |
|
|
21
|
+
| Browser redirect & callback endpoints | Integrating application |
|
|
22
|
+
| Redis / Neon infrastructure | Integrating application |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Callback Wiring & Session Binding
|
|
2
|
+
|
|
3
|
+
In the Fayda eSignet OpenID Connect flow, citizens authorize in their web browser and are redirected back to the host application's registered `redirect_uri`.
|
|
4
|
+
|
|
5
|
+
## Why Host Session Binding is Required
|
|
6
|
+
|
|
7
|
+
Without session binding, an attacker could initiate an identity verification flow and trick a victim into completing it, or hijack another user's verification state (CSRF / session injection).
|
|
8
|
+
|
|
9
|
+
`fayda-mcp` requires a **host session-binding hook** to ensure:
|
|
10
|
+
1. When verification is started, the caller context records a secure browser binding identifier (e.g. `session_id`, cookie HMAC, or authenticated user reference).
|
|
11
|
+
2. When the user returns from Fayda eSignet to `/callback`, the callback router invokes the host hook to extract the browser identifier from the HTTP request.
|
|
12
|
+
3. If the returned session does not match the bound identifier, the callback is rejected with `InvalidStateError`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## FastAPI Integration
|
|
17
|
+
|
|
18
|
+
Use `create_callback_router` to mount the callback handler on your FastAPI app:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from fastapi import FastAPI, Request
|
|
22
|
+
from fastapi.responses import RedirectResponse
|
|
23
|
+
from fayda_mcp.integrations.fastapi import create_callback_router
|
|
24
|
+
|
|
25
|
+
app = FastAPI()
|
|
26
|
+
|
|
27
|
+
# 1. Define host session-binding hook
|
|
28
|
+
def resolve_browser_session(request: Request) -> str | None:
|
|
29
|
+
# Extracts the host session cookie from incoming citizen request
|
|
30
|
+
return request.cookies.get("app_session_id")
|
|
31
|
+
|
|
32
|
+
# 2. Define on_success redirect hook
|
|
33
|
+
async def on_verification_complete(request: Request, result) -> RedirectResponse:
|
|
34
|
+
# Redirect citizen to host onboarding completion screen
|
|
35
|
+
return RedirectResponse(
|
|
36
|
+
url=f"/onboarding/complete?request_id={result.request_id}&status={result.status}",
|
|
37
|
+
status_code=303,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 3. Mount callback router
|
|
41
|
+
router = create_callback_router(
|
|
42
|
+
service=service,
|
|
43
|
+
session_binding_hook=resolve_browser_session,
|
|
44
|
+
on_success=on_verification_complete,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
app.include_router(router)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Registered Redirect URI Alignment
|
|
53
|
+
|
|
54
|
+
By default, `create_callback_router` inspects `service.config.redirect_uri` to determine the exact path to mount:
|
|
55
|
+
- If `redirect_uri="https://my-app.example/auth/fayda/callback"`, the router mounts on `/auth/fayda/callback`.
|
|
56
|
+
- If `redirect_uri="http://localhost:8000/callback"`, the router mounts on `/callback`.
|
|
57
|
+
|
|
58
|
+
You can also pass explicit `prefix` and `callback_path` arguments if you mount behind reverse proxies or API gateways.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Error Handling
|
|
63
|
+
|
|
64
|
+
When Fayda returns an error (such as a citizen canceling consent), Fayda redirects with query parameters `?error=access_denied&error_description=...`.
|
|
65
|
+
The router automatically parses provider errors and passes them to `on_error` if configured, or returns an HTTP 400 Bad Request JSON response.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Cleanup & Lifecycle Management
|
|
2
|
+
|
|
3
|
+
Managing background connections, cached tokens, and short-lived session states properly ensures high security and prevents memory or connection leaks.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Service Lifecycle Management
|
|
8
|
+
|
|
9
|
+
`FaydaVerificationService` manages an underlying HTTP client pool (`httpx.AsyncClient`) and JWKS cache. Always manage its lifecycle using one of the following patterns:
|
|
10
|
+
|
|
11
|
+
### Async Context Manager (Recommended for scripts & tests)
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
async with FaydaVerificationService(config, sessions, results) as service:
|
|
15
|
+
# Service connections are active
|
|
16
|
+
...
|
|
17
|
+
# Cleanly closed upon block exit (service.aclose() executed)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Combined FastAPI/FastMCP Lifespan (Recommended for servers)
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from fayda_mcp.integrations.fastapi import create_combined_lifespan
|
|
24
|
+
|
|
25
|
+
lifespan = create_combined_lifespan(
|
|
26
|
+
service=service,
|
|
27
|
+
mcp_server=mcp_server,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = FastAPI(lifespan=lifespan)
|
|
31
|
+
```
|
|
32
|
+
Using `create_combined_lifespan` guarantees:
|
|
33
|
+
1. `service` HTTP pools and caches start first.
|
|
34
|
+
2. `mcp_server` and host application start up cleanly.
|
|
35
|
+
3. Upon SIGTERM or shutdown, all components are gracefully closed in reverse order.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Session and Result Expiry (TTLs)
|
|
40
|
+
|
|
41
|
+
To prevent unbounded storage growth and eliminate stale cryptographic state, `fayda-mcp` enforces bounded TTLs on all stored data:
|
|
42
|
+
|
|
43
|
+
| Setting | Default | Purpose |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `session_ttl_seconds` | 600s (10 min) | Lifetime of pending authorization state, nonce, and PKCE verifier |
|
|
46
|
+
| `result_ttl_seconds` | 900s (15 min) | Lifetime of completed minimal verification result |
|
|
47
|
+
| `jwks_cache_ttl_seconds` | 300s (5 min) | Cached Fayda public keys |
|
|
48
|
+
|
|
49
|
+
### Storage Adapter Behaviors
|
|
50
|
+
|
|
51
|
+
- **In-Memory Adapter (`MemorySessionStore`, `MemoryResultRepository`)**:
|
|
52
|
+
Automatically prunes expired entries on access and lookup.
|
|
53
|
+
- **Redis Adapter**:
|
|
54
|
+
Applies native Redis `EXPIRE` commands matching the configured TTL.
|
|
55
|
+
- **Postgres / Neon Adapter**:
|
|
56
|
+
Stores `expires_at` ISO timestamps. An index on `expires_at` enables efficient scheduled cleanup jobs (e.g. `DELETE FROM fayda_sessions WHERE expires_at < NOW()`).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Configuration Guide
|
|
2
|
+
|
|
3
|
+
The `fayda-mcp` library is configured via `FaydaConfig`. It separates Relying Party (RP) credentials from runtime policies and infrastructure adapters.
|
|
4
|
+
|
|
5
|
+
## Environment Variables
|
|
6
|
+
|
|
7
|
+
Developers can configure their environment using a `.env` file or environment variables:
|
|
8
|
+
|
|
9
|
+
| Variable | Description | Example (Sandbox) |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `FAYDA_CLIENT_ID` | Registered Fayda relying party client ID | `acme_service_client_1` |
|
|
12
|
+
| `FAYDA_REDIRECT_URI` | Registered callback URL on the host application | `https://my-app.example/auth/fayda/callback` |
|
|
13
|
+
| `FAYDA_ISSUER_URL` | Fayda OIDC issuer base URL | `https://esignet.sandbox.fayda.et` |
|
|
14
|
+
| `FAYDA_AUTHORIZATION_URL` | Fayda eSignet user authorization endpoint | `https://esignet.sandbox.fayda.et/authorize` |
|
|
15
|
+
| `FAYDA_TOKEN_URL` | Fayda token exchange endpoint | `https://esignet.sandbox.fayda.et/v1/esignet/oauth/v2/token` |
|
|
16
|
+
| `FAYDA_USERINFO_URL` | Fayda UserInfo claims endpoint | `https://esignet.sandbox.fayda.et/v1/esignet/oidc/userinfo` |
|
|
17
|
+
| `FAYDA_JWKS_URL` | Fayda public key JWKS endpoint | `https://esignet.sandbox.fayda.et/v1/esignet/oauth/v2/jwks` |
|
|
18
|
+
| `FAYDA_SIGNING_KEY_PATH` | Path to host private RSA key for `private_key_jwt` | `/run/secrets/fayda_private_key.json` |
|
|
19
|
+
| `FAYDA_SESSION_TTL_SECONDS` | TTL for pending OIDC state & PKCE verifiers (default: 600s) | `600` |
|
|
20
|
+
| `FAYDA_RESULT_TTL_SECONDS` | TTL for completed verification results (default: 900s) | `900` |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Loading Configuration
|
|
25
|
+
|
|
26
|
+
### 1. From Environment (`FaydaConfig.from_env()`)
|
|
27
|
+
|
|
28
|
+
Loads standard environment variables with prefix `FAYDA_`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from fayda_mcp import FaydaConfig
|
|
32
|
+
|
|
33
|
+
config = FaydaConfig.from_env()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Sandbox Preset (`FaydaConfig.sandbox()`)
|
|
37
|
+
|
|
38
|
+
Quickly pre-populates official Ethiopian Fayda eSignet sandbox endpoints:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from fayda_mcp import FaydaConfig
|
|
42
|
+
|
|
43
|
+
config = FaydaConfig.sandbox(
|
|
44
|
+
client_id="your_sandbox_client_id",
|
|
45
|
+
redirect_uri="http://localhost:8000/auth/fayda/callback",
|
|
46
|
+
signing_key_path="secrets/fayda_private_key.pem",
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Explicit Production Configuration
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fayda_mcp import FaydaConfig
|
|
54
|
+
|
|
55
|
+
config = FaydaConfig(
|
|
56
|
+
client_id="prod_client_id",
|
|
57
|
+
redirect_uri="https://app.example.com/auth/fayda/callback",
|
|
58
|
+
issuer="https://esignet.ida.fayda.et",
|
|
59
|
+
authorization_endpoint="https://esignet.ida.fayda.et/authorize",
|
|
60
|
+
token_endpoint="https://esignet.ida.fayda.et/v1/esignet/oauth/v2/token",
|
|
61
|
+
userinfo_endpoint="https://esignet.ida.fayda.et/v1/esignet/oidc/userinfo",
|
|
62
|
+
jwks_uri="https://esignet.ida.fayda.et/v1/esignet/oauth/v2/jwks",
|
|
63
|
+
signing_key_path="/run/secrets/fayda_key.pem",
|
|
64
|
+
session_ttl_seconds=300,
|
|
65
|
+
result_ttl_seconds=600,
|
|
66
|
+
)
|
|
67
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Credential Ownership & Cryptography
|
|
2
|
+
|
|
3
|
+
The `fayda-mcp` library adheres to a zero-secret design:
|
|
4
|
+
- **No hardcoded credentials**: The library contains zero client IDs, secret keys, or test credentials.
|
|
5
|
+
- **Host ownership**: The integrating host application owns and manages its Relying Party registration with National ID / Fayda (eSignet).
|
|
6
|
+
- **Private key isolation**: Private keys never leave the host environment and are never transmitted over the Model Context Protocol.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Authentication Method: `private_key_jwt`
|
|
11
|
+
|
|
12
|
+
Fayda eSignet uses RFC 7523 `private_key_jwt` authentication at the token endpoint instead of shared client secrets.
|
|
13
|
+
|
|
14
|
+
To authenticate:
|
|
15
|
+
1. The host signs an assertion JWT using its private RSA key (`RS256`).
|
|
16
|
+
2. Fayda verifies the signature using the host's public JWKS registered during Relying Party onboarding.
|
|
17
|
+
3. No shared secret is ever transmitted over the wire.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Generating Your Keypair
|
|
22
|
+
|
|
23
|
+
Generate a standard 2048-bit or 4096-bit RSA keypair:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Generate private key
|
|
27
|
+
openssl genrsa -out fayda_private_key.pem 2048
|
|
28
|
+
|
|
29
|
+
# Extract public key to register with Fayda
|
|
30
|
+
openssl rsa -in fayda_private_key.pem -pubout -out fayda_public_key.pem
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Alternatively, you can export a JSON Web Key (JWK) containing the `kty: "RSA"`, `kid`, `n`, and `e` fields for registration in the Fayda Developer Portal.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Configuring the Private Key
|
|
38
|
+
|
|
39
|
+
### Option A: Local File Path
|
|
40
|
+
Provide the path to your PEM or JWK private key file via configuration:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from fayda_mcp import FaydaConfig
|
|
44
|
+
|
|
45
|
+
config = FaydaConfig(
|
|
46
|
+
client_id="my_registered_client_id",
|
|
47
|
+
redirect_uri="https://app.example/callback",
|
|
48
|
+
signing_key_path="/run/secrets/fayda_private_key.pem",
|
|
49
|
+
...,
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Option B: Cloud Secret Managers (`KeyProvider` Interface)
|
|
54
|
+
In production, avoid storing private keys on disk. Implement the `KeyProvider` protocol to resolve keys from AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from fayda_mcp.secrets.protocols import KeyProvider
|
|
58
|
+
|
|
59
|
+
class VaultKeyProvider(KeyProvider):
|
|
60
|
+
async def get_private_key(self) -> str:
|
|
61
|
+
# Fetch private key PEM from HashiCorp Vault or AWS Secrets Manager
|
|
62
|
+
return await vault_client.read_secret("fayda/private_key")
|
|
63
|
+
|
|
64
|
+
service = FaydaVerificationService(
|
|
65
|
+
config=config,
|
|
66
|
+
sessions=sessions,
|
|
67
|
+
results=results,
|
|
68
|
+
key_provider=VaultKeyProvider(),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Example mounting Fayda FastMCP server into FastAPI with callback route."""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import RedirectResponse
|
|
5
|
+
from fayda_mcp import FaydaConfig, FaydaVerificationService
|
|
6
|
+
from fayda_mcp.storage.memory import MemoryResultRepository, MemorySessionStore
|
|
7
|
+
from fayda_mcp.mcp.factory import create_mcp_server
|
|
8
|
+
from fayda_mcp.integrations.fastapi import (
|
|
9
|
+
create_callback_router,
|
|
10
|
+
create_combined_lifespan,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# 1. Host Fayda Configuration
|
|
14
|
+
config = FaydaConfig(
|
|
15
|
+
client_id="demo-client",
|
|
16
|
+
redirect_uri="http://localhost:8000/auth/fayda/callback",
|
|
17
|
+
issuer="https://esignet.sandbox.fayda.et",
|
|
18
|
+
authorization_endpoint="https://esignet.sandbox.fayda.et/authorize",
|
|
19
|
+
token_endpoint="https://esignet.sandbox.fayda.et/oauth/token",
|
|
20
|
+
userinfo_endpoint="https://esignet.sandbox.fayda.et/oidc/userinfo",
|
|
21
|
+
jwks_uri="https://esignet.sandbox.fayda.et/jwks.json",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 2. Service instance with storage contracts
|
|
25
|
+
service = FaydaVerificationService(
|
|
26
|
+
config=config,
|
|
27
|
+
sessions=MemorySessionStore(),
|
|
28
|
+
results=MemoryResultRepository(),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# 3. FastMCP server instance
|
|
32
|
+
mcp_server = create_mcp_server(service=service)
|
|
33
|
+
|
|
34
|
+
# 4. Combined lifecycle ensuring both service and MCP lifespans run cleanly
|
|
35
|
+
lifespan = create_combined_lifespan(
|
|
36
|
+
service=service,
|
|
37
|
+
mcp_server=mcp_server,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 5. Host FastAPI application
|
|
41
|
+
app = FastAPI(title="Developer Host App", lifespan=lifespan)
|
|
42
|
+
|
|
43
|
+
# Mount MCP endpoint
|
|
44
|
+
app.mount("/mcp", mcp_server.http_app())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# 6. Host session-binding hook (extracts cookie or session header)
|
|
48
|
+
def host_session_binding(request: Request) -> str | None:
|
|
49
|
+
return request.cookies.get("host_session_token")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 7. Mount callback router matching host registered redirect URI
|
|
53
|
+
app.include_router(
|
|
54
|
+
create_callback_router(
|
|
55
|
+
service=service,
|
|
56
|
+
session_binding_hook=host_session_binding,
|
|
57
|
+
on_success=lambda req, result: {"status": "success", "result": result},
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.get("/health")
|
|
63
|
+
async def health():
|
|
64
|
+
return {"status": "ok"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
import uvicorn
|
|
69
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|