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.
Files changed (56) hide show
  1. fayda_mcp-0.1.0/.gitignore +11 -0
  2. fayda_mcp-0.1.0/LICENSE +17 -0
  3. fayda_mcp-0.1.0/PKG-INFO +134 -0
  4. fayda_mcp-0.1.0/README.md +94 -0
  5. fayda_mcp-0.1.0/docs/architecture.md +22 -0
  6. fayda_mcp-0.1.0/docs/callback_wiring.md +65 -0
  7. fayda_mcp-0.1.0/docs/cleanup_and_lifecycle.md +56 -0
  8. fayda_mcp-0.1.0/docs/configuration.md +67 -0
  9. fayda_mcp-0.1.0/docs/credentials.md +70 -0
  10. fayda_mcp-0.1.0/examples/fastapi_server.py +69 -0
  11. fayda_mcp-0.1.0/examples/remote_http_server.py +104 -0
  12. fayda_mcp-0.1.0/examples/sandbox_flow.py +152 -0
  13. fayda_mcp-0.1.0/examples/stdio_server.py +70 -0
  14. fayda_mcp-0.1.0/guide.pdf +0 -0
  15. fayda_mcp-0.1.0/pyproject.toml +82 -0
  16. fayda_mcp-0.1.0/pyrightconfig.json +6 -0
  17. fayda_mcp-0.1.0/src/fayda_mcp/__init__.py +62 -0
  18. fayda_mcp-0.1.0/src/fayda_mcp/claims.py +85 -0
  19. fayda_mcp-0.1.0/src/fayda_mcp/config.py +147 -0
  20. fayda_mcp-0.1.0/src/fayda_mcp/context.py +95 -0
  21. fayda_mcp-0.1.0/src/fayda_mcp/exceptions.py +90 -0
  22. fayda_mcp-0.1.0/src/fayda_mcp/integrations/__init__.py +2 -0
  23. fayda_mcp-0.1.0/src/fayda_mcp/integrations/fastapi.py +223 -0
  24. fayda_mcp-0.1.0/src/fayda_mcp/mcp/__init__.py +4 -0
  25. fayda_mcp-0.1.0/src/fayda_mcp/mcp/factory.py +40 -0
  26. fayda_mcp-0.1.0/src/fayda_mcp/mcp/tools.py +118 -0
  27. fayda_mcp-0.1.0/src/fayda_mcp/oidc/__init__.py +4 -0
  28. fayda_mcp-0.1.0/src/fayda_mcp/oidc/assertions.py +38 -0
  29. fayda_mcp-0.1.0/src/fayda_mcp/oidc/authorization.py +59 -0
  30. fayda_mcp-0.1.0/src/fayda_mcp/oidc/client.py +90 -0
  31. fayda_mcp-0.1.0/src/fayda_mcp/oidc/jwks.py +75 -0
  32. fayda_mcp-0.1.0/src/fayda_mcp/oidc/mapping.py +109 -0
  33. fayda_mcp-0.1.0/src/fayda_mcp/oidc/tokens.py +126 -0
  34. fayda_mcp-0.1.0/src/fayda_mcp/policy.py +65 -0
  35. fayda_mcp-0.1.0/src/fayda_mcp/schemas.py +75 -0
  36. fayda_mcp-0.1.0/src/fayda_mcp/secrets/__init__.py +6 -0
  37. fayda_mcp-0.1.0/src/fayda_mcp/secrets/file.py +18 -0
  38. fayda_mcp-0.1.0/src/fayda_mcp/secrets/keys.py +85 -0
  39. fayda_mcp-0.1.0/src/fayda_mcp/secrets/protocols.py +12 -0
  40. fayda_mcp-0.1.0/src/fayda_mcp/service.py +365 -0
  41. fayda_mcp-0.1.0/src/fayda_mcp/storage/__init__.py +20 -0
  42. fayda_mcp-0.1.0/src/fayda_mcp/storage/memory.py +187 -0
  43. fayda_mcp-0.1.0/src/fayda_mcp/storage/postgres.py +41 -0
  44. fayda_mcp-0.1.0/src/fayda_mcp/storage/protocols.py +84 -0
  45. fayda_mcp-0.1.0/src/fayda_mcp/storage/redis.py +50 -0
  46. fayda_mcp-0.1.0/tests/integration/__init__.py +1 -0
  47. fayda_mcp-0.1.0/tests/integration/test_sandbox_flow.py +104 -0
  48. fayda_mcp-0.1.0/tests/unit/test_claims_results.py +261 -0
  49. fayda_mcp-0.1.0/tests/unit/test_code_exchange_jwt.py +215 -0
  50. fayda_mcp-0.1.0/tests/unit/test_fastapi_integration.py +303 -0
  51. fayda_mcp-0.1.0/tests/unit/test_foundation.py +122 -0
  52. fayda_mcp-0.1.0/tests/unit/test_mcp_factory.py +317 -0
  53. fayda_mcp-0.1.0/tests/unit/test_oidc_authorization.py +190 -0
  54. fayda_mcp-0.1.0/tests/unit/test_optional_extras.py +46 -0
  55. fayda_mcp-0.1.0/tests/unit/test_release_packaging.py +103 -0
  56. fayda_mcp-0.1.0/tests/unit/test_storage_contracts.py +249 -0
@@ -0,0 +1,11 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ scratch*
@@ -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.
@@ -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
+ [![PyPI version](https://img.shields.io/badge/version-0.1.0-blue.svg)](https://pypi.org/project/fayda-mcp/)
44
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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
+ [![PyPI version](https://img.shields.io/badge/version-0.1.0-blue.svg)](https://pypi.org/project/fayda-mcp/)
4
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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)