Authentication Mechanisms Explained
The authentication path to a remote backend
When flyte-sdk creates a remote client, authentication configuration and token acquisition are deliberately separate from the first authenticated RPC. _initialize_client() forwards auth_type, endpoint, client credentials, ClientConfig, TLS settings, and the HTTP proxy URL to ClientSet.for_endpoint(). The channel setup first has an unauthenticated gRPC channel available for metadata discovery. RemoteClientConfigStore.get_client_config() uses AuthMetadataServiceStub to request OAuth metadata and public-client configuration concurrently, then combines them into a ClientConfig.
ClientConfig contains the values consumed by all three flows:
token_endpointandauthorization_endpointare required.redirect_uriandclient_idare required by the model and are used by browser and device flows.device_authorization_endpointis optional and must be present for device authentication.scopesandaudienceare optional.header_keydefaults to"authorization", but the backend can supply a deployment-specificauthorization_metadata_key.
A local configuration can override the remote configuration. Authenticator._resolve_config() fetches the remote value once and calls remote_config.with_override(self._client_config) when a local ClientConfig was supplied. The override uses truthiness checks, so empty strings and empty scope lists do not replace non-empty remote values. Also, ClientConfig is not a partial model: its required fields must still be supplied when constructing one.
The factory maps the public authentication mode strings to concrete authenticators:
match auth_type:
case "Pkce":
from flyte.remote._client.auth._authenticators.pkce import PKCEAuthenticator
return PKCEAuthenticator(endpoint=endpoint, cfg_store=cfg_store, verify=verify, **kwargs)
case "ClientSecret":
from flyte.remote._client.auth._authenticators.client_credentials import ClientCredentialsAuthenticator
return ClientCredentialsAuthenticator(endpoint=endpoint, cfg_store=cfg_store, verify=verify, **kwargs)
case "DeviceFlow":
from flyte.remote._client.auth._authenticators.device_code import DeviceCodeAuthenticator
return DeviceCodeAuthenticator(endpoint=endpoint, cfg_store=cfg_store, verify=verify, **kwargs)
case _:
raise ValueError(
f"Invalid auth mode [{auth_type}] specified. Please update the creds config to use a valid value"
)
The supported public AuthType literal also includes "ExternalCommand"; that mode is dispatched to AsyncCommandAuthenticator by the same factory. The mechanisms below cover the PKCE, device-code, and client-credentials authenticators described here.
Shared credential lifecycle
Authenticator is the asynchronous base class. Its constructor records the endpoint, accepts an injected ClientConfigStore, optional local configuration and credentials, and either reuses an httpx.AsyncClient or creates one. If credentials are not passed explicitly, it attempts KeyringStore.retrieve(endpoint).
Authentication is lazy. AsyncAuthenticationHTTPAdapter.add_auth_header() refreshes when no credentials are available, obtains GrpcAuthMetadata, and copies each metadata pair into the HTTP request. The resulting value is Bearer <access_token>. AsyncAuthenticatedClient.send() then retries the request once after an HTTP 401:
creds_id = await self.auth_adapter.add_auth_header(request)
response = await super().send(request, **kwargs)
if response.status_code == HTTPStatus.UNAUTHORIZED:
await self.authenticator.refresh_credentials(creds_id=creds_id)
await self.auth_adapter.add_auth_header(request)
response = await super().send(request, **kwargs)
return response
The gRPC interceptors use the same metadata and refresh model for unauthenticated RPC failures. There is one retry, not an unbounded retry loop.
refresh_credentials() delegates the actual OAuth operation to the subclass's _do_refresh_credentials(). It uses the credential id as a change marker and an asyncio lock to coordinate concurrent callers. A caller that observed an older ID returns if another caller has already replaced the credentials; the check is repeated after acquiring the lock. Successful credentials are stored through KeyringStore, and any exception causes cached credentials for the endpoint to be deleted before the exception is re-raised.
Credentials derives its ID from the access token rather than treating the ID as another secret:
@pydantic.field_validator("for_endpoint", mode="after")
@classmethod
def validate_endpoint(cls, v: str) -> str:
return strip_scheme(v)
@pydantic.model_validator(mode="after")
def compute_id(self) -> "Credentials":
if self.access_token:
self.id = hashlib.md5(self.access_token.encode()).hexdigest()
return self
The endpoint is normalized for keyring use (https://foo.com becomes foo.com, and dns:///foo.com becomes foo.com). KeyringStore stores access_token and, when present, refresh_token under that normalized endpoint. Keyring failures are logged at debug level and do not make authentication fail, so a successful login may not be cached on a machine without a usable keyring backend.
Choosing a flow
| Mode | Authenticator | Interaction | Credential source and refresh behavior |
|---|---|---|---|
Pkce | PKCEAuthenticator | Opens a browser and receives a localhost callback | Uses a cached refresh token when available; otherwise starts authorization-code login |
DeviceFlow | DeviceCodeAuthenticator | Displays a verification URL and user code | Uses a cached refresh token when available; otherwise requests a device code and polls |
ClientSecret | ClientCredentialsAuthenticator | No user interaction | Sends client ID and secret to the token endpoint using HTTP Basic authentication |
The public initialization entry point defaults auth_type to "Pkce". For example, endpoint initialization forwards the selected mode and its related settings without constructing a concrete authenticator directly:
async def _initialize_client(
api_key: str | None = None,
auth_type: AuthType = "Pkce",
endpoint: str | None = None,
client_config: ClientConfig | None = None,
headless: bool = False,
insecure: bool = False,
insecure_skip_verify: bool = False,
ca_cert_file_path: str | None = None,
command: List[str] | None = None,
proxy_command: List[str] | None = None,
client_id: str | None = None,
client_credentials_secret: str | None = None,
rpc_retries: int = 3,
http_proxy_url: str | None = None,
) -> ClientSet:
if endpoint:
return await ClientSet.for_endpoint(
endpoint,
insecure=insecure,
insecure_skip_verify=insecure_skip_verify,
auth_type=auth_type,
headless=headless,
ca_cert_file_path=ca_cert_file_path,
command=command,
proxy_command=proxy_command,
client_id=client_id,
client_credentials_secret=client_credentials_secret,
client_config=client_config,
rpc_retries=rpc_retries,
http_proxy_url=http_proxy_url,
)
PKCE: browser-based authorization code flow
PKCEAuthenticator lazily creates an AuthorizationClient. _initialize_auth_client() generates a code verifier and an S256 code challenge, resolves the backend configuration, and passes the verifier only as an access-token request parameter:
async def _initialize_auth_client(self):
if not self._auth_client:
code_verifier = await _generate_code_verifier()
code_challenge = await _create_code_challenge(code_verifier)
cfg = await self._resolve_config()
self._auth_client = AuthorizationClient(
endpoint=self._endpoint,
redirect_uri=cfg.redirect_uri,
client_id=cfg.client_id,
audience=cfg.audience,
scopes=cfg.scopes,
auth_endpoint=cfg.authorization_endpoint,
token_endpoint=cfg.token_endpoint,
verify=self._verify,
http_session=self._http_session,
request_auth_code_params={
"code_challenge": code_challenge,
"code_challenge_method": "S256",
},
request_access_token_params={"code_verifier": code_verifier},
refresh_access_token_params={},
add_request_auth_code_params_to_request_access_token_params=True,
)
The AuthorizationClient creates an asyncio TCP server at the hostname and port parsed from redirect_uri. It opens the authorization URL in a new browser tab; if that fails, it prints the URL instead. The URL includes response_type=code, the configured client ID, space-separated scopes, redirect URI, a generated state value, the optional audience, and the PKCE challenge parameters.
OAuthCallbackHandler accepts only the configured callback path. It extracts code and state from the query string. AuthorizationClient._request_access_token() compares the returned state with the originally generated state and raises ValueError on a mismatch. It posts the authorization code with grant_type=authorization_code, requires an HTTP 200 response, and requires an access_token in the JSON response. refresh_token and expires_in are copied when supplied.
The refresh sequence is explicit:
await self._initialize_auth_client()
if self._creds:
try:
return await self._auth_client.refresh_access_token(self._creds)
except AccessTokenNotFoundError:
logger.warning("Logging in...")
return await self._auth_client.get_creds_from_remote()
refresh_access_token() raises AccessTokenNotFoundError if there is no refresh token or if the refresh endpoint is non-200, so those cases fall back to browser login. Other exceptions from the refresh operation are not caught by PKCEAuthenticator and therefore propagate. The full browser result is cached by AuthorizationClient for up to 60 seconds while concurrent callers coordinate through its thread lock.
The redirect URI must therefore identify an available local port and the callback path expected by the server. For Auth0 deployments, the PKCE class's configuration note calls out scopes such as offline_access, offline, all, and openid when refresh-token caching is required; without an appropriate scope configuration, the response may contain only an access token.
Device flow: user authorization without a callback browser
DeviceCodeAuthenticator is intended for headless user authentication. It first resolves configuration and checks device_authorization_endpoint. If the backend does not advertise that endpoint, it raises:
raise AuthenticationError(
"Device Authentication is not available on the Flyte backend / authentication server"
)
If cached credentials contain a refresh token, the authenticator first requests a refresh-token grant using the configured token endpoint, client ID, audience, and scopes. It catches AuthenticationError and AuthenticationPending, logs "Logging in...", and then proceeds to a new device login. The full branch requests a device code, prints a URL formed from verification_uri and user_code, and calls token_client.poll_token_endpoint() with the device response.
The polling helper handles the provider's pending responses. In flyte-sdk, authorization_pending and slow_down are represented by AuthenticationPending; the polling implementation waits the originally advertised interval even for slow_down rather than dynamically increasing it. Device authentication consequently remains blocked until the user completes authorization or the device code expires.
Client credentials: service identity
ClientCredentialsAuthenticator is the non-interactive option for SDK or service use. Its constructor requires both values immediately:
if not client_id or not client_credentials_secret:
raise ValueError("both client_id and client_credentials_secret are required.")
self._client_id = client_id
self._client_credentials_secret = client_credentials_secret
super().__init__(**kwargs)
Unlike PKCE, the client ID for this flow is explicitly supplied to the authenticator; the implementation does not take it from the backend's public client configuration. _do_refresh_credentials() resolves the token endpoint and settings, creates an HTTP Basic authorization header, and calls token_client.get_token() with the configured scopes, audience, proxy, TLS verification, and shared HTTP session:
cfg = await self._resolve_config()
authorization_header = token_client.get_basic_authorization_header(
self._client_id, self._client_credentials_secret
)
token, refresh_token, expires_in = await token_client.get_token(
token_endpoint=cfg.token_endpoint,
authorization_header=authorization_header,
http_proxy_url=self._http_proxy_url,
verify=self._verify,
scopes=cfg.scopes,
audience=cfg.audience,
http_session=self._http_session,
)
return Credentials(
for_endpoint=self._endpoint,
access_token=token,
refresh_token=refresh_token,
expires_in=expires_in,
)
The token client uses its default grant for this request, which is the client-credentials grant. The surrounding implementation does not assume that a refresh token is available for this mode; the flow should be treated as acquiring an access token with the service identity rather than as an interactive refresh-token flow.
Configuration and transport constraints
The mode and credentials reach the factory through initialization settings commonly represented by admin.authType, admin.clientId, and the client-credentials secret settings. A client-credentials secret can be read from admin.clientSecretLocation or admin.clientSecretEnvVar; the configuration layer strips a trailing newline from a file value and automatically selects ClientSecret when it finds a secret. The configuration comments warn that environment-based secret handling is less secure than mounting a file.
admin.scopes is passed to token, authorization, and device requests. audience is passed when present and is especially relevant to Auth0-compatible configurations. authorization_endpoint, token_endpoint, and device_authorization_endpoint come from the remote AuthMetadataService unless a local ClientConfig override supplies truthy replacements. header_key controls the bearer metadata key, while the bearer value remains Bearer <access_token>.
TLS and proxy settings are shared with the authentication HTTP session. Authenticator accepts verify, ca_cert_path, and http_proxy_url; the factory sets verify=False for insecure_skip_verify, and uses verification when a CA path is supplied. The authorization client documents that disabling certificate verification accepts invalid certificates and is vulnerable to man-in-the-middle attacks. Keep verification enabled outside controlled development or testing scenarios.
Operational failure modes
The authentication error classes are lightweight runtime exceptions with distinct roles:
AccessTokenNotFoundErrormarks a missing or unusable PKCE refresh-token path and causes interactive login fallback.AuthenticationPendingrepresents a device authorization response that is not complete yet.AuthenticationErrorrepresents general authentication failures, including unavailable device authentication.
Direct construction of a concrete authenticator also requires a configuration path: if no resolved configuration exists and no ClientConfigStore was supplied, _resolve_config() raises ValueError("ClientConfigStore is not set. Cannot resolve configuration.").
Finally, keyring caching is opportunistic rather than required. KeyringStore normalizes schemes before lookup, but endpoint host/path differences can still produce separate service names. A failed refresh deletes the normalized endpoint's cached access and refresh entries. The credential ID is an internal change detector for concurrent refreshes, not a protection mechanism for the token itself.