Skip to content

Commit 878b19e

Browse files
committed
refactor: simplify token acquisition by removing async calls and unused tests
1 parent ec35916 commit 878b19e

7 files changed

Lines changed: 171 additions & 193 deletions

File tree

docs/api/exceptions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ OAuth Error Handling
129129
client_id=client_id,
130130
client_secret=client_secret
131131
)
132-
token = await credential_helper.get_token()
132+
token = credential_helper.get_token()
133133
except OAuthError as e:
134134
logger.error(f"OAuth authentication failed: {e}")
135135
# Handle OAuth failure - check credentials

docs/authentication.rst

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,6 @@ Set these environment variables for OAuth authentication:
7676
audience=os.getenv("OAUTH_AUDIENCE", "crisp-athena-live"),
7777
)
7878
79-
# Test token acquisition
80-
try:
81-
token = await credential_helper.get_token()
82-
print(f"Successfully acquired token (length: {len(token)})")
83-
except Exception as e:
84-
print(f"Failed to acquire OAuth token: {e}")
85-
return
86-
8779
# Create authenticated channel
8880
channel = await create_channel_with_credentials(
8981
host=os.getenv("ATHENA_HOST"),
@@ -261,7 +253,7 @@ Handle OAuth-specific errors gracefully:
261253
from resolver_athena_client.client.exceptions import AuthenticationError
262254
263255
try:
264-
token = await credential_helper.get_token()
256+
token = credential_helper.get_token()
265257
except AuthenticationError as e:
266258
logger.error(f"OAuth authentication failed: {e}")
267259
# Handle authentication failure
@@ -356,7 +348,7 @@ Test your authentication setup:
356348
client_secret=os.getenv("OAUTH_CLIENT_SECRET"),
357349
)
358350
359-
token = await credential_helper.get_token()
351+
token = credential_helper.get_token()
360352
print(f"✓ Authentication successful (token length: {len(token)})")
361353
return True
362354

examples/classify_single_example.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,6 @@ async def main() -> int:
213213
audience=audience,
214214
)
215215

216-
# Test token acquisition
217-
try:
218-
logger.info("Acquiring OAuth token...")
219-
token = await credential_helper.get_token()
220-
logger.info("Successfully acquired token (length: %d)", len(token))
221-
except Exception:
222-
logger.exception("Failed to acquire OAuth token")
223-
return 1
224-
225216
# Configure client options
226217
options = AthenaOptions(
227218
host=host,

examples/example.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,6 @@ async def main() -> int:
163163
audience=audience,
164164
)
165165

166-
# Test token acquisition
167-
try:
168-
logger.info("Acquiring OAuth token...")
169-
token = await credential_helper.get_token()
170-
logger.info("Successfully acquired token (length: %d)", len(token))
171-
except Exception:
172-
logger.exception("Failed to acquire OAuth token")
173-
return 1
174-
175166
# Get available deployment
176167
channel = await create_channel_with_credentials(host, credential_helper)
177168
async with DeploymentSelector(channel) as deployment_selector:

src/resolver_athena_client/client/channel.py

Lines changed: 83 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Channel creation utilities for the Athena client."""
22

3-
import asyncio
43
import json
4+
import threading
55
import time
66
from typing import override
77

@@ -16,39 +16,6 @@
1616
)
1717

1818

19-
class TokenMetadataPlugin(grpc.AuthMetadataPlugin):
20-
"""Plugin that adds authorization token to gRPC metadata."""
21-
22-
def __init__(self, token: str) -> None:
23-
"""Initialize the plugin with the auth token.
24-
25-
Args:
26-
----
27-
token: The authorization token to add to requests
28-
29-
"""
30-
self._token: str = token
31-
32-
@override
33-
def __call__(
34-
self,
35-
_: grpc.AuthMetadataContext,
36-
callback: grpc.AuthMetadataPluginCallback,
37-
) -> None:
38-
"""Pass authentication metadata to the provided callback.
39-
40-
This method will be invoked asynchronously in a separate thread.
41-
42-
Args:
43-
----
44-
callback: An AuthMetadataPluginCallback to be invoked either
45-
synchronously or asynchronously.
46-
47-
"""
48-
metadata = (("authorization", f"Token {self._token}"),)
49-
callback(metadata, None)
50-
51-
5219
class CredentialHelper:
5320
"""OAuth credential helper for managing authentication tokens."""
5421

@@ -82,13 +49,14 @@ def __init__(
8249
self._audience: str = audience
8350
self._token: str | None = None
8451
self._token_expires_at: float | None = None
85-
self._lock: asyncio.Lock = asyncio.Lock()
52+
self._lock: threading.Lock = threading.Lock()
8653

87-
async def get_token(self) -> str:
54+
def get_token(self) -> str:
8855
"""Get a valid authentication token.
8956
90-
This method will return a cached token if it's still valid,
91-
or fetch a new token if needed.
57+
Returns a cached token if still valid, or acquires a new one.
58+
The entire check-and-refresh cycle runs under a single lock
59+
acquisition to prevent TOCTOU races with ``invalidate_token``.
9260
9361
Returns
9462
-------
@@ -97,21 +65,18 @@ async def get_token(self) -> str:
9765
Raises
9866
------
9967
OAuthError: If token acquisition fails
100-
TokenExpiredError: If token has expired and refresh fails
68+
RuntimeError: If token is unexpectedly None after refresh
10169
10270
"""
103-
async with self._lock:
104-
if self._is_token_valid():
105-
if self._token is None:
106-
msg = "Token should be valid but is None"
107-
raise RuntimeError(msg)
108-
return self._token
109-
110-
await self._refresh_token()
111-
if self._token is None:
112-
msg = "Token refresh failed"
71+
with self._lock:
72+
if not self._is_token_valid():
73+
self._refresh_token()
74+
75+
token = self._token
76+
if token is None:
77+
msg = "Token is unexpectedly None after validity check"
11378
raise RuntimeError(msg)
114-
return self._token
79+
return token
11580

11681
def _is_token_valid(self) -> bool:
11782
"""Check if the current token is valid and not expired.
@@ -127,9 +92,12 @@ def _is_token_valid(self) -> bool:
12792
# Add 30 second buffer before expiration
12893
return time.time() < (self._token_expires_at - 30)
12994

130-
async def _refresh_token(self) -> None:
95+
def _refresh_token(self) -> None:
13196
"""Refresh the authentication token by making an OAuth request.
13297
98+
This is a synchronous call (suitable for the gRPC metadata-plugin
99+
thread) and must be called while ``self._lock`` is held.
100+
133101
Raises
134102
------
135103
OAuthError: If the OAuth request fails
@@ -145,21 +113,19 @@ async def _refresh_token(self) -> None:
145113
headers = {"content-type": "application/json"}
146114

147115
try:
148-
async with httpx.AsyncClient() as client:
149-
response = await client.post(
116+
with httpx.Client() as client:
117+
response = client.post(
150118
self._auth_url,
151119
json=payload,
152120
headers=headers,
153121
timeout=30.0,
154122
)
155123
_ = response.raise_for_status()
156124

157-
token_data = response.json()
158-
self._token = token_data["access_token"]
159-
expires_in = token_data.get(
160-
"expires_in", 3600
161-
) # Default 1 hour
162-
self._token_expires_at = time.time() + expires_in
125+
token_data = response.json()
126+
self._token = token_data["access_token"]
127+
expires_in = token_data.get("expires_in", 3600) # Default 1 hour
128+
self._token_expires_at = time.time() + expires_in
163129

164130
except httpx.HTTPStatusError as e:
165131
error_detail = ""
@@ -190,13 +156,59 @@ async def _refresh_token(self) -> None:
190156
msg = f"Unexpected error during OAuth: {e}"
191157
raise OAuthError(msg) from e
192158

193-
async def invalidate_token(self) -> None:
159+
def invalidate_token(self) -> None:
194160
"""Invalidate the current token to force a refresh on next use."""
195-
async with self._lock:
161+
with self._lock:
196162
self._token = None
197163
self._token_expires_at = None
198164

199165

166+
class _AutoRefreshTokenAuthMetadataPlugin(grpc.AuthMetadataPlugin):
167+
"""gRPC auth plugin that fetches a fresh token for every RPC.
168+
169+
The plugin delegates to ``CredentialHelper.get_token()`` which
170+
handles caching, expiry checks, and thread-safe refresh internally.
171+
This callback is invoked by gRPC on a *separate* thread, so the
172+
underlying ``CredentialHelper`` must use ``threading.Lock`` (not
173+
``asyncio.Lock``).
174+
"""
175+
176+
def __init__(self, credential_helper: CredentialHelper) -> None:
177+
"""Initialize with a credential helper.
178+
179+
Args:
180+
----
181+
credential_helper: The helper that manages token lifecycle
182+
183+
"""
184+
self._credential_helper: CredentialHelper = credential_helper
185+
186+
@override
187+
def __call__(
188+
self,
189+
_: grpc.AuthMetadataContext,
190+
callback: grpc.AuthMetadataPluginCallback,
191+
) -> None:
192+
"""Supply authorization metadata for an RPC.
193+
194+
Called by the gRPC runtime on a background thread before each
195+
RPC. On success the token is forwarded as a Bearer token; on
196+
failure the error is passed to the callback so gRPC can surface
197+
it as an RPC error.
198+
199+
Args:
200+
----
201+
callback: gRPC callback to receive metadata or an error
202+
203+
"""
204+
try:
205+
token = self._credential_helper.get_token()
206+
metadata = (("authorization", f"Bearer {token}"),)
207+
callback(metadata, None)
208+
except OAuthError as err:
209+
callback(None, err) # pyright: ignore[reportArgumentType]
210+
211+
200212
async def create_channel_with_credentials(
201213
host: str,
202214
credential_helper: CredentialHelper,
@@ -215,19 +227,23 @@ async def create_channel_with_credentials(
215227
Raises:
216228
------
217229
InvalidHostError: If host is empty
218-
OAuthError: If OAuth authentication fails
230+
231+
Note:
232+
----
233+
OAuth errors are no longer raised at channel-creation time.
234+
Instead, they surface as RPC errors when the per-request auth
235+
metadata plugin attempts to acquire a token.
219236
220237
"""
221238
if not host:
222239
raise InvalidHostError(InvalidHostError.default_message)
223240

224-
# Get a valid token from the credential helper
225-
token = await credential_helper.get_token()
226-
227-
# Create credentials with token authentication
241+
# Create credentials with per-RPC token refresh
228242
credentials = grpc.composite_channel_credentials(
229243
grpc.ssl_channel_credentials(),
230-
grpc.access_token_call_credentials(token),
244+
grpc.metadata_call_credentials(
245+
_AutoRefreshTokenAuthMetadataPlugin(credential_helper)
246+
),
231247
)
232248

233249
# Configure gRPC options for persistent connections

0 commit comments

Comments
 (0)