@@ -183,48 +183,66 @@ function should_use_device_auth()
183183 return ! isempty (get_device_auth_client_id ())
184184end
185185
186- function get_openid_configuration (state:: NoAuthentication )
186+ # Query the /auth/configuration endpoint to get the refresh url and
187+ # device authentication endpoints. Returns a Dict with the following
188+ # fields:
189+ # - `device_flow_supported`::Bool: Indicates whether device flow is
190+ # enabled on the server.
191+ # - `refresh_url`::String: The refresh URL for refreshing the auth
192+ # token
193+ # - `device_authorization_endpoint`::String: The endpoint that must
194+ # be called to initiate device flow authentication. This field is
195+ # only present when device flow is enabled on the server.
196+ # - `token_endpoint`::String: The endpoint that should be called to
197+ # retrieve the authentication token after the user has approved
198+ # the authorization request. This field is only present when device
199+ # flow is enabled on the server.
200+ function get_auth_configuration (state:: NoAuthentication )
187201 output = IOBuffer ()
202+ auth_suffix = isempty (state. auth_suffix) ? " auth" : state. auth_suffix
188203 response = Downloads. request (
189- joinpath (state. server, " .well-known/openid- configuration" ),
204+ joinpath (state. server, auth_suffix, " configuration" ),
190205 method = " GET" ,
191206 output = output,
192207 throw = false ,
193208 headers = [" Accept" => " application/json" ],
194209 )
195210
211+ def_resp = Dict {String, Any} (
212+ " device_flow_supported" => false ,
213+ " refresh_url" => joinpath (state. server, auth_suffix, " renew/token.toml/v2/" )
214+ )
215+
196216 if response isa Downloads. Response && response. status == 200
197217 body = nothing
198218 content = String (take! (output))
199219 try
200220 body = JSON. parse (content)
201221 catch ex
202222 @debug " Request for well known configuration returned: " , content
203- return false , " " , " "
223+ return def_resp
204224 end
205225
206226 if body != = nothing
207- return true , body[" device_authorization_endpoint" ], body[" token_endpoint" ]
227+ @assert haskey (body, " device_flow_supported" )
228+ @assert haskey (body, " refresh_url" )
229+ @assert (body[" device_flow_supported" ] && haskey (body, " device_authorization_endpoint" ) && haskey (body, " token_endpoint" )) || ! body[" device_flow_supported" ]
230+ return body
208231 end
209232 end
210233
211- return false , " " , " "
234+ return def_resp
212235end
213236
214237function step (state:: NoAuthentication ):: Union{RequestLogin, Failure}
215- token_endpoint = " "
216- device_endpoint = " "
217- if should_use_device_auth ()
218- s, device_endpoint, token_endpoint = get_openid_configuration (state)
219- s || GenericError (" Unable to get device and token endpoints" )
220- end
221- success, challenge, body_or_response = if should_use_device_auth ()
222- fetch_device_code (state, device_endpoint)
238+ auth_config = get_auth_configuration (state)
239+ success, challenge, body_or_response = if auth_config[" device_flow_supported" ]
240+ fetch_device_code (state, auth_config[" device_authorization_endpoint" ])
223241 else
224242 initiate_browser_challenge (state)
225243 end
226244 if success
227- return RequestLogin (state. server, state. auth_suffix, challenge, body_or_response, token_endpoint)
245+ return RequestLogin (state. server, state. auth_suffix, challenge, body_or_response, get (auth_config, " token_endpoint" , " " ), auth_config[ " refresh_url " ] )
228246 else
229247 return HttpError (body_or_response)
230248 end
@@ -251,7 +269,6 @@ function fetch_device_code(state::NoAuthentication, device_endpoint::AbstractStr
251269 end
252270
253271 if body != = nothing
254- body[" client" ] = " device"
255272 return true , " " , body
256273 end
257274 end
@@ -314,7 +331,6 @@ Base.show(io::IO, s::NeedRefresh) = print(io, "NeedRefresh($(s.server), $(s.auth
314331function step (state:: NeedRefresh ):: Union{HasNewToken, NoAuthentication}
315332 refresh_token = state. token[" refresh_token" ]
316333 output = IOBuffer ()
317- is_device = get (state. token, " client" , nothing ) == " device"
318334 response = Downloads. request (
319335 state. token[" refresh_url" ],
320336 method = " GET" ,
@@ -331,17 +347,15 @@ function step(state::NeedRefresh)::Union{HasNewToken, NoAuthentication}
331347 assert_dict_keys (body, " expires_in" ; msg= msg)
332348 assert_dict_keys (body, " expires" , " expires_at" ; msg= msg)
333349 end
334- if is_device
335- body[" client" ] = " device"
336- # refresh_url and expires/expires_at will be present in this refreshed token
337- # so no need to manually add them here
338- end
350+ @info (" Successfully refreshed token" )
339351 return HasNewToken (state. server, body)
340352 catch err
341353 @debug " invalid body received while refreshing token" exception= (err, catch_backtrace ())
342354 end
355+ @info " Did not refresh token, could not json parse " , response
343356 return NoAuthentication (state. server, state. auth_suffix)
344357 else
358+ @info " Did not refresh token, got non 200 response " , response
345359 @debug " request for refreshing token failed" response
346360 return NoAuthentication (state. server, state. auth_suffix)
347361 end
@@ -404,11 +418,12 @@ struct RequestLogin <: State
404418 challenge:: String
405419 response:: Union{String, Dict{String, Any}}
406420 token_endpoint:: String
421+ refresh_url:: String
407422end
408- Base. show (io:: IO , s:: RequestLogin ) = print (io, " RequestLogin($(s. server) , $(s. auth_suffix) , <REDACTED>, $(s. response) , $(s. token_endpoint) )" )
423+ Base. show (io:: IO , s:: RequestLogin ) = print (io, " RequestLogin($(s. server) , $(s. auth_suffix) , <REDACTED>, $(s. response) , $(s. token_endpoint) , $(s . refresh_url) )" )
409424
410425function step (state:: RequestLogin ):: Union{ClaimToken, Failure}
411- is_device = state . response isa Dict{String, Any} && get (state. response, " client " , nothing ) == " device "
426+ is_device = ! isempty (state. token_endpoint)
412427 url = if is_device
413428 string (state. response[" verification_uri_complete" ])
414429 else
@@ -418,9 +433,9 @@ function step(state::RequestLogin)::Union{ClaimToken, Failure}
418433 success = open_browser (url)
419434 if success && is_device
420435 # In case of device tokens, timeout for challenge is received in the initial request.
421- return ClaimToken (state. server, state. auth_suffix, state. challenge, state. response, Inf , time (), state. response[" expires_in" ], 2 , 0 , 10 , state. token_endpoint)
436+ return ClaimToken (state. server, state. auth_suffix, state. challenge, state. response, Inf , time (), state. response[" expires_in" ], 2 , 0 , 10 , state. token_endpoint, state . refresh_url )
422437 elseif success
423- return ClaimToken (state. server, state. auth_suffix, state. challenge, state. response, state. token_endpoint)
438+ return ClaimToken (state. server, state. auth_suffix, state. challenge, state. response, state. token_endpoint, state . refresh_url )
424439 else # this can only happen for the browser hook
425440 return GenericError (" Failed to execute open_browser hook." )
426441 end
@@ -443,11 +458,12 @@ struct ClaimToken <: State
443458 failures:: Int
444459 max_failures:: Int
445460 token_endpoint:: String
461+ refresh_url:: String
446462end
447- Base. show (io:: IO , s:: ClaimToken ) = print (io, " ClaimToken($(s. server) , $(s. auth_suffix) , <REDACTED>, $(s. response) , $(s. expiry) , $(s. start_time) , $(s. timeout) , $(s. poll_interval) , $(s. failures) , $(s. max_failures) , $(s. token_endpoint) )" )
463+ Base. show (io:: IO , s:: ClaimToken ) = print (io, " ClaimToken($(s. server) , $(s. auth_suffix) , <REDACTED>, $(s. response) , $(s. expiry) , $(s. start_time) , $(s. timeout) , $(s. poll_interval) , $(s. failures) , $(s. max_failures) , $(s. token_endpoint) , $(s . refresh_url) )" )
448464
449- ClaimToken (server, auth_suffix, challenge, response, token_endpoint, expiry = Inf , failures = 0 ) =
450- ClaimToken (server, auth_suffix, challenge, response, expiry, time (), 180 , 2 , failures, 10 , token_endpoint)
465+ ClaimToken (server, auth_suffix, challenge, response, token_endpoint, refresh_url, expiry = Inf , failures = 0 ) =
466+ ClaimToken (server, auth_suffix, challenge, response, expiry, time (), 180 , 2 , failures, 10 , token_endpoint, refresh_url )
451467
452468function step (state:: ClaimToken ):: Union{ClaimToken, HasNewToken, Failure}
453469 if time () > state. expiry || (time () - state. start_time)/ 1e6 > state. timeout # server-side or client-side timeout
@@ -461,7 +477,7 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
461477 sleep (state. poll_interval)
462478
463479 output = IOBuffer ()
464- is_device = state . response isa Dict{String, Any} && get (state. response, " client " , nothing ) == " device "
480+ is_device = ! isempty (state. token_endpoint)
465481 if is_device
466482 output = IOBuffer ()
467483 response = Downloads. request (
@@ -490,25 +506,25 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
490506 body = try
491507 JSON. parse (String (take! (output)))
492508 catch err
493- 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. token_endpoint)
509+ 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. token_endpoint, state . refresh_url )
494510 end
495511
496512 if haskey (body, " token" )
497513 return HasNewToken (state. server, body[" token" ])
498514 elseif haskey (body, " expiry" ) # time at which the response/challenge pair will expire on the server
499- 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. token_endpoint)
515+ 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. token_endpoint, state . refresh_url )
500516 else
501- 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. token_endpoint)
517+ 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. token_endpoint, state . refresh_url )
502518 end
503519 elseif response isa Downloads. Response && response. status == 200
504520 body = JSON. parse (String (take! (output)))
505- body[" client" ] = " device"
506521 body[" expires" ] = body[" expires_in" ] + Int (floor (time ()))
507522 body[" expires_at" ] = body[" expires" ]
508- body[" refresh_url" ] = joinpath (state. server, " auth/renew/token.toml/device/" ) # Need to be careful with auth suffix, if set
523+ @info (" Setting refresh url to " , state. refresh_url)
524+ body[" refresh_url" ] = state. refresh_url
509525 return HasNewToken (state. server, body)
510526 elseif response isa Downloads. Response && response. status in [401 , 400 ] && is_device
511- 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. token_endpoint)
527+ 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. token_endpoint, state . refresh_url )
512528 else
513529 return HttpError (response)
514530 end
0 commit comments