Skip to content

Let clients run the device flow against the OIDC provider and send ID tokens - #8080

Open
santhosh-shanmugham wants to merge 2 commits into
flyteorg:masterfrom
santhosh-shanmugham:sshanmugham/client-idtoken-device-flow
Open

santhosh-shanmugham wants to merge 2 commits into
flyteorg:masterfrom
santhosh-shanmugham:sshanmugham/client-idtoken-device-flow

Conversation

@santhosh-shanmugham

@santhosh-shanmugham santhosh-shanmugham commented Sep 25, 2026 •

Copy link
Copy Markdown

Tracking issue

Related to the recurring question of running flytectl device flow with Flyte's built-in authorization server, for example How do I setup device code authentication on the community forum.

Why are the changes needed?

Flyte's built-in authorization server (authServerType: Self) does not implement the device authorization grant (RFC 8628), so flytectl cannot log a user in on a host without a browser unless the deployment moves to an external authorization server. That is a large change for a deployment that only needs headless CLI login.

flyteadmin already accepts OIDC ID tokens from its userAuth.openId provider over gRPC when they are sent with the IDToken scheme (GetAuthenticationInterceptor falls back to GRPCGetIdentityFromIDToken). Most OIDC providers, including Dex, implement the device grant. The Go admin client just cannot reach them today: it discovers endpoints only from flyteadmin's metadata, and it always sends the access token as Bearer.

What changes were proposed in this pull request?

Client-side only, in flyteidl/clients/go/admin. No flyteadmin changes.

  • New admin.deviceAuthorizationUrl (for DeviceFlow) and admin.authorizationUrl (for Pkce). Each is used together with the existing admin.tokenUrl override, and when set the flow runs against those endpoints instead of the ones flyteadmin advertises, with admin.clientId, admin.scopes and admin.audience used for the client registered there. tokenUrl and scopes are required in that mode. The values are the provider's device_authorization_endpoint (RFC 8628 section 4), authorization_endpoint and token_endpoint from its OpenID Connect Discovery document. Endpoints are configured explicitly rather than discovered, which matches how tokenUrl already works, needs no request at startup and no new dependency.
  • New admin.tokenType: Bearer (default) or IDToken. With IDToken, the id_token from the token response replaces the access token and the token type is set so the request carries IDToken <jwt>. Applied in the device flow, the PKCE code exchange, token refresh and ExternalCommand. The type is stored on the cached token so the existing cache and refresh logic is unchanged.
  • The deprecated authorizationServerUrl key and its Go field are untouched. It was deprecated when endpoint discovery from flyteadmin arrived and is ignored today; giving it new meaning would change behaviour for configs that still carry it from that era, so this uses a new key instead.
  • Validation: tokenType: IDToken with authType: ClientSecret is rejected (the client credentials grant issues no id_token). With an explicit endpoint, tokenUrl and scopes are required and a clientId left at the default flytepropeller logs a warning. A tokenUrl on its own, which some configs still carry for ClientSecret, changes nothing for Pkce or DeviceFlow.
  • Switching tokenType with a token already cached needs no special handling: the first request fails with Unauthenticated, the auth interceptor purges the cached token as it does today, and the flow restarts with the new type.
  • admin.scopes, admin.clientId and admin.audience keep being ignored for Pkce and DeviceFlow unless the matching endpoint is set, so existing configs behave exactly as before.
  • Docs: a new "Headless login with device flow and ID tokens" section in the auth setup page with the client config, the IdP requirements (public client, audience containing flyteadmin's client id) and provider notes for Dex, Keycloak and providers that cannot add audiences to ID tokens.

Example client config:

admin:
  endpoint: dns:///flyte.example.com
  authType: DeviceFlow
  deviceAuthorizationUrl: https://dex.example.com/device/code
  tokenUrl: https://dex.example.com/token
  tokenType: IDToken
  clientId: flytectl
  scopes: [openid, email, profile, offline_access, "audience:server:client_id:flyteadmin"]

How was this patch tested?

Unit tests added:

  • oauth: NormalizeTokenType, PrepareToken (Bearer no-op, ID token substitution, missing id_token, nil).
  • deviceflow: full device flow against a fake server with tokenType: IDToken (id_token becomes the token and is cached), Bearer unchanged, and missing id_token error.
  • tokenorchestrator: refresh grant with tokenType: IDToken returns and caches the new id_token.
  • admin: ExternalCommand with default and IDToken types, unsupported type rejected, DeviceFlow and Pkce with explicit endpoints (endpoints, client id and scopes from config, audience and redirect from admin), missing tokenUrl or scopes rejected, ClientSecret with IDToken rejected, a stale tokenUrl alone leaving DeviceFlow unchanged, and admin-only discovery unchanged.

go test ./clients/go/admin/... passes, gofmt/go vet clean, golangci-lint run --new-from-rev=master with the flyteidl config reports no new issues, flytectl builds. Config flags regenerated with pflags.

Size: about 120 lines of implementation, 350 lines of tests, 40 of docs, and a regenerated flags file.

End to end against two providers.

Keycloak 26.3 (local, realm with a confidential flyteadmin client, a public flytectl client with the device grant enabled and an Audience mapper adding flyteadmin): the direct-grant ID token carries aud: [flytectl, flyteadmin], azp: flytectl, refresh returns a new id_token, and the device grant issues codes to the public client. flytectl from this branch with deviceAuthorizationUrl/tokenUrl pointed at Keycloak ran the device flow, the verification and consent were completed in a browser session, and the resulting ID token was sent to a real flyteadmin with the IDToken scheme, which rejected it only on issuer, expected <its Dex issuer>, got http://localhost:18080/realms/flyte, confirming the token travelled as an ID token end to end.

Dex: a flytectl built from this branch with the config above, against a flyteadmin on the self authorization server with Dex as userAuth.openId. From a Linux host with no browser, the device flow printed the verification URL and code, the user approved on a phone, the ID token had aud containing both the CLI client and flyteadmin's client id, and flytectl get project succeeded with the user's identity. A second run used the cached token with no prompt, and the refresh grant renewed it without interaction. Stock flytectl with the same token via ExternalCommand fails with Unauthenticated ... Request unauthenticated with IDToken, which is the gap this closes.

Labels

  • added

@github-actions github-actions Bot added the flyte label Sep 25, 2026
@santhosh-shanmugham
santhosh-shanmugham force-pushed the sshanmugham/client-idtoken-device-flow branch 7 times, most recently from 6f25862 to 2d41069 Compare September 25, 2026 05:18
… tokens

flyteadmin's built-in authorization server has no device authorization
endpoint, so flytectl on a host without a browser has no way to log in as
a user. flyteadmin does accept OIDC ID tokens from its userAuth provider
over gRPC when they are sent with the IDToken scheme, the same path the
console uses. This change lets the Go admin client use that path:

- admin.deviceAuthorizationUrl (DeviceFlow) and admin.authorizationUrl
  (Pkce), each together with the existing admin.tokenUrl, run the flow
  against another authorization server, for example admin's userAuth
  OIDC provider. admin.clientId, admin.scopes and admin.audience are then
  used for the client registered there. Endpoints are configured
  explicitly, matching the existing tokenUrl override, so there is no
  discovery request and no new dependency.
- admin.tokenType selects the scheme sent to admin: Bearer (default) or
  IDToken. With IDToken the id_token from the token response replaces the
  access token for the device, PKCE, refresh and ExternalCommand paths.
  The token type is stored on the cached token so refreshes keep working.

Behaviour is unchanged unless the new settings are used. The deprecated
authorizationServerUrl key is left as is.

Signed-off-by: Santhosh Shanmugham <sshanmugham@waabi.ai>
@santhosh-shanmugham
santhosh-shanmugham force-pushed the sshanmugham/client-idtoken-device-flow branch from 2d41069 to 7e36dad Compare September 25, 2026 05:26
@santhosh-shanmugham
santhosh-shanmugham marked this pull request as ready for review September 25, 2026 14:01
Tested the client against Dex and Keycloak. The only provider-specific
step is getting flyteadmin's client id into the ID token audience, so
spell out how for Dex and Keycloak and what the constraint means for
providers that cannot add audiences to ID tokens.

Signed-off-by: Santhosh Shanmugham <sshanmugham@waabi.ai>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant