Skip to content

Split device code login into authorization and token exchange steps - #7217

Merged
maksim-zuev-db merged 6 commits into
mainfrom
device-code-split-steps
Oct 30, 2026
Merged

maksim-zuev-db merged 6 commits into
mainfrom
device-code-split-steps

Conversation

@maksim-zuev-db

@maksim-zuev-db maksim-zuev-db commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Changes

A refactoring for the non-interactive device code login (#7220), with no user-visible change.

libs/auth/u2m now provides the two steps of device code login instead of the whole login. PersistentAuth.NewDeviceCodeLogin(ctx) checks that the host supports device code login and fetches the OAuth endpoints once, with the caller's context. Its RequestDeviceAuthorization and RequestDeviceAccessToken methods make the two requests. A rejected code matches ErrInvalidConfirmationCode or ErrInvalidDeviceCode, so callers no longer compare the server's error description.

The confirmation code prompt and its retries move from DeviceCodeChallenge, which is removed, to auth login, built on the two steps. auth login adds the token exchange: prefix to the errors it does not retry, so the login prints the same messages as before.

Why

In the non-interactive login (#7220), one CLI invocation requests the device authorization and a later one exchanges the device code, so the two steps have to be callable separately.

Tests

The u2m tests call the two steps instead of DeviceCodeChallenge. They are edited in place, so the diff shows the changed calls and the error messages without the token exchange: prefix. The retry tests move out of u2m with the loop: the device-code/nominal and device-code/wrong-code acceptance tests cover it and pass unchanged. New termtest tests check the loop in auth login. One enters a wrong confirmation code followed by the right one, three wrong codes, and a wrong code followed by no input, and checks the number of token requests for each. Another checks that a rate-limited token request and an unknown device code each end the login after one token request, without asking for the code again.

This pull request and its description were written by Isaac.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 84cdd4e

Run: 40904988105

Env 🔄​flaky ✅​pass 🙈​skip Time
✅​ aws linux-2core-8gb 281 15 5:44
🔄​ aws-windows-latest-4core-16gb 2 281 13 5:39
✅​ azure linux-2core-8gb 280 15 5:23
✅​ azure-windows-latest-4core-16gb 282 13 6:16
✅​ gcp linux-2core-8gb 281 15 5:33
✅​ gcp-windows-latest-4core-16gb 283 13 5:16
Test Name aws-windows-latest-4core-16gb
🔄​ TestFilerRecursiveDelete 🔄​f
🔄​ TestFilerRecursiveDelete/files 🔄​f
Top 6 slowest tests (at least 2 minutes):
duration env testname
6:08 azure-windows-latest-4core-16gb TestAccept
5:13 aws-windows-latest-4core-16gb TestAccept
5:09 gcp-windows-latest-4core-16gb TestAccept
4:02 aws linux-2core-8gb TestAccept
3:46 azure linux-2core-8gb TestAccept
3:37 gcp linux-2core-8gb TestAccept

libs/auth/u2m now provides the two steps of device code login instead of
the whole login. PersistentAuth.DeviceCodeLogin checks that the host
supports it and fetches the OAuth endpoints, and its
RequestDeviceAuthorization and RequestDeviceAccessToken methods make the
two requests. A rejected code matches ErrInvalidConfirmationCode or
ErrInvalidDeviceCode.

The confirmation code prompt and its retries move from
DeviceCodeChallenge to auth login. The login's output does not change.

Co-authored-by: Isaac <no-reply@databricks.com>
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-split-steps branch from 74b6992 to 0ab827c Compare October 29, 2026 12:19
@maksim-zuev-db
maksim-zuev-db removed this pull request from stack #7237 October 29, 2026 12:22
maksim-zuev-db and others added 5 commits October 29, 2026 12:51
The retries moved from u2m to auth login without unit tests. The new test
enters a wrong code followed by the right one, three wrong codes, and a
wrong code followed by no input, and checks the number of token requests
for each.

Also rename PersistentAuth.DeviceCodeLogin to NewDeviceCodeLogin, which
had the same name as the DeviceCodeLogin type it returns, rename
TestRequestDeviceAccessToken_RetriesWrongConfirmationCode to
TestRequestDeviceAccessToken_WrongConfirmationCode because it no longer
retries, and check the token requests in TestDeviceCodeLogin_Errors again.

Co-authored-by: Isaac <no-reply@databricks.com>
The retry test covered missing input only after a wrong code. The new
case closes stdin at the first prompt and checks that no token request
is sent.

Co-authored-by: Isaac <no-reply@databricks.com>
NewDeviceCodeLogin fetches the OAuth endpoints, and used the context that
PersistentAuth was created with. It now takes the context of the call,
like the two steps of the login, and passes it to oauth2Config.

Co-authored-by: Isaac <no-reply@databricks.com>
Each case now states how many token requests it expects: none when the
device authorization is rejected, and one otherwise. The previous upper
bound of one also passed if a case that should reach the token endpoint
never did.

Co-authored-by: Isaac <no-reply@databricks.com>
The server answers both a wrong confirmation code and an unknown device
code with HTTP 400 and invalid_request, and only a wrong confirmation code
lets the user enter the code again. Only the u2m tests checked that an
unknown device code is told apart, so auth login could ask for the code
again with no test failing. TestLoginDeviceCodeFailsOnTokenExchangeError
now covers an unknown device code next to the rate limit, and checks that
each sends a single token request.

Co-authored-by: Isaac <no-reply@databricks.com>

@mihaimitrea-db mihaimitrea-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the ctx problem the refactoring looks good.

switch argg := a.oAuthArgument.(type) {
case WorkspaceOAuthArgument:
endpoints, err = a.endpointSupplier.GetWorkspaceOAuthEndpoints(a.ctx, argg.GetWorkspaceHost())
endpoints, err = a.endpointSupplier.GetWorkspaceOAuthEndpoints(ctx, argg.GetWorkspaceHost())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PersistentAuth has its own context why are we using a different one? We should use one or the other and in this case the one from within PersistentAuth seems to be the correct one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agents/rules/style-guide-go.md says to pass context.Context as an argument and never store it in a struct, and this came up for the device code login in #7093 (#7093 (comment)). The stored ctx is only there because Token() (oauth2.TokenSource) and Challenge() don't take a context. The device code login now uses only the context passed to it, including for fetching the endpoints, so it no longer mixes the two. Both callers pass the context PersistentAuth was created with, so behavior is unchanged.

@maksim-zuev-db
maksim-zuev-db added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit e7d21a2 Oct 30, 2026
27 checks passed
@maksim-zuev-db
maksim-zuev-db deleted the device-code-split-steps branch October 30, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants