mcp-authkit 0.2.1__tar.gz → 0.2.3__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.
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/PKG-INFO +1 -1
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/PKG-INFO +1 -1
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/auth_routes.py +26 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/oauth_provider.py +34 -13
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/pyproject.toml +1 -1
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_auth_routes.py +60 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/LICENSE +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/README.md +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/SOURCES.txt +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/dependency_links.txt +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/requires.txt +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/top_level.txt +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/__init__.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/auth_middleware.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/jwt_validator.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/__init__.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/credentials_provider.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/base.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_entry.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_error.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_success.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/oauth_error.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/oauth_success.html +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/py.typed +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/__init__.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/base.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/encryption.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/factory.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/file_store.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/memory.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/redis_store.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/setup.cfg +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_auth_middleware.py +0 -0
- {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_jwt_validator.py +0 -0
|
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import logging
|
|
26
26
|
import time
|
|
27
|
+
from urllib.parse import urlencode
|
|
27
28
|
|
|
28
29
|
import httpx
|
|
29
30
|
from fastapi import APIRouter, Request
|
|
@@ -37,6 +38,7 @@ def oauth_meta_router(
|
|
|
37
38
|
server_base_url: str,
|
|
38
39
|
issuer_url: str,
|
|
39
40
|
client_id: str,
|
|
41
|
+
extra_authorize_params: dict[str, str] | None = None,
|
|
40
42
|
) -> APIRouter:
|
|
41
43
|
"""
|
|
42
44
|
Return an ``APIRouter`` with well-known OAuth metadata routes and a DCR
|
|
@@ -52,6 +54,26 @@ def oauth_meta_router(
|
|
|
52
54
|
``"https://login.microsoftonline.com/{tenant}/v2.0"``.
|
|
53
55
|
client_id
|
|
54
56
|
Pre-registered public client ID returned by the DCR façade.
|
|
57
|
+
extra_authorize_params
|
|
58
|
+
Optional extra query parameters appended to the
|
|
59
|
+
``authorization_endpoint`` in the
|
|
60
|
+
``/.well-known/oauth-authorization-server`` response. MCP clients
|
|
61
|
+
read that URL and use it verbatim when redirecting the user to the
|
|
62
|
+
OIDC provider, so any hint placed here is automatically forwarded.
|
|
63
|
+
|
|
64
|
+
Use this for provider-specific routing parameters that fall outside
|
|
65
|
+
the standard OAuth 2.0 / OIDC spec. For example, Okta's ``idp``
|
|
66
|
+
parameter bypasses the Okta login page and routes users directly to
|
|
67
|
+
a configured external Identity Provider::
|
|
68
|
+
|
|
69
|
+
app.include_router(oauth_meta_router(
|
|
70
|
+
server_base_url=settings.server_base_url,
|
|
71
|
+
issuer_url="https://your-org.okta.com/oauth2/default",
|
|
72
|
+
client_id=settings.okta_client_id,
|
|
73
|
+
extra_authorize_params={"idp": "0oaz2r21a8RBmZyOL0h7"},
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
Default: ``None`` (no extra params — fully retro-compatible).
|
|
55
77
|
"""
|
|
56
78
|
router = APIRouter()
|
|
57
79
|
base = server_base_url.rstrip("/")
|
|
@@ -85,6 +107,10 @@ def oauth_meta_router(
|
|
|
85
107
|
except Exception as exc:
|
|
86
108
|
logger.warning("Could not fetch OIDC metadata: %s", exc)
|
|
87
109
|
|
|
110
|
+
if extra_authorize_params:
|
|
111
|
+
sep = "&" if "?" in auth_ep else "?"
|
|
112
|
+
auth_ep += sep + urlencode(extra_authorize_params)
|
|
113
|
+
|
|
88
114
|
return JSONResponse(
|
|
89
115
|
{
|
|
90
116
|
"issuer": base,
|
|
@@ -208,10 +208,11 @@ class OAuthProvider:
|
|
|
208
208
|
refresh_token_fn: Callable[..., Coroutine[Any, Any, ExchangeResult]] | None = None,
|
|
209
209
|
token_timeout: float = 120.0,
|
|
210
210
|
http_verify: bool | ssl.SSLContext | str = True,
|
|
211
|
+
extra_authorize_params: dict[str, str] | None = None,
|
|
211
212
|
) -> OAuthProvider:
|
|
212
213
|
"""
|
|
213
214
|
Convenience factory for any standard OAuth2 Authorization Code provider
|
|
214
|
-
(GitHub, Google, Jira, Entra, etc.).
|
|
215
|
+
(GitHub, Google, Jira, Entra, Okta, etc.).
|
|
215
216
|
|
|
216
217
|
Builds ``build_auth_url`` and ``exchange_code`` internally from standard
|
|
217
218
|
OAuth2 endpoints so the caller only needs to supply configuration::
|
|
@@ -242,6 +243,25 @@ class OAuthProvider:
|
|
|
242
243
|
Space-separated scope string.
|
|
243
244
|
http_verify
|
|
244
245
|
Passed as ``verify=`` to httpx for the token exchange request.
|
|
246
|
+
extra_authorize_params
|
|
247
|
+
Optional extra query parameters appended to every authorization URL.
|
|
248
|
+
Use this for provider-specific routing hints that are not part of the
|
|
249
|
+
standard OAuth2 spec. For example, Okta supports an ``idp`` parameter
|
|
250
|
+
to bypass its login page and route users directly to a configured
|
|
251
|
+
external Identity Provider::
|
|
252
|
+
|
|
253
|
+
okta = OAuthProvider.from_standard_oauth2(
|
|
254
|
+
name="okta",
|
|
255
|
+
authorization_url="https://your-org.okta.com/oauth2/default/v1/authorize",
|
|
256
|
+
token_url="https://your-org.okta.com/oauth2/default/v1/token",
|
|
257
|
+
...
|
|
258
|
+
extra_authorize_params={"idp": "0oaz2r21a8RBmZyOL0h7"},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
These parameters are merged into the standard ones
|
|
262
|
+
(``client_id``, ``redirect_uri``, ``scope``, ``state``,
|
|
263
|
+
``response_type``). Standard parameters always take precedence so
|
|
264
|
+
they cannot be overridden here. Default: ``None`` (no extra params).
|
|
245
265
|
token_store
|
|
246
266
|
Optional persistent store override.
|
|
247
267
|
pending_store
|
|
@@ -251,19 +271,20 @@ class OAuthProvider:
|
|
|
251
271
|
"""
|
|
252
272
|
|
|
253
273
|
def _build_auth_url(state: str, redir: str) -> str:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
274
|
+
params: dict[str, str] = {}
|
|
275
|
+
if extra_authorize_params:
|
|
276
|
+
params.update(extra_authorize_params)
|
|
277
|
+
# Standard params always win over any extra ones
|
|
278
|
+
params.update(
|
|
279
|
+
{
|
|
280
|
+
"client_id": client_id,
|
|
281
|
+
"redirect_uri": redir,
|
|
282
|
+
"scope": scope,
|
|
283
|
+
"state": state,
|
|
284
|
+
"response_type": "code",
|
|
285
|
+
}
|
|
266
286
|
)
|
|
287
|
+
return authorization_url + "?" + urlencode(params)
|
|
267
288
|
|
|
268
289
|
async def _exchange_code(code: str, state: str, redir: str) -> ExchangeResult:
|
|
269
290
|
async with httpx.AsyncClient(
|
|
@@ -85,6 +85,66 @@ def test_authorization_server_pkce_supported(client):
|
|
|
85
85
|
assert "S256" in data["code_challenge_methods_supported"]
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def test_authorization_server_no_extra_params_by_default(client):
|
|
89
|
+
"""Default (extra_authorize_params=None) does not append any extra query params."""
|
|
90
|
+
data = client.get("/.well-known/oauth-authorization-server").json()
|
|
91
|
+
assert "idp=" not in data["authorization_endpoint"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_authorization_server_extra_params_appended():
|
|
95
|
+
"""extra_authorize_params are appended to authorization_endpoint."""
|
|
96
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
97
|
+
|
|
98
|
+
app = FastAPI()
|
|
99
|
+
app.include_router(
|
|
100
|
+
oauth_meta_router(
|
|
101
|
+
server_base_url=SERVER,
|
|
102
|
+
issuer_url=ISSUER,
|
|
103
|
+
client_id=CLIENT_ID,
|
|
104
|
+
extra_authorize_params={"idp": "0oaz2r21a8RBmZyOL0h7"},
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
tc = TestClient(app, raise_server_exceptions=False)
|
|
108
|
+
|
|
109
|
+
oidc_doc = {
|
|
110
|
+
"authorization_endpoint": "https://org.okta.com/oauth2/default/v1/authorize",
|
|
111
|
+
"token_endpoint": "https://org.okta.com/oauth2/default/v1/token",
|
|
112
|
+
"jwks_uri": "https://org.okta.com/oauth2/default/v1/keys",
|
|
113
|
+
}
|
|
114
|
+
mock_resp = MagicMock()
|
|
115
|
+
mock_resp.status_code = 200
|
|
116
|
+
mock_resp.json.return_value = oidc_doc
|
|
117
|
+
mock_client = MagicMock()
|
|
118
|
+
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
119
|
+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
120
|
+
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
121
|
+
|
|
122
|
+
with patch("mcpauthkit.auth_routes.httpx.AsyncClient", return_value=mock_client):
|
|
123
|
+
data = tc.get("/.well-known/oauth-authorization-server").json()
|
|
124
|
+
|
|
125
|
+
assert "idp=0oaz2r21a8RBmZyOL0h7" in data["authorization_endpoint"]
|
|
126
|
+
# Standard OIDC fields are still present
|
|
127
|
+
assert data["token_endpoint"] == "https://org.okta.com/oauth2/default/v1/token"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_authorization_server_extra_params_fallback_no_oidc():
|
|
131
|
+
"""extra_authorize_params appended even when OIDC discovery is unreachable."""
|
|
132
|
+
app = FastAPI()
|
|
133
|
+
app.include_router(
|
|
134
|
+
oauth_meta_router(
|
|
135
|
+
server_base_url=SERVER,
|
|
136
|
+
issuer_url=ISSUER,
|
|
137
|
+
client_id=CLIENT_ID,
|
|
138
|
+
extra_authorize_params={"idp": "abc123", "prompt": "login"},
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
tc = TestClient(app, raise_server_exceptions=False)
|
|
142
|
+
data = tc.get("/.well-known/oauth-authorization-server").json()
|
|
143
|
+
auth_ep = data["authorization_endpoint"]
|
|
144
|
+
assert "idp=abc123" in auth_ep
|
|
145
|
+
assert "prompt=login" in auth_ep
|
|
146
|
+
|
|
147
|
+
|
|
88
148
|
def test_authorization_server_uses_oidc_config(client):
|
|
89
149
|
"""When OIDC discovery returns 200, its endpoints override the defaults."""
|
|
90
150
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_entry.html
RENAMED
|
File without changes
|
{mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_error.html
RENAMED
|
File without changes
|
{mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_success.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|