Skip to content

Commit 79e417d

Browse files
committed
refactor, use dex well known conf, use env for client id
1 parent 0f3d45a commit 79e417d

File tree

1 file changed

+74
-28
lines changed

1 file changed

+74
-28
lines changed

src/PkgAuthentication.jl

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -174,31 +174,76 @@ struct NoAuthentication <: State
174174
end
175175
Base.show(io::IO, s::NoAuthentication) = print(io, "NoAuthentication($(s.server))")
176176

177+
function get_openid_configuration(state::NoAuthentication)
178+
output = IOBuffer()
179+
response = Downloads.request(
180+
joinpath(state.server, "dex/.well-known/openid-configuration"),
181+
method = "GET",
182+
output = output,
183+
throw = false,
184+
headers = ["Accept" => "application/json"],
185+
)
186+
187+
if response isa Downloads.Response && response.status == 200
188+
body = nothing
189+
content = String(take!(output))
190+
try
191+
body = JSON.parse(content)
192+
catch ex
193+
@debug "Request for well known configuration returned: ", content
194+
return false, "", ""
195+
end
196+
197+
return body !== nothing && haskey(body, "device_authorization_endpoint") && haskey(body, "grant_types_supported") && "urn:ietf:params:oauth:grant-type:device_code" in body["grant_types_supported"], body["device_authorization_endpoint"], body["token_endpoint"]
198+
end
199+
200+
return false, "", ""
201+
end
202+
177203
function step(state::NoAuthentication)::Union{RequestLogin, Failure}
204+
device_tokens_supported, device_endpoint, token_endpoint = get_openid_configuration(state)
205+
success, challenge, body_or_response = if device_tokens_supported
206+
fetch_device_code(state, device_endpoint)
207+
else
208+
initiate_browser_challenge(state)
209+
end
210+
if success
211+
return RequestLogin(state.server, challenge, body_or_response, token_endpoint)
212+
else
213+
return HttpError(body_or_reponse)
214+
end
215+
end
216+
217+
function fetch_device_code(state::NoAuthentication, device_endpoint::AbstractString)
178218
output = IOBuffer()
179-
# Try device flow first
180219
response = Downloads.request(
181-
joinpath(state.server, "dex/device/code"),
220+
device_endpoint,
182221
method = "POST",
183-
input = IOBuffer("client_id=device&scope=openid email profile offline_access"),
222+
input = IOBuffer("client_id=$(get(ENV, "JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID", "device"))&scope=openid email profile offline_access"),
184223
output = output,
185224
throw = false,
186225
headers = Dict("Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded"),
187226
)
188227
if response isa Downloads.Response && response.status == 200
189228
body = nothing
229+
content = String(take!(output))
190230
try
191-
body = JSON.parse(String(take!(output)))
231+
body = JSON.parse(content)
192232
catch ex
193-
@debug "Request for device code returned: ", body
233+
@debug "Request for device code returned: ", content
234+
return false, "", response
194235
end
195236

196237
if body !== nothing
197-
body["client_id"] = "device"
198-
return RequestLogin(state.server, "", body)
238+
body["client"] = "device"
239+
return true, "", body
199240
end
200241
end
242+
return false, "", response
243+
end
201244

245+
function initiate_browser_challenge(state::NoAuthentication)
246+
output = IOBuffer()
202247
challenge = Random.randstring(32)
203248
response = Downloads.request(
204249
string(state.server, "/challenge"),
@@ -208,9 +253,9 @@ function step(state::NoAuthentication)::Union{RequestLogin, Failure}
208253
throw = false,
209254
)
210255
if response isa Downloads.Response && response.status == 200
211-
return RequestLogin(state.server, challenge, String(take!(output)))
256+
return true, challenge, String(take!(output))
212257
else
213-
return HttpError(response)
258+
return false, challenge, response
214259
end
215260
end
216261

@@ -251,7 +296,7 @@ Base.show(io::IO, s::NeedRefresh) = print(io, "NeedRefresh($(s.server), <REDACTE
251296
function step(state::NeedRefresh)::Union{HasNewToken, NoAuthentication}
252297
refresh_token = state.token["refresh_token"]
253298
output = IOBuffer()
254-
is_device = get(state.token, "client_id", nothing) == "device"
299+
is_device = get(state.token, "client", nothing) == "device"
255300
response = Downloads.request(
256301
state.token["refresh_url"],
257302
method = "GET",
@@ -269,7 +314,7 @@ function step(state::NeedRefresh)::Union{HasNewToken, NoAuthentication}
269314
assert_dict_keys(body, "expires", "expires_at"; msg=msg)
270315
end
271316
if is_device
272-
body["client_id"] = "device"
317+
body["client"] = "device"
273318
# refresh_url and expires/expires_at will be present in this refreshed token
274319
# so no need to manually add them here
275320
end
@@ -339,11 +384,12 @@ struct RequestLogin <: State
339384
server::String
340385
challenge::String
341386
response::Union{String, Dict{String, Any}}
387+
token_endpoint::String
342388
end
343-
Base.show(io::IO, s::RequestLogin) = print(io, "RequestLogin($(s.server), <REDACTED>, $(s.response))")
389+
Base.show(io::IO, s::RequestLogin) = print(io, "RequestLogin($(s.server), <REDACTED>, $(s.response), $(s.token_endpoint))")
344390

345391
function step(state::RequestLogin)::Union{ClaimToken, Failure}
346-
is_device = state.response isa Dict{String, Any} && get(state.response, "client_id", nothing) == "device"
392+
is_device = state.response isa Dict{String, Any} && get(state.response, "client", nothing) == "device"
347393
url = if is_device
348394
string(state.response["verification_uri_complete"])
349395
else
@@ -353,9 +399,9 @@ function step(state::RequestLogin)::Union{ClaimToken, Failure}
353399
success = open_browser(url)
354400
if success && is_device
355401
# In case of device tokens, timeout for challenge is received in the initial request.
356-
return ClaimToken(state.server, state.challenge, state.response, Inf, time(), state.response["expires_in"], 2, 0, 10)
402+
return ClaimToken(state.server, state.challenge, state.response, Inf, time(), state.response["expires_in"], 2, 0, 10, state.token_endpoint)
357403
elseif success
358-
return ClaimToken(state.server, state.challenge, state.response)
404+
return ClaimToken(state.server, state.challenge, state.response, state.token_endpoint)
359405
else # this can only happen for the browser hook
360406
return GenericError("Failed to execute open_browser hook.")
361407
end
@@ -376,11 +422,12 @@ struct ClaimToken <: State
376422
poll_interval::Float64
377423
failures::Int
378424
max_failures::Int
425+
token_endpoint::String
379426
end
380-
Base.show(io::IO, s::ClaimToken) = print(io, "ClaimToken($(s.server), <REDACTED>, $(s.response), $(s.expiry), $(s.start_time), $(s.timeout), $(s.poll_interval), $(s.failures), $(s.max_failures))")
427+
Base.show(io::IO, s::ClaimToken) = print(io, "ClaimToken($(s.server), <REDACTED>, $(s.response), $(s.expiry), $(s.start_time), $(s.timeout), $(s.poll_interval), $(s.failures), $(s.max_failures), $(s.token_endpoint))")
381428

382-
ClaimToken(server, challenge, response, expiry = Inf, failures = 0) =
383-
ClaimToken(server, challenge, response, expiry, time(), 180, 2, failures, 10)
429+
ClaimToken(server, challenge, response, token_endpoint, expiry = Inf, failures = 0) =
430+
ClaimToken(server, challenge, response, expiry, time(), 180, 2, failures, 10, token_endpoint)
384431

385432
function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
386433
if time() > state.expiry || (time() - state.start_time)/1e6 > state.timeout # server-side or client-side timeout
@@ -394,13 +441,13 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
394441
sleep(state.poll_interval)
395442

396443
output = IOBuffer()
397-
is_device = state.response isa Dict{String, Any} && get(state.response, "client_id", nothing) == "device"
444+
is_device = state.response isa Dict{String, Any} && get(state.response, "client", nothing) == "device"
398445
if is_device
399446
output = IOBuffer()
400447
response = Downloads.request(
401-
joinpath(state.server, "dex/token"),
448+
state.token_endpoint,
402449
method = "POST",
403-
input = IOBuffer("client_id=device&scope=openid profile offline_access&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$(state.response["device_code"])"),
450+
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"])"),
404451
output = output,
405452
throw = false,
406453
headers = Dict("Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded"),
@@ -423,24 +470,24 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
423470
body = try
424471
JSON.parse(String(take!(output)))
425472
catch err
426-
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures)
473+
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.token_endpoint)
427474
end
428475

429476
if haskey(body, "token")
430477
return HasNewToken(state.server, body["token"])
431478
elseif haskey(body, "expiry") # time at which the response/challenge pair will expire on the server
432-
return ClaimToken(state.server, state.challenge, state.response, body["expiry"], state.start_time, state.timeout, state.poll_interval, state.failures, state.max_failures)
479+
return ClaimToken(state.server, state.challenge, state.response, body["expiry"], state.start_time, state.timeout, state.poll_interval, state.failures, state.max_failures, state.token_endpoint)
433480
else
434-
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures)
481+
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.token_endpoint)
435482
end
436483
elseif response isa Downloads.Response && response.status == 200
437484
body = JSON.parse(String(take!(output)))
438-
body["client_id"] = "device"
485+
body["client"] = "device"
439486
body["expires"] = body["expires_in"] + Int(floor(time()))
440487
body["refresh_url"] = joinpath(state.server, "auth/renew/token.toml/v2/") # Need to be careful with auth suffix, if set
441488
return HasNewToken(state.server, body)
442489
elseif response isa Downloads.Response && response.status in [401, 400] && is_device
443-
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures)
490+
return ClaimToken(state.server, state.challenge, state.response, state.expiry, state.start_time, state.timeout, state.poll_interval, state.failures + 1, state.max_failures, state.token_endpoint)
444491
else
445492
return HttpError(response)
446493
end
@@ -492,8 +539,7 @@ is_new_auth_mechanism() =
492539
is_token_valid(toml) =
493540
get(toml, "id_token", nothing) isa AbstractString &&
494541
get(toml, "refresh_token", nothing) isa AbstractString &&
495-
(get(toml, "refresh_url", nothing) isa AbstractString ||
496-
get(toml, "client_id", nothing) == "device") &&
542+
get(toml, "refresh_url", nothing) isa AbstractString &&
497543
(get(toml, "expires_at", nothing) isa Union{Integer, AbstractFloat} ||
498544
get(toml, "expires", nothing) isa Union{Integer, AbstractFloat})
499545

0 commit comments

Comments
 (0)