Skip to content

Commit 3e66ca1

Browse files
authored
Improve readability of the device auth code (#45)
1 parent 66cd84a commit 3e66ca1

File tree

2 files changed

+127
-13
lines changed

2 files changed

+127
-13
lines changed

src/PkgAuthentication.jl

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,31 @@ struct NoAuthentication <: State
175175
end
176176
Base.show(io::IO, s::NoAuthentication) = print(io, "NoAuthentication($(s.server), $(s.auth_suffix))")
177177

178-
function get_device_auth_client_id()
179-
return get(ENV, "JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID", "")
178+
function device_client_id()
179+
return get(ENV, "JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID", "device")
180180
end
181181

182-
function should_use_device_auth()
183-
return !isempty(get_device_auth_client_id())
182+
# Constructs the body if the device authentication flow requests, in accordance with
183+
# the Sections 3.1 and 3.4 of RFC8628 (https://datatracker.ietf.org/doc/html/rfc8628).
184+
# Returns an IOBuffer() object that can be passed to Downloads.download(input=...).
185+
function device_token_request_body(;
186+
client_id::AbstractString,
187+
scope::Union{AbstractString, Nothing} = nothing,
188+
device_code::Union{AbstractString, Nothing} = nothing,
189+
grant_type::Union{AbstractString, Nothing} = nothing,
190+
)
191+
b = IOBuffer()
192+
write(b, "client_id=", client_id)
193+
if !isnothing(scope)
194+
write(b, "&scope=", scope)
195+
end
196+
if !isnothing(device_code)
197+
write(b, "&device_code=", device_code)
198+
end
199+
if !isnothing(grant_type)
200+
write(b, "&grant_type=", grant_type)
201+
end
202+
return seek(b, 0)
184203
end
185204

186205
# Query the /auth/configuration endpoint to get the refresh url and
@@ -235,7 +254,14 @@ function step(state::NoAuthentication)::Union{RequestLogin, Failure}
235254
initiate_browser_challenge(state)
236255
end
237256
if success
238-
return RequestLogin(state.server, state.auth_suffix, challenge, body_or_response, get(auth_config, "device_token_endpoint", ""), get(auth_config, "device_token_refresh_url", ""))
257+
return RequestLogin(
258+
state.server,
259+
state.auth_suffix,
260+
challenge,
261+
body_or_response,
262+
get(auth_config, "device_token_endpoint", ""),
263+
get(auth_config, "device_token_refresh_url", ""),
264+
)
239265
else
240266
return HttpError(body_or_response)
241267
end
@@ -246,7 +272,10 @@ function fetch_device_code(state::NoAuthentication, device_endpoint::AbstractStr
246272
response = Downloads.request(
247273
device_endpoint,
248274
method = "POST",
249-
input = IOBuffer("client_id=$(get(ENV, "JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID", "device"))&scope=openid email profile offline_access"),
275+
input = device_token_request_body(
276+
client_id = device_client_id(),
277+
scope = "openid profile offline_access",
278+
),
250279
output = output,
251280
throw = false,
252281
headers = Dict("Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded"),
@@ -426,9 +455,29 @@ function step(state::RequestLogin)::Union{ClaimToken, Failure}
426455
success = open_browser(url)
427456
if success && is_device
428457
# In case of device tokens, timeout for challenge is received in the initial request.
429-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, Inf, time(), state.response["expires_in"], 2, 0, 10, state.device_token_endpoint, state.device_token_refresh_url)
458+
return ClaimToken(
459+
state.server,
460+
state.auth_suffix,
461+
state.challenge,
462+
state.response,
463+
Inf,
464+
time(),
465+
state.response["expires_in"],
466+
2,
467+
0,
468+
10,
469+
state.device_token_endpoint,
470+
state.device_token_refresh_url,
471+
)
430472
elseif success
431-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.device_token_endpoint, state.device_token_refresh_url)
473+
return ClaimToken(
474+
state.server,
475+
state.auth_suffix,
476+
state.challenge,
477+
state.response,
478+
state.device_token_endpoint,
479+
state.device_token_refresh_url
480+
)
432481
else # this can only happen for the browser hook
433482
return GenericError("Failed to execute open_browser hook.")
434483
end
@@ -476,7 +525,11 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
476525
response = Downloads.request(
477526
state.device_token_endpoint,
478527
method = "POST",
479-
input = IOBuffer("client_id=$(get(ENV, "JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID", "device"))&scope=openid profile offline_access&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$(state.response["device_code"])"),
528+
input = device_token_request_body(
529+
client_id = device_client_id(),
530+
device_code = state.response["device_code"],
531+
grant_type = "urn:ietf:params:oauth:grant-type:device_code",
532+
),
480533
output = output,
481534
throw = false,
482535
headers = Dict("Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded"),
@@ -499,15 +552,54 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
499552
body = try
500553
JSON.parse(String(take!(output)))
501554
catch err
502-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.device_token_endpoint, state.device_token_refresh_url)
555+
return ClaimToken(
556+
state.server,
557+
state.auth_suffix,
558+
state.challenge,
559+
state.response,
560+
state.expiry,
561+
state.start_time,
562+
state.timeout,
563+
state.poll_interval,
564+
state.failures + 1,
565+
state.max_failures,
566+
state.device_token_endpoint,
567+
state.device_token_refresh_url,
568+
)
503569
end
504570

505571
if haskey(body, "token")
506572
return HasNewToken(state.server, body["token"])
507573
elseif haskey(body, "expiry") # time at which the response/challenge pair will expire on the server
508-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, body["expiry"], state.start_time, state.timeout, state.poll_interval, state.failures, state.max_failures, state.device_token_endpoint, state.device_token_refresh_url)
574+
return ClaimToken(
575+
state.server,
576+
state.auth_suffix,
577+
state.challenge,
578+
state.response,
579+
body["expiry"],
580+
state.start_time,
581+
state.timeout,
582+
state.poll_interval,
583+
state.failures,
584+
state.max_failures,
585+
state.device_token_endpoint,
586+
state.device_token_refresh_url,
587+
)
509588
else
510-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.device_token_endpoint, state.device_token_refresh_url)
589+
return ClaimToken(
590+
state.server,
591+
state.auth_suffix,
592+
state.challenge,
593+
state.response,
594+
state.expiry,
595+
state.start_time,
596+
state.timeout,
597+
state.poll_interval,
598+
state.failures + 1,
599+
state.max_failures,
600+
state.device_token_endpoint,
601+
state.device_token_refresh_url
602+
)
511603
end
512604
elseif response isa Downloads.Response && response.status == 200
513605
body = JSON.parse(String(take!(output)))
@@ -516,7 +608,20 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
516608
body["refresh_url"] = state.device_token_refresh_url
517609
return HasNewToken(state.server, body)
518610
elseif response isa Downloads.Response && response.status in [401, 400] && is_device
519-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.device_token_endpoint, state.device_token_refresh_url)
611+
return ClaimToken(
612+
state.server,
613+
state.auth_suffix,
614+
state.challenge,
615+
state.response,
616+
state.expiry,
617+
state.start_time,
618+
state.timeout,
619+
state.poll_interval,
620+
state.failures + 1,
621+
state.max_failures,
622+
state.device_token_endpoint,
623+
state.device_token_refresh_url,
624+
)
520625
else
521626
return HttpError(response)
522627
end

test/utilities_test.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,12 @@
1111

1212
@test PkgAuthentication.detectwsl() isa Bool
1313
end
14+
15+
@testset "device_token_request_body" begin
16+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo"))) == "client_id=foo"
17+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo", scope="bar"))) == "client_id=foo&scope=bar"
18+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo", device_code="bar"))) == "client_id=foo&device_code=bar"
19+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo", grant_type="bar"))) == "client_id=foo&grant_type=bar"
20+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo", scope="bar", device_code="baz", grant_type="qux"))) == "client_id=foo&scope=bar&device_code=baz&grant_type=qux"
21+
@test String(take!(PkgAuthentication.device_token_request_body(client_id="foo", scope=nothing, device_code=nothing, grant_type=nothing))) == "client_id=foo"
22+
end

0 commit comments

Comments
 (0)