Skip to content

Commit 336aebc

Browse files
committed
Implement subcommand to generate id tokens for service accounts
Signed-off-by: Fabio Graetz <fabiograetz@googlemail.com>
1 parent c677ff3 commit 336aebc

File tree

2 files changed

+87
-8
lines changed
  • flytekit/clients/auth
  • plugins/flytekit-identity-aware-proxy/flytekitplugins/identity_aware_proxy

2 files changed

+87
-8
lines changed

flytekit/clients/auth/keyring.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def retrieve(for_endpoint: str) -> typing.Optional[Credentials]:
6262
logging.debug(f"KeyRing not available, tokens will not be cached. Error: {e}")
6363
return None
6464

65-
if not access_token:
65+
if not access_token and not id_token:
6666
return None
6767
return Credentials(access_token, refresh_token, for_endpoint, id_token=id_token)
6868

plugins/flytekit-identity-aware-proxy/flytekitplugins/identity_aware_proxy/cli.py

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import logging
2+
import os
23
import typing
34

45
import click
6+
import jwt
57
from google.api_core.exceptions import NotFound
8+
from google.auth import default
9+
from google.auth.transport.requests import Request
610
from google.cloud import secretmanager
11+
from google.oauth2 import id_token
712

813
from flytekit.clients.auth.auth_client import AuthorizationClient
914
from flytekit.clients.auth.authenticator import Authenticator
1015
from 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

1426
class 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():
147155
def 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+
166245
if __name__ == "__main__":
167246
cli()

0 commit comments

Comments
 (0)