882. MCP server redirects to auth.acedata.cloud/oauth2/authorize (consent page)
993. User logs in (if needed), sees consent page, approves
10104. 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
1717import base64
@@ -48,33 +48,31 @@ def _normalize_scopes(scopes: list[str] | None) -> list[str]:
4848class 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