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.
Files changed (34) hide show
  1. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/PKG-INFO +1 -1
  2. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/PKG-INFO +1 -1
  3. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/auth_routes.py +26 -0
  4. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/oauth_provider.py +34 -13
  5. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/pyproject.toml +1 -1
  6. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_auth_routes.py +60 -0
  7. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/LICENSE +0 -0
  8. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/README.md +0 -0
  9. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/SOURCES.txt +0 -0
  10. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/dependency_links.txt +0 -0
  11. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/requires.txt +0 -0
  12. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcp_authkit.egg-info/top_level.txt +0 -0
  13. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/__init__.py +0 -0
  14. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/auth_middleware.py +0 -0
  15. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/jwt_validator.py +0 -0
  16. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/__init__.py +0 -0
  17. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/credentials_provider.py +0 -0
  18. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/base.html +0 -0
  19. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_entry.html +0 -0
  20. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_error.html +0 -0
  21. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/credentials_success.html +0 -0
  22. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/oauth_error.html +0 -0
  23. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/providers/templates/oauth_success.html +0 -0
  24. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/py.typed +0 -0
  25. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/__init__.py +0 -0
  26. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/base.py +0 -0
  27. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/encryption.py +0 -0
  28. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/factory.py +0 -0
  29. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/file_store.py +0 -0
  30. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/memory.py +0 -0
  31. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/mcpauthkit/store/redis_store.py +0 -0
  32. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/setup.cfg +0 -0
  33. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_auth_middleware.py +0 -0
  34. {mcp_authkit-0.2.1 → mcp_authkit-0.2.3}/tests/test_jwt_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-authkit
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Pluggable OAuth 2.0 + credentials elicitation library for FastMCP servers
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/masterela/mcp-authkit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-authkit
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Pluggable OAuth 2.0 + credentials elicitation library for FastMCP servers
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/masterela/mcp-authkit
@@ -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
- return (
255
- authorization_url
256
- + "?"
257
- + urlencode(
258
- {
259
- "client_id": client_id,
260
- "redirect_uri": redir,
261
- "scope": scope,
262
- "state": state,
263
- "response_type": "code",
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(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-authkit"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Pluggable OAuth 2.0 + credentials elicitation library for FastMCP servers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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