Skip to content

Commit 0473b0b

Browse files
sync: chore(mcp): drop dead refresh/offline_access flow from 21 per-service MCPs (#428)
Synced from monorepo directory: suno
1 parent 71d3ca1 commit 0473b0b

1 file changed

Lines changed: 31 additions & 160 deletions

File tree

core/oauth.py

Lines changed: 31 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
2. MCP server redirects to auth.acedata.cloud/oauth2/authorize (consent page)
99
3. User logs in (if needed), sees consent page, approves
1010
4. auth.acedata.cloud issues an authorization code, redirects to /oauth/callback
11-
5. MCP server exchanges code for JWT + refresh_token via POST /oauth2/token (with PKCE)
12-
6. MCP server uses JWT to fetch/create user's API credential
13-
7. Issues the credential token as the OAuth access_token
14-
8. On refresh: calls auth.acedata.cloud with stored refresh_token → new JWT → re-fetch credential
11+
5. MCP server exchanges code for a JWT via POST /oauth2/token (with PKCE)
12+
6. MCP server uses the JWT to fetch/create the user's API credential
13+
7. Issues the durable credential token as the OAuth access_token — the credential
14+
does not expire, so there is no refresh flow and restarts never force re-auth
1515
"""
1616

1717
import base64
@@ -48,33 +48,31 @@ def _normalize_scopes(scopes: list[str] | None) -> list[str]:
4848
class AceDataCloudOAuthProvider:
4949
"""OAuth provider that delegates authentication to AceDataCloud platform.
5050
51-
Refresh tokens are backed by auth.acedata.cloud — pod restarts don't break
52-
refresh because the real refresh_token lives at the authorization server.
51+
The access token is the user's durable, non-expiring api.acedata.cloud
52+
credential. There is no refresh flow — the credential never expires, so pod
53+
restarts/redeploys never force re-authorization.
5354
"""
5455

5556
def __init__(self) -> None:
5657
self._clients: dict[str, OAuthClientInformationFull] = {}
5758
self._auth_codes: dict[
58-
str, tuple[AuthorizationCode, str, str | None]
59-
] = {} # code → (AuthCode, api_token, auth_refresh_token)
59+
str, tuple[AuthorizationCode, str]
60+
] = {} # code → (AuthCode, api_token)
6061
self._access_tokens: dict[str, AccessToken] = {}
61-
self._refresh_tokens: dict[str, RefreshToken] = {}
62-
# Maps MCP refresh_token → auth.acedata.cloud refresh_token (the real one)
63-
self._auth_refresh_tokens: dict[str, str] = {}
6462
self._pending_auth: dict[str, dict] = {} # mcp_state → {client_id, params}
6563

6664
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
6765
client = self._clients.get(client_id)
6866
if client:
6967
return client
7068
# After pod restart, registered clients are forgotten. Synthesize so
71-
# token/refresh calls don't get a hard 401 — real auth is via
72-
# auth.acedata.cloud refresh_token validation.
69+
# token calls don't get a hard 401 — the access token is a durable
70+
# api.acedata.cloud credential validated upstream.
7371
synthetic = OAuthClientInformationFull(
7472
client_id=client_id,
7573
redirect_uris=[AnyUrl("https://auth.acedata.cloud/user/connections")],
7674
token_endpoint_auth_method="none",
77-
grant_types=["authorization_code", "refresh_token"],
75+
grant_types=["authorization_code"],
7876
response_types=["code"],
7977
)
8078
self._clients[client_id] = synthetic
@@ -111,12 +109,12 @@ async def authorize(
111109

112110
callback_url = f"{settings.server_url}/oauth/callback"
113111

114-
# Request offline_access so auth.acedata.cloud returns a refresh_token
112+
# Only need profile + platform to fetch the user's durable credential.
115113
auth_params = {
116114
"client_id": settings.oauth_client_id,
117115
"redirect_uri": callback_url,
118116
"response_type": "code",
119-
"scope": "profile platform offline_access",
117+
"scope": "profile platform",
120118
"state": mcp_state,
121119
"code_challenge": auth_code_challenge,
122120
"code_challenge_method": "S256",
@@ -149,7 +147,6 @@ async def handle_callback(self, request: Request) -> RedirectResponse | JSONResp
149147
)
150148

151149
jwt_token = token_data["access_token"]
152-
auth_refresh = token_data.get("refresh_token") # from auth.acedata.cloud
153150

154151
# Fetch user's API credential using the JWT
155152
api_token = await self._get_user_credential(jwt_token)
@@ -174,7 +171,7 @@ async def handle_callback(self, request: Request) -> RedirectResponse | JSONResp
174171
redirect_uri_provided_explicitly=pending["redirect_uri_provided_explicitly"],
175172
resource=pending.get("resource"),
176173
)
177-
self._auth_codes[auth_code_str] = (auth_code, api_token, auth_refresh)
174+
self._auth_codes[auth_code_str] = (auth_code, api_token)
178175

179176
# Redirect back to Claude with the MCP auth code
180177
redirect_uri = pending["redirect_uri"]
@@ -211,141 +208,46 @@ async def exchange_authorization_code(
211208
data = self._auth_codes.pop(authorization_code.code, None)
212209
if not data:
213210
raise ValueError("Authorization code not found or already used")
214-
_, api_token, auth_refresh = data
211+
_, api_token = data
215212

216213
client_id = client.client_id or ""
217214

218-
# Store access token
215+
# The access token is the user's durable, non-expiring credential.
219216
self._access_tokens[api_token] = AccessToken(
220217
token=api_token,
221218
client_id=client_id,
222219
scopes=_normalize_scopes(authorization_code.scopes),
223220
expires_at=None,
224221
)
225222

226-
# Issue MCP refresh_token backed by auth.acedata.cloud's refresh_token
227-
mcp_refresh_str = secrets.token_urlsafe(48)
228-
self._refresh_tokens[mcp_refresh_str] = RefreshToken(
229-
token=mcp_refresh_str,
230-
client_id=client_id,
231-
scopes=_normalize_scopes(authorization_code.scopes),
232-
)
233-
if auth_refresh:
234-
self._auth_refresh_tokens[mcp_refresh_str] = auth_refresh
235-
236-
logger.info(
237-
f"OAuth token exchange: issued access token for client {client_id} "
238-
f"(auth_refresh={'yes' if auth_refresh else 'no'})"
239-
)
223+
logger.info(f"OAuth token exchange: issued durable credential for client {client_id}")
240224
return OAuthToken(
241225
access_token=api_token,
242226
token_type="Bearer",
243227
scope=" ".join(_normalize_scopes(authorization_code.scopes)),
244-
refresh_token=mcp_refresh_str if auth_refresh else None,
245228
)
246229

247230
async def load_refresh_token(
248231
self,
249-
client: OAuthClientInformationFull,
250-
refresh_token: str,
232+
client: OAuthClientInformationFull, # noqa: ARG002
233+
refresh_token: str, # noqa: ARG002
251234
) -> RefreshToken | None:
252-
stored = self._refresh_tokens.get(refresh_token)
253-
if stored:
254-
return stored
255-
# After pod restart, in-memory refresh tokens are lost. Synthesize so
256-
# exchange_refresh_token can attempt auth.acedata.cloud refresh.
257-
return RefreshToken(
258-
token=refresh_token,
259-
client_id=client.client_id or "",
260-
scopes=[MCP_ACCESS_SCOPE],
261-
)
235+
# No refresh flow — the access token is a durable, non-expiring credential.
236+
return None
262237

263238
async def exchange_refresh_token(
264239
self,
265-
client: OAuthClientInformationFull,
266-
refresh_token: RefreshToken,
267-
scopes: list[str],
240+
client: OAuthClientInformationFull, # noqa: ARG002
241+
refresh_token: RefreshToken, # noqa: ARG002
242+
scopes: list[str], # noqa: ARG002
268243
) -> OAuthToken:
269-
"""Refresh by calling auth.acedata.cloud with the stored auth refresh_token."""
270-
client_id = client.client_id or ""
271-
old_mcp_refresh = refresh_token.token
272-
273-
# Get the auth.acedata.cloud refresh_token mapped to this MCP refresh
274-
auth_refresh = self._auth_refresh_tokens.pop(old_mcp_refresh, None)
275-
self._refresh_tokens.pop(old_mcp_refresh, None)
276-
277-
if not auth_refresh:
278-
# Post-restart: no mapping. Fall back to in-memory access_token if available.
279-
for token, at in self._access_tokens.items():
280-
if at.client_id == client_id:
281-
new_mcp_refresh = secrets.token_urlsafe(48)
282-
self._refresh_tokens[new_mcp_refresh] = RefreshToken(
283-
token=new_mcp_refresh,
284-
client_id=client_id,
285-
scopes=_normalize_scopes(scopes or refresh_token.scopes),
286-
)
287-
return OAuthToken(
288-
access_token=token,
289-
token_type="Bearer",
290-
scope=" ".join(_normalize_scopes(scopes or refresh_token.scopes)),
291-
refresh_token=new_mcp_refresh,
292-
)
293-
from mcp.server.auth.provider import TokenError
294-
295-
raise TokenError(
296-
error="invalid_grant",
297-
error_description="Refresh token expired, please re-authorize",
298-
)
299-
300-
# Call auth.acedata.cloud to refresh the JWT
301-
token_data = await self._refresh_auth_token(auth_refresh)
302-
if not token_data:
303-
from mcp.server.auth.provider import TokenError
304-
305-
raise TokenError(
306-
error="invalid_grant",
307-
error_description="auth.acedata.cloud refresh failed, please re-authorize",
308-
)
244+
from mcp.server.auth.provider import TokenError
309245

310-
new_jwt = token_data["access_token"]
311-
new_auth_refresh = token_data.get("refresh_token", auth_refresh)
312-
313-
# Use new JWT to fetch/verify the user's API credential
314-
api_token = await self._get_user_credential(new_jwt)
315-
if not api_token:
316-
from mcp.server.auth.provider import TokenError
317-
318-
raise TokenError(
319-
error="invalid_grant",
320-
error_description="Failed to fetch API credential after refresh",
321-
)
322-
323-
# Remove old access_token for this client, store new one
324-
for token, at in list(self._access_tokens.items()):
325-
if at.client_id == client_id:
326-
del self._access_tokens[token]
327-
self._access_tokens[api_token] = AccessToken(
328-
token=api_token,
329-
client_id=client_id,
330-
scopes=_normalize_scopes(scopes or refresh_token.scopes),
331-
expires_at=None,
332-
)
333-
334-
# Issue new MCP refresh_token mapped to the new auth refresh_token
335-
new_mcp_refresh = secrets.token_urlsafe(48)
336-
self._refresh_tokens[new_mcp_refresh] = RefreshToken(
337-
token=new_mcp_refresh,
338-
client_id=client_id,
339-
scopes=_normalize_scopes(scopes or refresh_token.scopes),
340-
)
341-
self._auth_refresh_tokens[new_mcp_refresh] = new_auth_refresh
342-
343-
logger.info(f"OAuth refresh: issued new access token for client {client_id}")
344-
return OAuthToken(
345-
access_token=api_token,
346-
token_type="Bearer",
347-
scope=" ".join(_normalize_scopes(scopes or refresh_token.scopes)),
348-
refresh_token=new_mcp_refresh,
246+
# Tokens never expire, so there is nothing to refresh. An old client that
247+
# still holds a pre-migration refresh_token gets a clean re-auth prompt.
248+
raise TokenError(
249+
error="invalid_grant",
250+
error_description="This server issues non-expiring tokens; refresh is not supported.",
349251
)
350252

351253
async def load_access_token(self, token: str) -> AccessToken | None:
@@ -369,9 +271,6 @@ async def load_access_token(self, token: str) -> AccessToken | None:
369271
async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
370272
if isinstance(token, AccessToken):
371273
self._access_tokens.pop(token.token, None)
372-
elif isinstance(token, RefreshToken):
373-
self._auth_refresh_tokens.pop(token.token, None)
374-
self._refresh_tokens.pop(token.token, None)
375274
logger.info(f"Revoked token: {token.token[:8]}...")
376275

377276
# --- Internal helpers ---
@@ -434,34 +333,6 @@ async def _exchange_code_for_tokens(
434333
logger.exception("OAuth token exchange error")
435334
return None
436335

437-
async def _refresh_auth_token(self, auth_refresh_token: str) -> dict[str, str] | None:
438-
"""Call auth.acedata.cloud to refresh the JWT using the auth refresh_token."""
439-
token_url = f"{settings.auth_base_url}/oauth2/token"
440-
try:
441-
async with httpx.AsyncClient(timeout=30) as client:
442-
response = await client.post(
443-
token_url,
444-
data={
445-
"grant_type": "refresh_token",
446-
"refresh_token": auth_refresh_token,
447-
"client_id": settings.oauth_client_id,
448-
},
449-
)
450-
if response.status_code == 200:
451-
data: dict[str, str] = response.json()
452-
if data.get("access_token"):
453-
logger.info("auth.acedata.cloud refresh OK, new JWT issued")
454-
return data
455-
logger.error(f"Auth refresh 200 but no access_token: {list(data.keys())}")
456-
else:
457-
logger.warning(
458-
f"auth.acedata.cloud refresh failed: {response.status_code} "
459-
f"{response.text[:200]}"
460-
)
461-
except Exception:
462-
logger.exception("auth.acedata.cloud refresh error")
463-
return None
464-
465336
async def _get_user_credential(self, jwt_token: str) -> str | None:
466337
"""Fetch or auto-create user's API credential token from PlatformBackend.
467338

0 commit comments

Comments
 (0)