Skip to content

Commit 4974a18

Browse files
committed
namespace device config response; use list of auth_flows
1 parent 0dd85e7 commit 4974a18

File tree

3 files changed

+41
-49
lines changed

3 files changed

+41
-49
lines changed

docs/auth-flows.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ For a valid implementation of the configuration endpoint, the package server:
9898

9999
If the response is invalid (non-`200` code or an invalid JSON object), PkgAuthentication will assume that the server only supports the Classic Authentication Flow, and proceed accordingly.
100100

101+
When device authentication is not supported by the server the response body MAY contain the following JSON data:
102+
101103
```json
102104
{
103-
"device_flow_supported": false
105+
"auth_flows": ["classic"]
104106
}
105107
```
106108

@@ -110,10 +112,10 @@ When device authentication _is_ supported by the server, the response body MUST
110112

111113
```json
112114
{
113-
"device_flow_supported": true,
114-
"refresh_url": "https://juliahub.com/auth/renew/token.toml/device/",
115+
"auth_flows": ["classic", "device"],
116+
"device_token_refresh_url": "https://juliahub.com/auth/renew/token.toml/device/",
115117
"device_authorization_endpoint": "https://auth.juliahub.com/auth/device/code",
116-
"token_endpoint": "https://auth.juliahub.com/auth/token"
118+
"device_token_endpoint": "https://auth.juliahub.com/auth/token"
117119
}
118120
```
119121

@@ -210,11 +212,11 @@ The flow goes through the following steps:
210212

211213
2. The client should open `verfication_uri_complete` in the browser so that the user can login and approve the authorization request. The package server SHOULD provide an interface for the user to login and approve or deny the authorization request.
212214

213-
3. The client should now poll for completion of the authorization request. It can do so by making a `POST` request to the `token_endpoint` with the same headers as was used for the `device_authorization_endpoint` call. The body of the request MUST contain the url encoded `client_id` and `scope`. The values of these parameters must match the values sent for `device_authorization_endpoint`. In addition to these two parameters, a `grant_type` and `device_code` parameter must also be included with values `urn:ietf:params:oauth:grant-type:device_code` and the `device_code` response value from the `device_authorization_endpoint` call, respectively.
215+
3. The client should now poll for completion of the authorization request. It can do so by making a `POST` request to the `device_token_endpoint` with the same headers as was used for the `device_authorization_endpoint` call. The body of the request MUST contain the url encoded `client_id` and `scope`. The values of these parameters must match the values sent for `device_authorization_endpoint`. In addition to these two parameters, a `grant_type` and `device_code` parameter must also be included with values `urn:ietf:params:oauth:grant-type:device_code` and the `device_code` response value from the `device_authorization_endpoint` call, respectively.
214216

215-
While the user hasn't finished responding to the authorization request or has denied the authorization request, the `token_endpoint` response status must be `401` or `400`.
217+
While the user hasn't finished responding to the authorization request or has denied the authorization request, the `device_token_endpoint` response status must be `401` or `400`.
216218

217-
When the user approves the authorization request, the `token_endpoint` response status must be 200 with a JSON body containing the `access_token`, `id_token`, `refresh_token` and `expires_in`. Example:
219+
When the user approves the authorization request, the `device_token_endpoint` response status must be 200 with a JSON body containing the `access_token`, `id_token`, `refresh_token` and `expires_in`. Example:
218220

219221
```json
220222
{
@@ -228,7 +230,7 @@ The flow goes through the following steps:
228230

229231
4. The client must generate the `auth.toml` file with the above values. The following extra key/values must be added by the client to the auth.toml:
230232
- `expires_at: <expires_in> + <time()>` This value is required to determine whether the token is expired and needs refresh. This is missing in the token response so it must be added by summing the `expires_in` value with the current timestamp.
231-
- `refresh_url` This value is also missing in the device token response but is necessary for refreshing expired tokens. This field must be added with value same as the `refresh_url` from the package server authentication configuration endpoint response.
233+
- `refresh_url` This value is also missing in the device token response but is necessary for refreshing expired tokens. This field must be added with value same as the `device_token_refresh_url` from the package server authentication configuration endpoint response.
232234

233235
#### Client ID for device authentication flow
234236

src/PkgAuthentication.jl

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,14 @@ end
186186
# Query the /auth/configuration endpoint to get the refresh url and
187187
# device authentication endpoints. Returns a Dict with the following
188188
# 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
189+
# - `auth_flows`::Vector{String}: The authentication mechanisms supported
190+
# by the server. Eg: ["classic", "device"]
191+
# - `device_token_refresh_url`::String: The refresh URL for refreshing the auth
192192
# token
193193
# - `device_authorization_endpoint`::String: The endpoint that must
194194
# be called to initiate device flow authentication. This field is
195195
# only present when device flow is enabled on the server.
196-
# - `token_endpoint`::String: The endpoint that should be called to
196+
# - `device_token_endpoint`::String: The endpoint that should be called to
197197
# retrieve the authentication token after the user has approved
198198
# the authorization request. This field is only present when device
199199
# flow is enabled on the server.
@@ -208,39 +208,34 @@ function get_auth_configuration(state::NoAuthentication)
208208
headers = ["Accept" => "application/json"],
209209
)
210210

211-
def_resp = Dict{String, Any}(
212-
"device_flow_supported" => false,
213-
)
214-
215211
if response isa Downloads.Response && response.status == 200
216212
body = nothing
217213
content = String(take!(output))
218214
try
219215
body = JSON.parse(content)
220216
catch ex
221217
@debug "Request for well known configuration returned: ", content
222-
return def_resp
218+
return Dict{String, Any}()
223219
end
224220

225221
if body !== nothing
226-
@assert haskey(body, "device_flow_supported")
227-
@assert (body["device_flow_supported"] && haskey(body, "device_authorization_endpoint") && haskey(body, "token_endpoint") && haskey(body, "refresh_url")) || !body["device_flow_supported"]
222+
@assert !haskey(body, "auth_flows") || !("device" in body["auth_flows"]) || (haskey(body, "device_authorization_endpoint") && haskey(body, "device_token_endpoint") && haskey(body, "device_token_refresh_url"))
228223
return body
229224
end
230225
end
231226

232-
return def_resp
227+
return Dict{String, Any}()
233228
end
234229

235230
function step(state::NoAuthentication)::Union{RequestLogin, Failure}
236231
auth_config = get_auth_configuration(state)
237-
success, challenge, body_or_response = if auth_config["device_flow_supported"]
232+
success, challenge, body_or_response = if "device" in get(auth_config, "auth_flows", [])
238233
fetch_device_code(state, auth_config["device_authorization_endpoint"])
239234
else
240235
initiate_browser_challenge(state)
241236
end
242237
if success
243-
return RequestLogin(state.server, state.auth_suffix, challenge, body_or_response, get(auth_config, "token_endpoint", ""), get(auth_config, "refresh_url", ""))
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", ""))
244239
else
245240
return HttpError(body_or_response)
246241
end
@@ -415,13 +410,13 @@ struct RequestLogin <: State
415410
auth_suffix::String
416411
challenge::String
417412
response::Union{String, Dict{String, Any}}
418-
token_endpoint::String
419-
refresh_url::String
413+
device_token_endpoint::String
414+
device_token_refresh_url::String
420415
end
421-
Base.show(io::IO, s::RequestLogin) = print(io, "RequestLogin($(s.server), $(s.auth_suffix), <REDACTED>, $(s.response), $(s.token_endpoint), $(s.refresh_url))")
416+
Base.show(io::IO, s::RequestLogin) = print(io, "RequestLogin($(s.server), $(s.auth_suffix), <REDACTED>, $(s.response), $(s.device_token_endpoint), $(s.device_token_refresh_url))")
422417

423418
function step(state::RequestLogin)::Union{ClaimToken, Failure}
424-
is_device = !isempty(state.token_endpoint)
419+
is_device = !isempty(state.device_token_endpoint)
425420
url = if is_device
426421
string(state.response["verification_uri_complete"])
427422
else
@@ -431,9 +426,9 @@ function step(state::RequestLogin)::Union{ClaimToken, Failure}
431426
success = open_browser(url)
432427
if success && is_device
433428
# In case of device tokens, timeout for challenge is received in the initial request.
434-
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)
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)
435430
elseif success
436-
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.token_endpoint, state.refresh_url)
431+
return ClaimToken(state.server, state.auth_suffix, state.challenge, state.response, state.device_token_endpoint, state.device_token_refresh_url)
437432
else # this can only happen for the browser hook
438433
return GenericError("Failed to execute open_browser hook.")
439434
end
@@ -455,13 +450,13 @@ struct ClaimToken <: State
455450
poll_interval::Float64
456451
failures::Int
457452
max_failures::Int
458-
token_endpoint::String
459-
refresh_url::String
453+
device_token_endpoint::String
454+
device_token_refresh_url::String
460455
end
461-
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))")
456+
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.device_token_endpoint), $(s.device_token_refresh_url))")
462457

463-
ClaimToken(server, auth_suffix, challenge, response, token_endpoint, refresh_url, expiry = Inf, failures = 0) =
464-
ClaimToken(server, auth_suffix, challenge, response, expiry, time(), 180, 2, failures, 10, token_endpoint, refresh_url)
458+
ClaimToken(server, auth_suffix, challenge, response, device_token_endpoint, device_token_refresh_url, expiry = Inf, failures = 0) =
459+
ClaimToken(server, auth_suffix, challenge, response, expiry, time(), 180, 2, failures, 10, device_token_endpoint, device_token_refresh_url)
465460

466461
function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
467462
if time() > state.expiry || (time() - state.start_time)/1e6 > state.timeout # server-side or client-side timeout
@@ -475,11 +470,11 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
475470
sleep(state.poll_interval)
476471

477472
output = IOBuffer()
478-
is_device = !isempty(state.token_endpoint)
473+
is_device = !isempty(state.device_token_endpoint)
479474
if is_device
480475
output = IOBuffer()
481476
response = Downloads.request(
482-
state.token_endpoint,
477+
state.device_token_endpoint,
483478
method = "POST",
484479
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"])"),
485480
output = output,
@@ -504,24 +499,24 @@ function step(state::ClaimToken)::Union{ClaimToken, HasNewToken, Failure}
504499
body = try
505500
JSON.parse(String(take!(output)))
506501
catch err
507-
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)
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)
508503
end
509504

510505
if haskey(body, "token")
511506
return HasNewToken(state.server, body["token"])
512507
elseif haskey(body, "expiry") # time at which the response/challenge pair will expire on the server
513-
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)
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)
514509
else
515-
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)
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)
516511
end
517512
elseif response isa Downloads.Response && response.status == 200
518513
body = JSON.parse(String(take!(output)))
519514
body["expires"] = body["expires_in"] + Int(floor(time()))
520515
body["expires_at"] = body["expires"]
521-
body["refresh_url"] = state.refresh_url
516+
body["refresh_url"] = state.device_token_refresh_url
522517
return HasNewToken(state.server, body)
523518
elseif response isa Downloads.Response && response.status in [401, 400] && is_device
524-
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)
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)
525520
else
526521
return HttpError(response)
527522
end

test/authserver.jl

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,15 @@ end
114114

115115
function auth_configuration(req)
116116
if MODE[] == LEGACY_MODE
117-
return HTTP.Response(
118-
200,
119-
""" {
120-
"device_flow_supported": false
121-
} """,
122-
)
117+
return HTTP.Response(200)
123118
else
124119
return HTTP.Response(
125120
200,
126121
""" {
127-
"device_flow_supported": true,
128-
"refresh_url": "http://localhost:$PORT/auth/renew/token.toml/device/",
122+
"auth_flows": ["classic", "device"],
123+
"device_token_refresh_url": "http://localhost:$PORT/auth/renew/token.toml/device/",
129124
"device_authorization_endpoint": "http://localhost:$PORT/auth/device/code",
130-
"token_endpoint": "http://localhost:$PORT/auth/token"
125+
"device_token_endpoint": "http://localhost:$PORT/auth/token"
131126
} """,
132127
)
133128
end

0 commit comments

Comments
 (0)