1717
1818import aiohttp
1919import google .auth
20- from google .auth .credentials import Credentials
20+ from google .auth .credentials import Credentials , Scoped
2121import google .auth .transport .requests
22- from typing import Any , Dict
22+ from typing import Any , Dict , List
2323import datetime
24+ import copy
2425import asyncio
2526import logging
2627
@@ -166,7 +167,7 @@ async def _get_ephemeral(
166167 elif not isinstance (pub_key , str ):
167168 raise TypeError (f"pub_key must be of type str, got { type (pub_key )} " )
168169
169- if not credentials .valid or enable_iam_auth :
170+ if not credentials .valid :
170171 request = google .auth .transport .requests .Request ()
171172 credentials .refresh (request )
172173
@@ -181,7 +182,9 @@ async def _get_ephemeral(
181182 data = {"public_key" : pub_key }
182183
183184 if enable_iam_auth :
184- data ["access_token" ] = credentials .token
185+ # down-scope credentials with only IAM login scope (refreshes them too)
186+ login_creds = _downscope_credentials (credentials )
187+ data ["access_token" ] = login_creds .token
185188
186189 resp = await client_session .post (
187190 url , headers = headers , json = data , raise_for_status = True
@@ -229,3 +232,38 @@ async def _is_valid(task: asyncio.Task) -> bool:
229232 # supress any errors from task
230233 logger .debug ("Current instance metadata is invalid." )
231234 return False
235+
236+
237+ def _downscope_credentials (
238+ credentials : Credentials ,
239+ scopes : List [str ] = ["https://www.googleapis.com/auth/sqlservice.login" ],
240+ ) -> Credentials :
241+ """Generate a down-scoped credential.
242+
243+ :type credentials: google.auth.credentials.Credentials
244+ :param credentials
245+ Credentials object used to generate down-scoped credentials.
246+
247+ :type scopes: List[str]
248+ :param scopes
249+ List of Google scopes to include in down-scoped credentials object.
250+
251+ :rtype: google.auth.credentials.Credentials
252+ :returns: Down-scoped credentials object.
253+ """
254+ # credentials sourced from a service account or metadata are children of
255+ # Scoped class and are capable of being re-scoped
256+ if isinstance (credentials , Scoped ):
257+ scoped_creds = credentials .with_scopes (scopes = scopes )
258+ # authenticated user credentials can not be re-scoped
259+ else :
260+ # create shallow copy to not overwrite scopes on default credentials
261+ scoped_creds = copy .copy (credentials )
262+ # overwrite '_scopes' to down-scope user credentials
263+ # Cloud SDK reference: https://github.com/google-cloud-sdk-unofficial/google-cloud-sdk/blob/93920ccb6d2cce0fe6d1ce841e9e33410551d66b/lib/googlecloudsdk/command_lib/sql/generate_login_token_util.py#L116
264+ scoped_creds ._scopes = scopes
265+ # down-scoped credentials require refresh, are invalid after being re-scoped
266+ if not scoped_creds .valid :
267+ request = google .auth .transport .requests .Request ()
268+ scoped_creds .refresh (request )
269+ return scoped_creds
0 commit comments