Skip to content

Commit 90d5979

Browse files
maksim-zuev-dbIsaac
andcommitted
Add a non-interactive device code login
Device code login reads the confirmation code from stdin, which only works when someone can type into the command. An AI agent or a script that runs the login as a one-off command cannot. When stdin is not a terminal, auth login --use-device-code requests the device authorization, stores the device code as a pending login in the token store, and prints the URL, the user code, and a command that completes the login. That command is the same login with the hidden --user-code and --confirmation-code flags: it exchanges the pending device code and then saves the login like any other. When an AI agent is detected, the first step also prints notes that tell it to pass the login on to the user. The device code never leaves the machine, so a confirmation code that ends up somewhere else, such as an agent's transcript, cannot complete the login elsewhere. The pending login is keyed by the login's cache key with a #device-code suffix, so the current token keeps working until the login completes. A new login for the same target replaces it, and the user code tells a replaced login apart without sending its confirmation code to the server. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 416a762 commit 90d5979

25 files changed

Lines changed: 1033 additions & 8 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cloud = false
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
=== A second login for the same profile replaces the first
3+
4+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
5+
6+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
7+
8+
>>> update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE
9+
10+
>>> cache_keys
11+
[
12+
"test#device-code"
13+
]
14+
15+
>>> musterr eval databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code --user-code 'WDJB-MJHT' --confirmation-code 'FTFA-QQKY'
16+
Error: a newer login for profile test replaced this one. Run the command printed by the newer login, or start a new login with: databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code
17+
18+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY
19+
Profile test was successfully saved
20+
21+
>>> print_requests.py --oneline //oidc/v1/token
22+
{"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"}
23+
24+
=== Logins for different targets are independent
25+
26+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile a --use-device-code
27+
28+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile b --use-device-code
29+
30+
>>> [CLI] auth login --host [DATABRICKS_URL] --use-device-code
31+
32+
>>> cache_keys
33+
[
34+
"a#device-code",
35+
"b#device-code",
36+
"[DATABRICKS_URL]",
37+
"[DATABRICKS_URL]#device-code",
38+
"test"
39+
]
40+
41+
>>> eval databricks auth login --host '[DATABRICKS_URL]' --profile 'b' --use-device-code --user-code 'WDJB-MJHT' --confirmation-code 'FTFA-QQKY'
42+
Profile b was successfully saved
43+
44+
>>> eval databricks auth login --host '[DATABRICKS_URL]' --use-device-code --user-code 'WDJB-MJHT' --confirmation-code 'FTFA-QQKY'
45+
46+
>>> eval databricks auth login --host '[DATABRICKS_URL]' --profile 'a' --use-device-code --user-code 'WDJB-MJHT' --confirmation-code 'FTFA-QQKY'
47+
Profile a was successfully saved
48+
49+
>>> cache_keys
50+
[
51+
"a",
52+
"b",
53+
"[DATABRICKS_URL]",
54+
"test"
55+
]
56+
57+
>>> print_requests.py --oneline //oidc/v1/token
58+
{"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"}
59+
{"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"}
60+
{"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"}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
sethome "./home"
2+
3+
# The real server issues a new user code for every login, while the fake server
4+
# issues the same one. The script gives the second login its own user code.
5+
title "A second login for the same profile replaces the first\n"
6+
start_login LOG.first $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
7+
start_login LOG.second $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
8+
trace update_file.py home/.databricks/token-cache.json WDJBMJHT NEWRCODE
9+
trace cache_keys
10+
trace musterr eval "$(complete_command LOG.first)"
11+
trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code NEWR-CODE --confirmation-code FTFA-QQKY
12+
trace print_requests.py --oneline //oidc/v1/token
13+
14+
title "Logins for different targets are independent\n"
15+
start_login LOG.a $CLI auth login --host $DATABRICKS_HOST --profile a --use-device-code
16+
start_login LOG.b $CLI auth login --host $DATABRICKS_HOST --profile b --use-device-code
17+
start_login LOG.host $CLI auth login --host $DATABRICKS_HOST --use-device-code
18+
trace cache_keys
19+
trace eval "$(complete_command LOG.b)"
20+
trace eval "$(complete_command LOG.host)"
21+
trace eval "$(complete_command LOG.a)"
22+
trace cache_keys
23+
trace print_requests.py --oneline //oidc/v1/token
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[resources]
2+
host = $DATABRICKS_HOST
3+
resources = https://resource.test
4+
auth_type = databricks-cli
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cloud = false
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
2+
=== Device code login is disabled by default
3+
4+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
5+
Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
6+
7+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
8+
Error: --use-device-code is experimental. Set DATABRICKS_CLI_DEVICE_CODE=true to enable it
9+
10+
=== Flags the non-interactive login cannot honor
11+
12+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --configure-cluster
13+
Error: --configure-cluster is not supported by the non-interactive device code login. Set cluster_id on the profile after the login
14+
15+
=== Missing host
16+
17+
>>> musterr [CLI] auth login --use-device-code
18+
Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
19+
20+
>>> musterr [CLI] auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
21+
Error: --use-device-code requires a workspace host. Pass it as an argument or with --host
22+
23+
=== Incomplete flags of the second step
24+
25+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --confirmation-code FTFA-QQKY
26+
Error: --confirmation-code requires --user-code
27+
28+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT
29+
Error: --user-code requires --confirmation-code
30+
31+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
32+
Error: --confirmation-code requires --use-device-code
33+
34+
=== Account host
35+
36+
>>> musterr [CLI] auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
37+
Warn: [hostmetadata] failed to fetch host metadata for https://accounts.cloud.databricks.test, will skip for 1m0s
38+
Error: --use-device-code is only supported for workspace hosts
39+
40+
=== Profile scoped to resources
41+
42+
>>> musterr [CLI] auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
43+
Error: --use-device-code cannot be used with profile resources because its login is scoped to resources. Log in without --use-device-code
44+
45+
=== No pending login
46+
47+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
48+
Error: no pending device code login for profile test. Start a new login with: databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code
49+
50+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
51+
Error: no pending device code login for host [DATABRICKS_URL]. Start a new login with: databricks auth login --host '[DATABRICKS_URL]' --use-device-code
52+
53+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
54+
55+
=== Wrong confirmation code
56+
57+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code AAAA-AAAA
58+
Error: token exchange: Invalid confirmation code (error code: invalid_request). Check the confirmation code and approve the login if you haven't, then rerun this command. If it still fails, start a new login with: databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code
59+
60+
=== Rerun with the right confirmation code
61+
62+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code wdjb-mjht --confirmation-code ftfa-qqky
63+
Profile test was successfully saved
64+
65+
=== Rerun after the login was completed
66+
67+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
68+
Error: no pending device code login for profile test. Start a new login with: databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code
69+
70+
=== Device code unknown to the server
71+
72+
>>> [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code
73+
74+
>>> update_file.py home/.databricks/token-cache.json device-code-2 unknown-device-code
75+
76+
>>> musterr [CLI] auth login --host [DATABRICKS_URL] --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
77+
Error: token exchange: Invalid device code (error code: invalid_request). Start a new login with: databricks auth login --host '[DATABRICKS_URL]' --profile 'test' --use-device-code
78+
79+
>>> cache_keys
80+
[
81+
"[DATABRICKS_URL]",
82+
"test"
83+
]
84+
85+
>>> print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
86+
{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
87+
{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=AAAAAAAA&device_code=device-code-1&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
88+
{"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"}
89+
{"method": "POST", "path": "/oidc/v1/device/auth", "raw_body": "client_id=databricks-cli&scope=offline_access+all-apis"}
90+
{"method": "POST", "path": "/oidc/v1/token", "raw_body": "client_id=databricks-cli&confirmation_code=FTFAQQKY&device_code=unknown-device-code&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
sethome "./home"
2+
3+
title "Device code login is disabled by default\n"
4+
(
5+
unset DATABRICKS_CLI_DEVICE_CODE
6+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
7+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
8+
)
9+
10+
title "Flags the non-interactive login cannot honor\n"
11+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --configure-cluster
12+
13+
# The non-interactive login cannot answer the profile picker or the host prompt.
14+
title "Missing host\n"
15+
trace musterr $CLI auth login --use-device-code
16+
trace musterr $CLI auth login --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
17+
18+
title "Incomplete flags of the second step\n"
19+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --confirmation-code FTFA-QQKY
20+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT
21+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
22+
23+
title "Account host\n"
24+
trace musterr $CLI auth login --host https://accounts.cloud.databricks.test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
25+
26+
envsubst < "./home/.databrickscfg.tmpl" > "./home/.databrickscfg"
27+
28+
title "Profile scoped to resources\n"
29+
trace musterr $CLI auth login --profile resources --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
30+
31+
title "No pending login\n"
32+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
33+
trace musterr $CLI auth login --host $DATABRICKS_HOST --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
34+
35+
start_login LOG.wrong-code $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
36+
37+
title "Wrong confirmation code\n"
38+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code AAAA-AAAA
39+
40+
# A wrong confirmation code leaves the device code valid, so the pending
41+
# login is kept.
42+
title "Rerun with the right confirmation code\n"
43+
trace $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code wdjb-mjht --confirmation-code ftfa-qqky
44+
45+
title "Rerun after the login was completed\n"
46+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
47+
48+
# The server discards the device code when the login expires. The pending
49+
# login is then deleted.
50+
title "Device code unknown to the server\n"
51+
start_login LOG.expired $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code
52+
trace update_file.py home/.databricks/token-cache.json device-code-2 unknown-device-code
53+
trace musterr $CLI auth login --host $DATABRICKS_HOST --profile test --use-device-code --user-code WDJB-MJHT --confirmation-code FTFA-QQKY
54+
trace cache_keys
55+
56+
trace print_requests.py --oneline //oidc/v1/device/auth //oidc/v1/token
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[scoped]
2+
host = $DATABRICKS_HOST
3+
scopes = sql
4+
client_id = databricks-cli
5+
auth_type = databricks-cli
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
; The profile defined in the DEFAULT section is to be used as a fallback when no profile is explicitly specified.
2+
[DEFAULT]
3+
4+
[scoped]
5+
host = [DATABRICKS_URL]
6+
scopes = sql
7+
auth_type = databricks-cli
8+
workspace_id = [WORKSPACE_ID]
9+
client_id = databricks-cli
10+
11+
[serverless]
12+
host = [DATABRICKS_URL]
13+
serverless_compute_id = auto
14+
workspace_id = [WORKSPACE_ID]
15+
auth_type = databricks-cli
16+
17+
[hostparams]
18+
host = [DATABRICKS_URL]
19+
workspace_id = 123456
20+
auth_type = databricks-cli
21+
22+
[idflag]
23+
host = [DATABRICKS_URL]
24+
workspace_id = 654321
25+
auth_type = databricks-cli
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cloud = false

0 commit comments

Comments
 (0)