@@ -174,31 +174,76 @@ struct NoAuthentication <: State
174174end
175175Base. 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+
177203function 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
215260end
216261
@@ -251,7 +296,7 @@ Base.show(io::IO, s::NeedRefresh) = print(io, "NeedRefresh($(s.server), <REDACTE
251296function 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
342388end
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
345391function 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
379426end
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
385432function 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() =
492539is_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