11import logging
2+ import os
23import typing
34
45import click
6+ import jwt
57from google .api_core .exceptions import NotFound
8+ from google .auth import default
9+ from google .auth .transport .requests import Request
610from google .cloud import secretmanager
11+ from google .oauth2 import id_token
712
813from flytekit .clients .auth .auth_client import AuthorizationClient
914from flytekit .clients .auth .authenticator import Authenticator
1015from flytekit .clients .auth .exceptions import AccessTokenNotFoundError
11- from flytekit .clients .auth .keyring import KeyringStore
16+ from flytekit .clients .auth .keyring import Credentials , KeyringStore
17+
18+ WEBAPP_CLIENT_ID_HELP = (
19+ "Webapp type OAuth 2.0 client ID used by the IAP. "
20+ "Typically in the form of `<xyz>.apps.googleusercontent.com`. "
21+ "Created when activating IAP for the Flyte deployment. "
22+ "https://cloud.google.com/iap/docs/enabling-kubernetes-howto#oauth-credentials"
23+ )
1224
1325
1426class GCPIdentityAwareProxyAuthenticator (Authenticator ):
@@ -131,11 +143,7 @@ def cli():
131143 type = str ,
132144 default = None ,
133145 required = True ,
134- help = (
135- "Webapp type OAuth 2.0 client ID. Typically in the form of `<xyz>.apps.googleusercontent.com`. "
136- "Created when activating IAP for the Flyte deployment. "
137- "https://cloud.google.com/iap/docs/enabling-kubernetes-howto#oauth-credentials"
138- ),
146+ help = WEBAPP_CLIENT_ID_HELP ,
139147)
140148@click .option (
141149 "--project" ,
@@ -147,7 +155,7 @@ def cli():
147155def generate_user_id_token (
148156 desktop_client_id : str , desktop_client_secret_gcp_secret_name : str , webapp_client_id : str , project : str
149157):
150- """Generate a user account ID token for proxy-authentication/ authorization with GCP Identity Aware Proxy."""
158+ """Generate a user account ID token for proxy-authorization with GCP Identity Aware Proxy."""
151159 desktop_client_secret = get_gcp_secret_manager_secret (project , desktop_client_secret_gcp_secret_name )
152160
153161 iap_authenticator = GCPIdentityAwareProxyAuthenticator (
@@ -163,5 +171,76 @@ def generate_user_id_token(
163171 click .echo (iap_authenticator .get_credentials ().id_token )
164172
165173
174+ def get_service_account_id_token (audience : str ) -> str :
175+ """Fetch an ID Token for the service account used by the current environment.
176+
177+ Uses flytekit's KeyringStore to cache the ID token.
178+
179+ This function acquires ID token from the environment in the following order.
180+ See https://google.aip.dev/auth/4110.
181+
182+ 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
183+ to the path of a valid service account JSON file, then ID token is
184+ acquired using this service account credentials.
185+ 2. If the application is running in Compute Engine, App Engine or Cloud Run,
186+ then the ID token are obtained from the metadata server.
187+
188+ Args:
189+ audience (str): The audience that this ID token is intended for.
190+ """
191+ credentials , _ = default ()
192+ # Flytekit's KeyringStore, by default, uses the endpoint as the key to store the credentials
193+ # We use the audience and the service account email as the key
194+ audience_and_account_key = audience + "-" + credentials .service_account_email
195+ creds = KeyringStore .retrieve (audience_and_account_key )
196+ if creds :
197+ is_expired = False
198+ try :
199+ exp_margin = - 300 # Generate a new token if it expires in less than 5 minutes
200+ jwt .decode (
201+ creds .id_token .encode ("utf-8" ),
202+ options = {"verify_signature" : False , "verify_exp" : True },
203+ leeway = exp_margin ,
204+ )
205+ except jwt .ExpiredSignatureError :
206+ is_expired = True
207+
208+ if not is_expired :
209+ return creds .id_token
210+
211+ token = id_token .fetch_id_token (Request (), audience )
212+
213+ KeyringStore .store (Credentials (for_endpoint = audience_and_account_key , access_token = "" , id_token = token ))
214+ return token
215+
216+
217+ @cli .command ()
218+ @click .option (
219+ "--webapp_client_id" ,
220+ type = str ,
221+ default = None ,
222+ required = True ,
223+ help = WEBAPP_CLIENT_ID_HELP ,
224+ )
225+ @click .option (
226+ "--service_account_key" ,
227+ type = click .Path (exists = True , dir_okay = False ),
228+ default = None ,
229+ required = False ,
230+ help = (
231+ "Path to a service account key file. Alternatively set the environment variable "
232+ "`GOOGLE_APPLICATION_CREDENTIALS` to the path of the service account key file. "
233+ "If not provided and in Compute Engine, App Engine, or Cloud Run, will retrieve "
234+ "the ID token from the metadata server."
235+ ),
236+ )
237+ def generate_service_account_id_token (webapp_client_id : str , service_account_key : str ):
238+ """Generate a service account ID token for proxy-authorization with GCP Identity Aware Proxy."""
239+ if service_account_key :
240+ os .environ ["GOOGLE_APPLICATION_CREDENTIALS" ] = service_account_key
241+ token = get_service_account_id_token (webapp_client_id )
242+ click .echo (token )
243+
244+
166245if __name__ == "__main__" :
167246 cli ()
0 commit comments