|
1 | 1 | """Authentication for the MCP server. |
2 | 2 |
|
3 | | -Supports two authentication modes simultaneously via MultiAuth: |
4 | | -
|
5 | | -1. **Keycloak OIDC** (for Claude.ai connectors and other OAuth clients): |
6 | | - The server proxies the full OAuth flow via OIDCProxy using pre-registered |
7 | | - Keycloak client credentials. No Dynamic Client Registration (DCR) needed. |
8 | | -
|
9 | | -2. **Bearer token** (for Claude Code, n8n, and other direct clients): |
10 | | - Simple static API key validation via Authorization: Bearer <key>. |
11 | | -
|
12 | | -The API key is configured via the THINGS_MCP_API_KEY environment variable |
13 | | -or .env file. If not set, the server generates one on first startup and |
14 | | -saves it to .env automatically. |
| 3 | +Uses OIDCProxy to proxy the OAuth flow to Keycloak using pre-registered |
| 4 | +client credentials. Claude.ai (and Claude Code through it) authenticates |
| 5 | +via the standard authorization_code flow. |
15 | 6 | """ |
16 | 7 |
|
17 | | -import hmac |
18 | | -import secrets |
19 | | - |
20 | | -from fastmcp.server.auth import ( |
21 | | - AccessToken, |
22 | | - MultiAuth, |
23 | | - TokenVerifier, |
24 | | -) |
25 | 8 | from fastmcp.server.auth.oidc_proxy import OIDCProxy |
26 | 9 |
|
27 | 10 | from .logging_config import get_logger |
28 | 11 |
|
29 | 12 | logger = get_logger(__name__) |
30 | 13 |
|
31 | 14 |
|
32 | | -class BearerTokenVerifier(TokenVerifier): |
33 | | - """Validates incoming requests against a static API key. |
34 | | -
|
35 | | - Uses constant-time comparison to prevent timing attacks. |
36 | | - """ |
37 | | - |
38 | | - def __init__(self, api_key: str): |
39 | | - super().__init__() |
40 | | - self._api_key = api_key |
41 | | - |
42 | | - async def verify_token(self, token: str) -> AccessToken | None: |
43 | | - if not hmac.compare_digest(token, self._api_key): |
44 | | - logger.warning("Rejected request with invalid API key") |
45 | | - return None |
46 | | - |
47 | | - return AccessToken( |
48 | | - token=token, |
49 | | - client_id="things-mcp-client", |
50 | | - scopes=["all"], |
51 | | - ) |
52 | | - |
53 | | - |
54 | 15 | def create_auth( |
55 | | - api_key: str | None, |
56 | 16 | base_url: str, |
57 | 17 | keycloak_issuer: str, |
58 | | - keycloak_audience: str, |
59 | 18 | keycloak_client_id: str, |
60 | 19 | keycloak_client_secret: str, |
61 | | -) -> MultiAuth: |
62 | | - """Create the authentication provider. |
63 | | -
|
64 | | - Returns a MultiAuth that accepts both: |
65 | | - - Keycloak OIDC clients (Claude.ai) via OIDCProxy (server-side OAuth) |
66 | | - - Bearer token clients (Claude Code, n8n) via static API key |
| 20 | +) -> OIDCProxy: |
| 21 | + """Create the OIDCProxy authentication provider. |
67 | 22 |
|
68 | 23 | Args: |
69 | | - api_key: Static API key for bearer token auth (None to skip). |
70 | 24 | base_url: Public URL of this server (e.g. https://things.example.com). |
71 | 25 | keycloak_issuer: Keycloak realm issuer URL |
72 | 26 | (e.g. https://auth.cdit-works.de/realms/cdit-mcp). |
73 | | - keycloak_audience: Expected JWT audience claim (e.g. mcp-things). |
74 | 27 | keycloak_client_id: Pre-registered Keycloak client ID. |
75 | 28 | keycloak_client_secret: Keycloak client secret. |
76 | 29 | """ |
77 | 30 | config_url = f"{keycloak_issuer}/.well-known/openid-configuration" |
78 | 31 |
|
79 | | - oidc_auth = OIDCProxy( |
| 32 | + return OIDCProxy( |
80 | 33 | config_url=config_url, |
81 | 34 | client_id=keycloak_client_id, |
82 | 35 | client_secret=keycloak_client_secret, |
83 | 36 | base_url=base_url, |
84 | 37 | ) |
85 | | - |
86 | | - verifiers: list[TokenVerifier] = [] |
87 | | - if api_key: |
88 | | - verifiers.append(BearerTokenVerifier(api_key)) |
89 | | - |
90 | | - return MultiAuth(server=oidc_auth, verifiers=verifiers) |
91 | | - |
92 | | - |
93 | | -def generate_api_key() -> str: |
94 | | - """Generate a cryptographically secure API key.""" |
95 | | - return f"tmcp_{secrets.token_urlsafe(32)}" |
0 commit comments