Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for and id_token result using a service account #840

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions auth/gcloud/aio/auth/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def get_service_data(
@dataclass
class TokenResponse:
value: str
expires_in: int
expires_in: int # Token TTL in seconds


class BaseToken:
Expand All @@ -160,7 +160,20 @@ class BaseToken:
def __init__(
self, service_file: Optional[Union[str, IO[AnyStr]]] = None,
session: Optional[Session] = None,
cache_preempt_after: float = 0.5,
cache_refresh_after: float = 0.95,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think "cache" is the wrong word here. What do you think about background_refresh_after and force_refresh_after, maybe?

Also occurs to me that in the gcloud-rest version of this library, there is no background/preempt version, so whichever is the lower of these values just becomes the forced/immediate refresh. So maybe it'd be even better to name accordingly: async_refresh_after and sync_refresh_after.

Thoughts? Trying to make usage as clear as possible in both cases, but I'm kind of blanking on wording which makes sense for both flows.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the first suggestion, and specifically background_refresh_after is spot on imo. However, in the gcloud-rest case, it make zero sense.
Having said that, having two thresholds doesn't make any sense in that case altogether, since it's whichever is first forces a blocking refresh. No idea how to name intricate details of a mechanism that is pure obfuscation in gcloud-rest.

) -> None:
if cache_preempt_after <= 0 or cache_preempt_after > 1:
raise ValueError(
'cache_preempt_after must be a value between 0 and 1')
if cache_refresh_after <= 0 or cache_refresh_after > 1:
raise ValueError(
'cache_refresh_after must be a value between 0 and 1')
# Portion of TTL after which a background refresh would start
self.cache_preempt_after = cache_preempt_after
# Portion of TTL after which a cached token is considered invalid
self.cache_refresh_after = cache_refresh_after

self.service_data = get_service_data(service_file)
if self.service_data:
self.token_type = Type(self.service_data['type'])
Expand All @@ -178,8 +191,12 @@ def __init__(
self.access_token: Optional[str] = None
self.access_token_duration = 0
self.access_token_acquired_at = datetime.datetime(1970, 1, 1)
# Timestamp after which we pre-fetch.
self.access_token_preempt_after = 0
# Timestamp after which we must re-fetch.
self.access_token_refresh_after = 0

self.acquiring: Optional['asyncio.Future[Any]'] = None
self.acquiring: Optional['asyncio.Task[None]'] = None

async def get_project(self) -> Optional[str]:
project = (
Expand Down Expand Up @@ -212,18 +229,27 @@ async def get(self) -> Optional[str]:
return self.access_token

async def ensure_token(self) -> None:
if self.acquiring and not self.acquiring.done():
await self.acquiring
return

if self.access_token:
now = datetime.datetime.now(datetime.timezone.utc)
delta = (now - self.access_token_acquired_at).total_seconds()
if delta <= self.access_token_duration / 2:
# Cached token exists
now_ts = int(
datetime.datetime.now(
datetime.timezone.utc).timestamp())
if now_ts > self.access_token_refresh_after:
# Cached token does not have enough duration left, fall through
pass
elif now_ts > self.access_token_preempt_after:
# Token is okay, but we need to fire up a preemptive refresh
if not self.acquiring:
self.acquiring = asyncio.create_task( # pylint: disable=possibly-used-before-assignment
self.acquire_access_token())
return
else:
# Cached token is valid for use
return

self.acquiring = asyncio.ensure_future( # pylint: disable=possibly-used-before-assignment
self.acquire_access_token())
if not self.acquiring:
self.acquiring = asyncio.create_task( # pylint: disable=possibly-used-before-assignment
self.acquire_access_token())
await self.acquiring

@abstractmethod
Expand All @@ -238,6 +264,11 @@ async def acquire_access_token(self, timeout: int = 10) -> None:
self.access_token_duration = resp.expires_in
self.access_token_acquired_at = datetime.datetime.now(
datetime.timezone.utc)
base_timstamp = self.access_token_acquired_at.timestamp()
self.access_token_preempt_after = int(
base_timstamp + (resp.expires_in * self.cache_preempt_after))
self.access_token_refresh_after = int(
base_timstamp + (resp.expires_in * self.cache_refresh_after))
alexdialpad marked this conversation as resolved.
Show resolved Hide resolved
self.acquiring = None

async def close(self) -> None:
Expand Down Expand Up @@ -324,8 +355,9 @@ async def _refresh_service_account(self, timeout: int) -> TokenResponse:
timeout=timeout,
)
content = await resp.json()
return TokenResponse(value=str(content['access_token']),
expires_in=int(content['expires_in']))
token = str(content.get('access_token') or content.get('id_token'))
alexdialpad marked this conversation as resolved.
Show resolved Hide resolved
expires = int(content.get('expires_in', '0')) or self.default_token_ttl
return TokenResponse(value=token, expires_in=expires)

async def _impersonate(self, token: TokenResponse,
*, timeout: int) -> TokenResponse:
Expand Down
6 changes: 6 additions & 0 deletions auth/tests/integration/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async def test_token_is_created(creds: str) -> None:
assert result
assert token.access_token is not None
assert token.access_token_duration != 0
assert token.access_token_preempt_after != 0
assert token.access_token_refresh_after != 0
assert token.access_token_acquired_at != datetime.datetime(1970, 1, 1)


Expand All @@ -41,6 +43,8 @@ async def test_token_does_not_require_session(creds: str) -> None:
assert result
assert token.access_token is not None
assert token.access_token_duration != 0
assert token.access_token_preempt_after != 0
assert token.access_token_refresh_after != 0
assert token.access_token_acquired_at != datetime.datetime(1970, 1, 1)


Expand All @@ -54,6 +58,8 @@ async def test_token_does_not_require_creds() -> None:
assert result
assert token.access_token is not None
assert token.access_token_duration != 0
assert token.access_token_preempt_after != 0
assert token.access_token_refresh_after != 0
assert token.access_token_acquired_at != datetime.datetime(1970, 1, 1)


Expand Down
2 changes: 2 additions & 0 deletions bin/build-rest
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ find . -type f \
| xargs -L1 $SED -Ei 's/AioSession/SyncSession/g'
find . -type f \
| xargs -L1 $SED -Ei 's/asyncio.ensure_future(.*)/\1/g'
find . -type f \
| xargs -L1 $SED -Ei 's/asyncio.create_task(.*)/\1/g'
find . -type f -path '*py' \
| xargs -L1 $SED -Ei 's/(async|await) //g'
find . -type f -path '*tests/*' \
Expand Down