|
22 | 22 | import base64 |
23 | 23 | import datetime |
24 | 24 | import logging |
| 25 | +import threading |
25 | 26 | from collections import namedtuple |
26 | 27 | from typing import Any, Callable, MutableMapping, Optional, Sequence |
27 | 28 |
|
@@ -261,6 +262,7 @@ def __init__( |
261 | 262 |
|
262 | 263 | self.token: Optional[str] = None |
263 | 264 | self.expiry: Optional[datetime.datetime] = None |
| 265 | + self._lock = threading.Lock() |
264 | 266 |
|
265 | 267 | def init_channel( |
266 | 268 | self, |
@@ -302,66 +304,81 @@ def create_async_auth_interceptor( |
302 | 304 | """Creates async gRPC interceptors that attach the Bearer token.""" |
303 | 305 | return self.create_async_auth_interceptors() |
304 | 306 |
|
305 | | - def refresh(self, request: Any = None) -> None: |
| 307 | + def _perform_refresh_token(self, request: Any = None) -> None: |
306 | 308 | """Refreshes the access token by performing the OPAQUE login flow with Spanner Omni. |
307 | 309 |
|
308 | 310 | Args: |
309 | 311 | request (Any, optional): Unused; part of google.auth.credentials.Credentials interface. |
310 | 312 | """ |
311 | | - login_channel = None |
312 | | - try: |
313 | | - if self.use_plain_text: |
314 | | - login_channel = grpc.insecure_channel(self.target) |
315 | | - elif self.ssl_credentials is not None: |
316 | | - login_channel = grpc.secure_channel(self.target, self.ssl_credentials) |
317 | | - elif self.ca_certificate: |
318 | | - with open(self.ca_certificate, "rb") as f: |
319 | | - ca_cert = f.read() |
320 | | - if self.client_certificate and self.client_key: |
321 | | - with open(self.client_certificate, "rb") as f: |
322 | | - client_cert = f.read() |
323 | | - with open(self.client_key, "rb") as f: |
324 | | - private_key = f.read() |
325 | | - ssl_creds = grpc.ssl_channel_credentials( |
326 | | - root_certificates=ca_cert, |
327 | | - private_key=private_key, |
328 | | - certificate_chain=client_cert, |
| 313 | + with self._lock: |
| 314 | + if self.valid: |
| 315 | + return |
| 316 | + login_channel = None |
| 317 | + try: |
| 318 | + if self.use_plain_text: |
| 319 | + login_channel = grpc.insecure_channel(self.target) |
| 320 | + elif self.ssl_credentials is not None: |
| 321 | + login_channel = grpc.secure_channel( |
| 322 | + self.target, self.ssl_credentials |
329 | 323 | ) |
330 | | - elif self.client_certificate or self.client_key: |
331 | | - raise ValueError( |
332 | | - "Both client_certificate and client_key must be provided for mTLS" |
| 324 | + elif self.ca_certificate: |
| 325 | + with open(self.ca_certificate, "rb") as f: |
| 326 | + ca_cert = f.read() |
| 327 | + if self.client_certificate and self.client_key: |
| 328 | + with open(self.client_certificate, "rb") as f: |
| 329 | + client_cert = f.read() |
| 330 | + with open(self.client_key, "rb") as f: |
| 331 | + private_key = f.read() |
| 332 | + ssl_creds = grpc.ssl_channel_credentials( |
| 333 | + root_certificates=ca_cert, |
| 334 | + private_key=private_key, |
| 335 | + certificate_chain=client_cert, |
| 336 | + ) |
| 337 | + elif self.client_certificate or self.client_key: |
| 338 | + raise ValueError( |
| 339 | + "Both client_certificate and client_key must be provided for mTLS" |
| 340 | + ) |
| 341 | + else: |
| 342 | + ssl_creds = grpc.ssl_channel_credentials( |
| 343 | + root_certificates=ca_cert |
| 344 | + ) |
| 345 | + login_channel = grpc.secure_channel(self.target, ssl_creds) |
| 346 | + else: |
| 347 | + login_channel = grpc.secure_channel( |
| 348 | + self.target, grpc.ssl_channel_credentials() |
333 | 349 | ) |
| 350 | + |
| 351 | + client = LoginClient(login_channel) |
| 352 | + proto_token = client.login(self.username, self._password) |
| 353 | + |
| 354 | + token_bytes = proto_token.SerializeToString() |
| 355 | + self.token = base64.b64encode(token_bytes).decode("ascii") |
| 356 | + |
| 357 | + if proto_token.HasField("expiration_time"): |
| 358 | + seconds = proto_token.expiration_time.seconds |
| 359 | + nanos = proto_token.expiration_time.nanos |
| 360 | + self.expiry = datetime.datetime.fromtimestamp( |
| 361 | + seconds + nanos / 1e9, tz=datetime.timezone.utc |
| 362 | + ).replace(tzinfo=None) |
334 | 363 | else: |
335 | | - ssl_creds = grpc.ssl_channel_credentials(root_certificates=ca_cert) |
336 | | - login_channel = grpc.secure_channel(self.target, ssl_creds) |
337 | | - else: |
338 | | - login_channel = grpc.secure_channel( |
339 | | - self.target, grpc.ssl_channel_credentials() |
340 | | - ) |
341 | | - |
342 | | - client = LoginClient(login_channel) |
343 | | - proto_token = client.login(self.username, self._password) |
344 | | - |
345 | | - token_bytes = proto_token.SerializeToString() |
346 | | - self.token = base64.b64encode(token_bytes).decode("ascii") |
347 | | - |
348 | | - if proto_token.HasField("expiration_time"): |
349 | | - seconds = proto_token.expiration_time.seconds |
350 | | - nanos = proto_token.expiration_time.nanos |
351 | | - self.expiry = datetime.datetime.fromtimestamp( |
352 | | - seconds + nanos / 1e9, tz=datetime.timezone.utc |
353 | | - ).replace(tzinfo=None) |
354 | | - else: |
355 | | - self.expiry = datetime.datetime.now(datetime.timezone.utc).replace( |
356 | | - tzinfo=None |
357 | | - ) + datetime.timedelta(hours=1) |
358 | | - except Exception as e: |
359 | | - raise google.auth.exceptions.RefreshError( |
360 | | - f"Failed to login to Spanner Omni: {e}" |
361 | | - ) from e |
362 | | - finally: |
363 | | - if login_channel is not None: |
364 | | - login_channel.close() |
| 364 | + self.expiry = datetime.datetime.now(datetime.timezone.utc).replace( |
| 365 | + tzinfo=None |
| 366 | + ) + datetime.timedelta(hours=1) |
| 367 | + except Exception as e: |
| 368 | + raise google.auth.exceptions.RefreshError( |
| 369 | + f"Failed to login to Spanner Omni: {e}" |
| 370 | + ) from e |
| 371 | + finally: |
| 372 | + if login_channel is not None: |
| 373 | + login_channel.close() |
| 374 | + |
| 375 | + def refresh(self, request: Any = None) -> None: |
| 376 | + """Refreshes the access token. |
| 377 | +
|
| 378 | + Args: |
| 379 | + request (Any, optional): Unused; part of google.auth.credentials.Credentials interface. |
| 380 | + """ |
| 381 | + self._perform_refresh_token(request) |
365 | 382 |
|
366 | 383 | def apply( |
367 | 384 | self, headers: MutableMapping[str, str], token: Optional[str] = None |
|
0 commit comments