Skip to content

Add a non-interactive device code login - #7220

Open
maksim-zuev-db wants to merge 13 commits into
device-code-split-stepsfrom
device-code-agent-login
Open

maksim-zuev-db wants to merge 13 commits into
device-code-split-stepsfrom
device-code-agent-login

Conversation

@maksim-zuev-db

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

Copy link
Copy Markdown
Member

Stacked on #7217.

Changes

When stdin is not a terminal, auth login --use-device-code runs in two steps. The first step stores the device code as a pending login and prints the URL and the user code. Running the same command with the hidden --user-code and --confirmation-code flags exchanges the device code and saves the login. In a terminal, the login stays interactive.

A new login for the same profile or host replaces its pending login. The second step checks the user code, and the host, client ID and scopes the login was started with. auth logout also deletes pending logins.

The pending login is a new PendingLogin field of storage.Entry, stored under the cache key with a #device-code suffix. Piped stdin no longer completes the login: it starts the two-step login instead.

Why

An AI agent or a script that runs the login as a one-off command cannot type the confirmation code into a running auth login.

Tests

Acceptance tests under device-code/non-interactive and logout/pending-device-code-login, unit tests for the stores and IsInputTTY, and termtest tests for the second step in a terminal. The device-code/nominal and device-code/wrong-code acceptance tests piped the code into stdin, so they move to termtest tests.

No changelog fragment: --use-device-code is still behind DATABRICKS_CLI_DEVICE_CODE.

This pull request and its description were written by Isaac.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Commit: 71775ba35 | Workflow run

Regression test report

Selected 6 changed tests (limit=20, 0 not selected)

6 tests: regression=6 unreleased=0 coverage=0 failing=0

test branch main (aed6702) latest (v1.23.0)
TestAccept/cmd/auth/login/device-code/non-interactive/concurrent ✅ ❌ ❌
TestAccept/cmd/auth/login/device-code/non-interactive/errors ✅ ❌ ❌
TestAccept/cmd/auth/login/device-code/non-interactive/flags ✅ ❌ ❌
TestAccept/cmd/auth/login/device-code/non-interactive/nominal ✅ ❌ ❌
TestAccept/cmd/auth/login/device-code/unsupported-host ✅ ❌ ❌
TestAccept/cmd/auth/logout/pending-device-code-login ✅ ❌ ❌
TestAccept/cmd/auth/login/device-code/non-interactive/concurrent: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/concurrent/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactiveconcurrent1299182883/002/output.txt
        @@ -3,58 +3,4 @@
         
         >>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
         
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
        -
        ->>> update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE
        -
        ->>> cache_keys
        -[
        -  "test#device-code"
        -]
        -
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: a newer login for profile test replaced this one. Complete the newer login with its user code. Run the same command without --user-code and --confirmation-code to start a new login
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY
        -Profile test was successfully saved
        -
        ->>> print_requests.py --oneline //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -
        -=== Logins for different targets are independent
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code
        -
        ->>> cache_keys
        -[
        -  "a#device-code",
        -  "b#device-code",
        -  "[DATABRICKS_URL]",
        -  "[DATABRICKS_URL]#device-code",
        -  "test"
        -]
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile b was successfully saved
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile a was successfully saved
        -
        ->>> cache_keys
        -[
        -  "a",
        -  "b",
        -  "[DATABRICKS_URL]",
        -  "test"
        -]
        -
        ->>> print_requests.py --oneline //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-4&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-5&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        +Exit code: 1
        
    acceptance_test.go:1119: 
        LOG.first: Using a browser on another device, visit:
        LOG.first: http://127.0.0.1:46735/device/verification
        LOG.first: 
        LOG.first: And enter the code:
        LOG.first: WDJB-MJHT
        LOG.first: 
        LOG.first: Once finished, enter the confirmation code provided in your browser.
        LOG.first: Confirmation code: Error: reading confirmation code: EOF

latest (v1.23.0):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/concurrent/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactiveconcurrent745440977/002/output.txt
        @@ -3,58 +3,4 @@
         
         >>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
         
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
        -
        ->>> update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE
        -
        ->>> cache_keys
        -[
        -  "test#device-code"
        -]
        -
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: a newer login for profile test replaced this one. Complete the newer login with its user code. Run the same command without --user-code and --confirmation-code to start a new login
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY
        -Profile test was successfully saved
        -
        ->>> print_requests.py --oneline //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -
        -=== Logins for different targets are independent
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code
        -
        ->>> cache_keys
        -[
        -  "a#device-code",
        -  "b#device-code",
        -  "[DATABRICKS_URL]",
        -  "[DATABRICKS_URL]#device-code",
        -  "test"
        -]
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile b was successfully saved
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile a was successfully saved
        -
        ->>> cache_keys
        -[
        -  "a",
        -  "b",
        -  "[DATABRICKS_URL]",
        -  "test"
        -]
        -
        ->>> print_requests.py --oneline //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-4&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-5&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        +Exit code: 1
        
    acceptance_test.go:1119: 
        LOG.first: Using a browser on another device, visit:
        LOG.first: http://127.0.0.1:36105/device/verification
        LOG.first: 
        LOG.first: And enter the code:
        LOG.first: WDJB-MJHT
        LOG.first: 
        LOG.first: Once finished, enter the confirmation code provided in your browser.
        LOG.first: Confirmation code: Error: reading confirmation code: EOF
TestAccept/cmd/auth/login/device-code/non-interactive/errors: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/errors/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactiveerrors2870499851/002/output.txt
        @@ -5,94 +5,264 @@
         Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Flags the non-interactive login cannot honor
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --configure-cluster
        -Error: --configure-cluster is not supported by the non-interactive device code login. Set cluster_id on the profile after the login
        +Using a browser on another device, visit:
        +[DATABRICKS_URL]/device/verification
         
        +And enter the code:
        +WDJB-MJHT
        +
        +Once finished, enter the confirmation code provided in your browser.
        +Confirmation code: Error: reading confirmation code: EOF
        +
         === Missing host
         
         >>> musterr [CLI] auth login --use-device-code
        -Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
        +Error: the command is being run in a non-interactive environment, please specify a host using --host
         
         >>> musterr [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Incomplete flags of the second step
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --confirmation-code FTFA-QQKY
        -Error: --confirmation-code requires --user-code
        +Error: unknown flag: --confirmation-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT
        -Error: --user-code requires --confirmation-code
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --confirmation-code requires --use-device-code
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Account host
         
         >>> musterr [CLI] auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Warn: [hostmetadata] failed to fetch host metadata for https://accounts.cloud.databricks.test, will skip for 1m0s
        -Error: --use-device-code is only supported for workspace hosts
        +Error: unknown flag: --user-code
         
        -=== Profile scoped to resources
        +Usage:
        +  databricks auth login [PROFILE] [flags]
         
        ->>> musterr [CLI] auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code cannot be used with profile resources because its login is scoped to resources. Log in without --use-device-code
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
         
        -=== No pending login
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
         
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login
         
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: no pending device code login for host [DATABRICKS_URL]. Run the same command without --user-code and --confirmation-code to start a new login
... (126 more lines truncated, raise DATABRICKS_CLI_TEST_MAX_LOG to see more)

latest (v1.23.0):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/errors/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactiveerrors4132462878/002/output.txt
        @@ -5,94 +5,264 @@
         Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Flags the non-interactive login cannot honor
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --configure-cluster
        -Error: --configure-cluster is not supported by the non-interactive device code login. Set cluster_id on the profile after the login
        +Using a browser on another device, visit:
        +[DATABRICKS_URL]/device/verification
         
        +And enter the code:
        +WDJB-MJHT
        +
        +Once finished, enter the confirmation code provided in your browser.
        +Confirmation code: Error: reading confirmation code: EOF
        +
         === Missing host
         
         >>> musterr [CLI] auth login --use-device-code
        -Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
        +Error: the command is being run in a non-interactive environment, please specify a host using --host
         
         >>> musterr [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Incomplete flags of the second step
         
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --confirmation-code FTFA-QQKY
        -Error: --confirmation-code requires --user-code
        +Error: unknown flag: --confirmation-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT
        -Error: --user-code requires --confirmation-code
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         >>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --confirmation-code requires --use-device-code
        +Error: unknown flag: --user-code
         
        +Usage:
        +  databricks auth login [PROFILE] [flags]
        +
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
        +
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
        +
        +
         === Account host
         
         >>> musterr [CLI] auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Warn: [hostmetadata] failed to fetch host metadata for https://accounts.cloud.databricks.test, will skip for 1m0s
        -Error: --use-device-code is only supported for workspace hosts
        +Error: unknown flag: --user-code
         
        -=== Profile scoped to resources
        +Usage:
        +  databricks auth login [PROFILE] [flags]
         
        ->>> musterr [CLI] auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: --use-device-code cannot be used with profile resources because its login is scoped to resources. Log in without --use-device-code
        +Flags:
        +      --client-id string       OAuth client ID to use for U2M authentication
        +      --configure-cluster      Prompts to configure cluster
        +      --configure-serverless   Prompts to configure serverless
        +  -h, --help                   help for login
        +      --resource stringArray   RFC 8707 resource indicator to scope the login to (repeatable). Requires --host.
        +      --scopes string          Comma-separated list of OAuth scopes to request (defaults to 'all-apis')
        +      --skip-workspace         Skip workspace selection for account-level access
        +      --timeout duration       Timeout for completing login challenge in the browser (default 1h0m0s)
         
        -=== No pending login
        +Global Flags:
        +      --account-id string     Databricks Account ID
        +      --debug                 enable debug logging
        +      --host string           Databricks Host
        +  -o, --output type           output type: text or json (default text)
        +  -p, --profile string        ~/.databrickscfg profile
        +  -t, --target string         bundle target to use (if applicable)
        +      --workspace-id string   Databricks Workspace ID
         
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: no pending device code login for profile test. Run the same command without --user-code and --confirmation-code to start a new login
         
        ->>> musterr [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Error: no pending device code login for host [DATABRICKS_URL]. Run the same command without --user-code and --confirmation-code to start a new login
... (126 more lines truncated, raise DATABRICKS_CLI_TEST_MAX_LOG to see more)
TestAccept/cmd/auth/login/device-code/non-interactive/flags: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/flags/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactiveflags2049083770/002/output.txt
        @@ -8,29 +8,7 @@
         And enter the code:
         WDJB-MJHT
         
        -Once finished, complete the login by running the same command with
        ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code
        -provided in your browser.
        +Once finished, enter the confirmation code provided in your browser.
        +Confirmation code: Error: reading confirmation code: EOF
         
        ->>> [CLI] auth login --profile scoped --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile scoped was successfully saved
        -
        ->>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+sql"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -
        -=== Serverless compute is configured in the second step
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile serverless --use-device-code --configure-serverless
        -Using a browser on another device, visit:
        -[DATABRICKS_URL]/device/verification
        -
        -And enter the code:
        -WDJB-MJHT
        -
        -Once finished, complete the login by running the same command with
        ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code
        -provided in your browser.
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile serverless --use-device-code --configure-serverless --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile serverless was successfully saved
        +Exit code: 1
        
    acceptance_test.go:1312: Missing output file: out.databrickscfg

latest (v1.23.0): same output as main (aed6702)

TestAccept/cmd/auth/login/device-code/non-interactive/nominal: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/non-interactive/nominal/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codenon-interactivenominal1594193811/002/output.txt
        @@ -8,86 +8,7 @@
         And enter the code:
         WDJB-MJHT
         
        -Once finished, complete the login by running the same command with
        ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code
        -provided in your browser.
        +Once finished, enter the confirmation code provided in your browser.
        +Confirmation code: Error: reading confirmation code: EOF
         
        -=== The first step stores the device code and saves no profile
        -
        ->>> find.py home
        -home/.databricks/token-cache.json
        -
        ->>> cache_keys
        -[
        -  "[DATABRICKS_URL]#device-code"
        -]
        -
        -=== Confirm the login
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -
        ->>> [CLI] auth token --host [DATABRICKS_URL]
        -json.access_token = "oauth-token";
        -
        ->>> cache_keys
        -[
        -  "[DATABRICKS_URL]"
        -]
        -
        -=== Start the login with a profile
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
        -Using a browser on another device, visit:
        -[DATABRICKS_URL]/device/verification
        -
        -And enter the code:
        -WDJB-MJHT
        -
        -Once finished, complete the login by running the same command with
        ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code
        -provided in your browser.
        -
        -=== Confirm the login
        -
        ->>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile test was successfully saved
        -
        ->>> [CLI] auth token --profile test
        -json.access_token = "oauth-token";
        -
        ->>> [CLI] auth profiles
        -Name            Host                    Valid
        -test (Default)  [DATABRICKS_URL]  YES
        -
        -=== Start the login again with the host from the saved profile
        -
        ->>> [CLI] auth login --profile test --use-device-code
        -Using a browser on another device, visit:
        -[DATABRICKS_URL]/device/verification
        -
        -And enter the code:
        -WDJB-MJHT
        -
        -Once finished, complete the login by running the same command with
        ---user-code WDJB-MJHT and --confirmation-code set to the confirmation code
        -provided in your browser.
        -
        ->>> [CLI] auth token --profile test
        -json.access_token = "oauth-token";
        -
        ->>> [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
        -Profile test was successfully saved
        -
        ->>> cache_keys
        -[
        -  "[DATABRICKS_URL]",
        -  "test"
        -]
        -
        ->>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
        -{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-2&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        -{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
        -{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=device-code-3&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
        +Exit code: 1
        
    acceptance_test.go:1312: Missing output file: out.databrickscfg

latest (v1.23.0): same output as main (aed6702)

TestAccept/cmd/auth/login/device-code/unsupported-host: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: Diff:
        --- cmd/auth/login/device-code/unsupported-host/output.txt
        +++ /tmp/TestAcceptcmdauthlogindevice-codeunsupported-host190340193/002/output.txt
        @@ -2,7 +2,7 @@
         === No host
         
         >>> musterr [CLI] auth login --profile new-profile --use-device-code
        -Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
        +Error: the command is being run in a non-interactive environment, please specify a host using --host
         
         === Account host

latest (v1.23.0): same output as main (aed6702)

TestAccept/cmd/auth/logout/pending-device-code-login: branch ✅ | main (aed6702) ❌ | latest (v1.23.0) ❌

main (aed6702):

acceptance_test.go:1341: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1341
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1082
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:641
        	Error:      	Not equal: 
        	            	expected: "\n=== Logout and delete dev\n>>> [CLI] auth logout --profile dev --auto-approve --delete\nLogged out of and deleted profile \"dev\".\n\n=== Token cache keys after logout — the host keys are kept, staging shares them\n[\n  \"[DATABRICKS_URL]\",\n  \"[DATABRICKS_URL]#device-code\",\n  \"staging\",\n  \"staging#device-code\"\n]\n\n=== Logout staging\n>>> [CLI] auth logout --profile staging --auto-approve\nLogged out of profile \"staging\". Use --delete to also remove it from the config file.\n\n=== Token cache keys after logout — no profile uses the host anymore\n[]\n"
        	            	actual  : "\n=== Logout and delete dev\n>>> [CLI] auth logout --profile dev --auto-approve --delete\nLogged out of and deleted profile \"dev\".\n\n=== Token cache keys after logout — the host keys are kept, staging shares them\n[\n  \"dev#device-code\",\n  \"[DATABRICKS_URL]\",\n  \"[DATABRICKS_URL]#device-code\",\n  \"staging\",\n  \"staging#device-code\"\n]\n\n=== Logout staging\n>>> [CLI] auth logout --profile staging --auto-approve\nLogged out of profile \"staging\". Use --delete to also remove it from the config file.\n\n=== Token cache keys after logout — no profile uses the host anymore\n[\n  \"dev#device-code\",\n  \"[DATABRICKS_URL]#device-code\",\n  \"staging#device-code\"\n]\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,2 +7,3 @@
        	            	 [
        	            	+  "dev#device-code",
        	            	   "[DATABRICKS_URL]",
        	            	@@ -18,3 +19,7 @@
        	            	 === Token cache keys after logout — no profile uses the host anymore
        	            	-[]
        	            	+[
        	            	+  "dev#device-code",
        	            	+  "[DATABRICKS_URL]#device-code",
        	            	+  "staging#device-code"
        	            	+]
        	            	 
        	Test:       	TestAccept/cmd/auth/logout/pending-device-code-login
        	Messages:   	cmd/auth/logout/pending-device-code-login/output.txt vs /tmp/TestAcceptcmdauthlogoutpending-device-code-login2513097971/002/output.txt

latest (v1.23.0): same output as main (aed6702)

@eng-dev-ecosystem-bot

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

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 71775ba

Run: 40910074760

Env ✅​pass 🙈​skip Time
✅​ aws linux-2core-8gb 281 21 5:33
✅​ aws-windows-latest-4core-16gb 283 19 5:15
✅​ azure linux-2core-8gb 280 21 5:13
✅​ azure-windows-latest-4core-16gb 282 19 3:45
✅​ gcp linux-2core-8gb 281 21 5:46
✅​ gcp-windows-latest-4core-16gb 283 19 5:19
Top 6 slowest tests (at least 2 minutes):
duration env testname
5:12 gcp-windows-latest-4core-16gb TestAccept
5:08 aws-windows-latest-4core-16gb TestAccept
3:56 gcp linux-2core-8gb TestAccept
3:45 aws linux-2core-8gb TestAccept
3:38 azure-windows-latest-4core-16gb TestAccept
3:31 azure linux-2core-8gb TestAccept

@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from 4ff0b1e to 1b2be13 Compare October 28, 2026 14:05
@maksim-zuev-db
maksim-zuev-db changed the base branch from device-code-split-steps to device-code-interactive-tests October 28, 2026 14:06
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from 1b2be13 to 26349d5 Compare October 28, 2026 14:35
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from 26349d5 to 90d5979 Compare October 28, 2026 14:45
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from cf4b0e9 to adfe6a4 Compare October 28, 2026 15:42
@maksim-zuev-db
maksim-zuev-db removed this pull request from stack #7223 October 28, 2026 15:48
@maksim-zuev-db
maksim-zuev-db changed the base branch from device-code-interactive-tests to device-code-pending-login-storage October 28, 2026 15:48
@maksim-zuev-db
maksim-zuev-db removed this pull request from stack #7236 October 28, 2026 15:57
@maksim-zuev-db
maksim-zuev-db changed the base branch from device-code-pending-login-storage to device-code-interactive-tests October 28, 2026 15:57
@maksim-zuev-db
maksim-zuev-db requested review from simonfaltum and removed request for simonfaltum October 28, 2026 16:03
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from adfe6a4 to b80d829 Compare October 29, 2026 12:15
@github-actions github-actions Bot added DABs DABs related issues AIR Databricks AI Runtime CLI labels Oct 29, 2026
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from b80d829 to 64abb91 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
maksim-zuev-db changed the base branch from device-code-interactive-tests to device-code-split-steps October 29, 2026 12:22
@maksim-zuev-db maksim-zuev-db removed DABs DABs related issues AIR Databricks AI Runtime CLI labels Oct 29, 2026
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch 2 times, most recently from fb2c74d to fa4c726 Compare October 29, 2026 13:37
maksim-zuev-db and others added 11 commits October 29, 2026 15:19
When stdin is not a terminal, e.g. when an AI agent runs it, auth login
--use-device-code stores the device code as a pending login in the token
store and prints the URL and the user code. Rerunning the command with
the hidden --user-code and --confirmation-code flags completes the
login. The device code stays on the machine, so a confirmation code in
an agent's transcript cannot complete the login elsewhere.

The pending login is a new PendingLogin field of storage.Entry, and auth
logout deletes it together with the token. The flow is picked with the
new cmdio.IsInputTTY.

The interactive login tests piped the code into stdin, which now starts
the non-interactive login, so they move from acceptance tests to the
terminal emulator.

Co-authored-by: Isaac <no-reply@databricks.com>
The interactive and the non-interactive device code login printed the same
lines that show where to approve the login. approvalInstructions returns
them for both, so they cannot drift apart.

Co-authored-by: Isaac <no-reply@databricks.com>
Only an acceptance test covered the check. The unit test completes a
login with the user code of another login, checks that no token request
is sent, and then completes it with the right user code in another
format.

Co-authored-by: Isaac <no-reply@databricks.com>
A pending device code login stayed in the token store after the profile
or host logged in another way, e.g. in the browser or with a device code
login in a terminal. storeLoginToken, which every login goes through,
now deletes the pending login of the same profile or host, so the
non-interactive completion no longer deletes it itself.

Co-authored-by: Isaac <no-reply@databricks.com>
An empty --user-code failed with "a newer login ... replaced this one",
and an empty --confirmation-code was sent to the server. Both now fail
before the login with an error that names the empty flag.

Co-authored-by: Isaac <no-reply@databricks.com>
TestLoginDeviceCodeWrongConfirmationCode checks three wrong codes and a
login without input, and also counts the token requests, so the tests
for the same cases are no longer needed.

Co-authored-by: Isaac <no-reply@databricks.com>
NewDeviceCodeLogin now takes a context.

Co-authored-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
A pending login was a token store entry under the cache key with a
#device-code suffix, which a profile can also be named. A pending login for
profile a then replaced the token of profile a#device-code, auth token for
that profile failed with a nil pointer dereference, and logging out of a
deleted it.

The stores now keep pending logins in their own namespace through a new
PendingLoginStore interface, under the plain cache key: the pending_logins
map of the token cache file, the databricks-cli-pending-login keyring
service, and a separate map in the memory store. Entry holds a token
again, and deleting a missing pending login no longer rewrites the token
cache file on every login and logout.

Co-authored-by: Isaac <no-reply@databricks.com>
The second step of the non-interactive login used the host, --client-id
and --scopes of its own command. The token gets the scopes of the device
authorization, so a second step without the --scopes of the first saved a
profile whose scopes did not match its token, and a different host or
client sent the device code to a server or for a client that did not
issue it.

The pending login now stores the host, client ID and scopes, and the second
step fails before the token exchange if any of them differs.

Co-authored-by: Isaac <no-reply@databricks.com>
The second step of the non-interactive login asked for the workspace when
it ran in a terminal and the host had an account ID but no workspace ID,
although neither step is meant to prompt. It now shows the hint that a
login without a terminal shows instead, and saves the profile without a
workspace ID.

Co-authored-by: Isaac <no-reply@databricks.com>
@maksim-zuev-db
maksim-zuev-db force-pushed the device-code-agent-login branch from fa4c726 to bda3307 Compare October 29, 2026 15:53
maksim-zuev-db and others added 2 commits October 29, 2026 16:08
This reverts commit 1af032a. Pending logins are token store entries
under the cache key with a #device-code suffix again, and keep the host,
client ID and scopes that a later commit added. The README describes the
settings check in its own paragraph.

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

This reverts commit bda3307. The picker only prompted when the second
step ran in a terminal for a host whose metadata has an account ID but no
workspace ID, and the prompt can be skipped, so the extra branch is not
worth it.

Co-authored-by: Isaac <no-reply@databricks.com>
@maksim-zuev-db
maksim-zuev-db marked this pull request as ready for review October 29, 2026 16:54
@maksim-zuev-db
maksim-zuev-db requested review from a team as code owners October 29, 2026 16:54

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left a few comments. Nothing blocking from my side. The one I care most about is the #device-code key collision, the rest are small.

// pendingLoginKeySuffix marks the token store entry that holds a
// non-interactive device code login until it completes. The entry sits next to
// the login's token, so that a token that still works is kept until then.
const pendingLoginKeySuffix = "#device-code"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nice to have] The #device-code suffix shares the keyspace with profile names, and INI accepts [a#device-code] as a section. If profiles a and a#device-code both exist, a non-interactive login for a overwrites the other profile's token, and every successful login or logout of a deletes it. databricks auth token -p 'a#device-code' then panics: loadToken (libs/auth/u2m/persistent_auth.go:303) returns a nil token with no error, and refresh reads oldToken.RefreshToken.

I know 1af032a fixed this and was reverted. At minimum I think loadToken should treat e.Token == nil as ErrNotFound, and login should reject profile names ending in #device-code.

}
token, err := login.RequestDeviceAccessToken(ctx, pending.DeviceCode, confirmationCode)
if err != nil {
err = fmt.Errorf("token exchange: %w", err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nice to have] A 429 "Device grant rate limit exceeded" doesn't match either sentinel, so complete keeps the pending login and returns the bare error without startNewLoginHint. The troubleshooting table says to start a new login, but an agent that reruns the same completion command just gets the 429 again. I think we should map 429 to its own sentinel and handle it like ErrInvalidDeviceCode: delete the pending login and append the hint.

// started with, or returns "" if none does.
func (l nonInteractiveDeviceCodeLogin) changedSetting(pending storage.PendingLogin) string {
switch {
case pending.Host != l.host:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] changedSetting compares pending.Host != l.host as raw strings, but the cache key goes through CanonicalHostName. Step 1 with --host adb-1.azuredatabricks.net and step 2 with --host https://adb-1.azuredatabricks.net find the same pending login, then fail with "started with a different host". Comparing canonical hosts (or storing the canonical host in PendingLogin) would fix it.

// with the wrong device code, which the server would reject as a wrong
// confirmation code.
if !sameCode(pending.UserCode, userCode) {
return nil, fmt.Errorf("a newer login for %s replaced this one. Complete the newer login with its user code. %s", l.target(), startNewLoginHint)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] Every mismatched --user-code gets "a newer login ... replaced this one", including a typo or swapped --user-code/--confirmation-code values. In those cases the message points at a newer login that doesn't exist. Something like "--user-code does not match the pending login for X. A newer login may have replaced it" would cover both cases.

Comment thread cmd/auth/login.go
// is not a terminal, e.g. when an AI agent runs the login as a one-off
// command, nobody can type the code in. The non-interactive login then
// stores the device code, and a rerun of the command with --user-code
// and --confirmation-code completes the login. Neither step prompts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] After the revert of bda3307, a second step that runs in a terminal can still show the workspace picker through shouldPromptWorkspace. Maybe "Neither step prompts for the profile or the host".

Comment thread cmd/auth/login.go
}
// A pending device code login for the same profile or host is left over
// once a login succeeds.
if err := tokenStore.Delete(pendingLoginKey(key)); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] storeLoginToken now deletes <key>#device-code after every login, browser and discovery included. fileStore.Delete rewrites token-cache.json even when the key is missing. Skipping the write when the key isn't in f.Tokens would leave normal logins unchanged for users who never use device code.

Comment thread cmd/auth/logout.go
}
// A pending device code login could still complete and restore the
// credentials that the user is logging out of.
if err := tokenStore.Delete(pendingLoginKey(p.Name)); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] Logout only calls clearTokenStore when auth_type = databricks-cli (isCreatedByLogin, line 207), so a pending login started for a PAT profile survives auth logout --profile dev. The docs say logout removes the profile's pending login. Either delete pendingLoginKey(p.Name) regardless of auth type, or narrow the docs.


func cloneEntry(e Entry) Entry {
e.Token = cloneToken(e.Token)
if e.PendingLogin != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] cloneEntry copies the PendingLogin struct, but Scopes still shares its backing array, so changing the caller's slice after Put changes the stored entry. slices.Clone(pending.Scopes) fixes it, and TestMemoryStore_PendingLoginUsesCopies could check Scopes too.

func TestLoginDeviceCodeCompletesInTerminal(t *testing.T) {
server := newDeviceCodeServer(t)
setUpDeviceCodeLogin(t, "")
ctx := cmdio.InContext(t.Context(), cmdio.NewIO(t.Context(), flags.OutputText, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}, "", ""))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] This and the tests at lines 477 and 531 build cmdio.InContext(..., cmdio.NewIO(..., strings.NewReader(""), ...)) by hand. That's what cmdio.MockDiscard(t.Context()) already does, and logout_test.go and switch_test.go use it.

title "A second login for the same profile replaces the first\n"
start_login LOG.first $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
start_login LOG.second $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
trace update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] The script rewrites token-cache.json with update_file.py because the fake server always issues WDJBMJHT. The testing rules prefer modeling this in libs/testserver/. For example, the fake could issue a new user code from a fixed sequence for each authorization, so the test exercises the replace behavior directly.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants