2424
2525FLYTE_DATABRICKS_AUTH_TYPE_ENV = "FLYTE_DATABRICKS_AUTH_TYPE"
2626FLYTE_DATABRICKS_OAUTH_SECRET_NAME_ENV = "FLYTE_DATABRICKS_OAUTH_SECRET_NAME"
27+ FLYTE_DATABRICKS_OIDC_TOKEN_FILE_ENV = "FLYTE_DATABRICKS_OIDC_TOKEN_FILE"
28+ FLYTE_DATABRICKS_OIDC_AUDIENCE_ENV = "FLYTE_DATABRICKS_OIDC_AUDIENCE"
2729DATABRICKS_CLIENT_ID_ENV = "DATABRICKS_CLIENT_ID"
2830DATABRICKS_CLIENT_SECRET_ENV = "DATABRICKS_CLIENT_SECRET"
31+ AWS_WEB_IDENTITY_TOKEN_FILE_ENV = "AWS_WEB_IDENTITY_TOKEN_FILE"
2932
3033DEFAULT_OAUTH_SECRET_NAME = "databricks-oauth"
34+ DEFAULT_OIDC_AUDIENCE = "databricks"
35+ DEFAULT_PROJECTED_SA_TOKEN_PATH = "/var/run/secrets/databricks/token"
3136TOKEN_REFRESH_BUFFER_SECONDS = 60
3237TOKEN_ENDPOINT_MAX_RETRIES = 3
3338TOKEN_ENDPOINT_BACKOFF_BASE_SECONDS = 0.2
34- VALID_AUTH_TYPES = {"pat" , "oauth_m2m" }
39+ VALID_AUTH_TYPES = {"pat" , "oauth_m2m" , "oidc_federation" }
3540
3641
3742class DatabricksAuthError (Exception ):
@@ -47,6 +52,8 @@ class _Settings:
4752 client_id : Optional [str ]
4853 oauth_secret_name : str
4954 token_secret_name : Optional [str ]
55+ oidc_token_file : Optional [str ]
56+ oidc_audience : str
5057 namespace : Optional [str ]
5158
5259 @staticmethod
@@ -74,10 +81,30 @@ def _pick(task_key: str, env_key: Optional[str], default: Optional[str] = None)
7481 )
7582 or DEFAULT_OAUTH_SECRET_NAME ,
7683 token_secret_name = custom .get ("databricksTokenSecret" ),
84+ oidc_token_file = _pick (
85+ "databricksOidcTokenFile" ,
86+ FLYTE_DATABRICKS_OIDC_TOKEN_FILE_ENV ,
87+ ),
88+ oidc_audience = _pick (
89+ "databricksOidcAudience" ,
90+ FLYTE_DATABRICKS_OIDC_AUDIENCE_ENV ,
91+ DEFAULT_OIDC_AUDIENCE ,
92+ )
93+ or DEFAULT_OIDC_AUDIENCE ,
7794 namespace = namespace ,
7895 )
7996
8097
98+ def _resolve_oidc_token_file (settings : _Settings ) -> Optional [str ]:
99+ """Resolve the first configured projected JWT file that exists."""
100+ candidates = (
101+ settings .oidc_token_file ,
102+ os .getenv (AWS_WEB_IDENTITY_TOKEN_FILE_ENV ),
103+ DEFAULT_PROJECTED_SA_TOKEN_PATH ,
104+ )
105+ return next ((path for path in candidates if path and os .path .exists (path )), None )
106+
107+
81108@dataclass
82109class _CachedToken :
83110 access_token : str
@@ -280,6 +307,74 @@ async def invalidate_cache(self) -> None:
280307 await _TOKEN_CACHE .invalidate ((self .workspace_url , client_id , self .settings .namespace or "_" ))
281308
282309
310+ class OIDCConnectorAuth (DatabricksAuth ):
311+ """Exchange the connector workload's projected JWT for a Databricks token."""
312+
313+ auth_type = "oidc_federation"
314+ strategy_name = "OIDCConnectorAuth"
315+
316+ def _client_id (self ) -> str :
317+ client_id = self .settings .client_id or os .getenv (DATABRICKS_CLIENT_ID_ENV )
318+ if not client_id :
319+ raise DatabricksAuthError (
320+ "OIDC federation requires a client ID. Configure databricks_client_id " "or DATABRICKS_CLIENT_ID."
321+ )
322+ return client_id
323+
324+ def _subject_token_file (self ) -> str :
325+ token_file = _resolve_oidc_token_file (self .settings )
326+ if not token_file :
327+ raise DatabricksAuthError (
328+ "OIDC federation requires a projected JWT file. Configure "
329+ "databricks_oidc_token_file, FLYTE_DATABRICKS_OIDC_TOKEN_FILE, "
330+ "or AWS_WEB_IDENTITY_TOKEN_FILE."
331+ )
332+ return token_file
333+
334+ def _cache_key (self , client_id : str ) -> Tuple [str , str , str ]:
335+ return (
336+ self .workspace_url ,
337+ client_id ,
338+ f"oidc:{ self .settings .oidc_audience } " ,
339+ )
340+
341+ async def get_bearer_token (self , session : "aiohttp.ClientSession" ) -> str : # type: ignore[name-defined]
342+ client_id = self ._client_id ()
343+ key = self ._cache_key (client_id )
344+ cached = await _TOKEN_CACHE .get (key )
345+ if cached :
346+ return cached
347+
348+ token_file = self ._subject_token_file ()
349+ try :
350+ with open (token_file ) as token_stream :
351+ subject_token = token_stream .read ().strip ()
352+ except OSError as error :
353+ raise DatabricksAuthError (f"Unable to read projected OIDC token file '{ token_file } ': { error } " ) from error
354+ if not subject_token :
355+ raise DatabricksAuthError (f"Projected OIDC token file '{ token_file } ' is empty" )
356+
357+ payload = await _post_token (
358+ session ,
359+ self .workspace_url ,
360+ {
361+ "grant_type" : "urn:ietf:params:oauth:grant-type:token-exchange" ,
362+ "scope" : "all-apis" ,
363+ "client_id" : client_id ,
364+ "subject_token" : subject_token ,
365+ "subject_token_type" : "urn:ietf:params:oauth:token-type:jwt" ,
366+ },
367+ )
368+ access_token = payload .get ("access_token" )
369+ if not access_token :
370+ raise DatabricksAuthError ("Databricks OIDC response did not contain access_token" )
371+ await _TOKEN_CACHE .put (key , access_token , int (payload .get ("expires_in" , 3600 )))
372+ return access_token
373+
374+ async def invalidate_cache (self ) -> None :
375+ await _TOKEN_CACHE .invalidate (self ._cache_key (self ._client_id ()))
376+
377+
283378async def select_auth (
284379 task_template : Optional [TaskTemplate ],
285380 workspace_url : str ,
@@ -294,6 +389,8 @@ async def select_auth(
294389 )
295390 if auth_type == "oauth_m2m" :
296391 return OAuthM2MAuth (workspace_url , settings )
392+ if auth_type == "oidc_federation" :
393+ return OIDCConnectorAuth (workspace_url , settings )
297394 return PATAuth (workspace_url , settings )
298395
299396
@@ -303,6 +400,8 @@ def build_auth(
303400 namespace : Optional [str ] = None ,
304401 client_id : Optional [str ] = None ,
305402 oauth_secret_name : Optional [str ] = None ,
403+ oidc_token_file : Optional [str ] = None ,
404+ oidc_audience : Optional [str ] = None ,
306405) -> DatabricksAuth :
307406 """Rebuild an auth strategy from persisted connector metadata."""
308407 settings = _Settings (
@@ -311,12 +410,16 @@ def build_auth(
311410 client_id = client_id ,
312411 oauth_secret_name = oauth_secret_name or DEFAULT_OAUTH_SECRET_NAME ,
313412 token_secret_name = None ,
413+ oidc_token_file = oidc_token_file ,
414+ oidc_audience = oidc_audience or DEFAULT_OIDC_AUDIENCE ,
314415 namespace = namespace ,
315416 )
316417 if auth_type == "oauth_m2m" :
317418 return OAuthM2MAuth (workspace_url , settings )
318419 if auth_type == "pat" :
319420 return PATAuth (workspace_url , settings )
421+ if auth_type == "oidc_federation" :
422+ return OIDCConnectorAuth (workspace_url , settings )
320423 raise DatabricksAuthError (
321424 f"Invalid Databricks auth type '{ auth_type } '. Expected one of { sorted (VALID_AUTH_TYPES )} ."
322425 )
0 commit comments