Skip to content

Commit

Permalink
Pkg Client auth: toward a more standard-worth protocol
Browse files Browse the repository at this point in the history
Use names from https://tools.ietf.org/html/rfc6749#section-5.1:

- access_token: an HTTP Auth Bearer token
- expires_in: a relative expiration time
- expires: an absolute expiration time
- refresh_token: an HTTP Auth Bearer token to refresh
- refresh_url: where to get a new auth.toml file

Change the auth file name to `auth.toml` from `token.toml`

Only send the access_token for regular requests

Only send the refresh_token for refresh requests
  • Loading branch information
StefanKarpinski committed Dec 10, 2019
1 parent 0dbd885 commit c5e71fd
Showing 1 changed file with 84 additions and 84 deletions.
168 changes: 84 additions & 84 deletions src/PlatformEngines.jl
Original file line number Diff line number Diff line change
Expand Up @@ -589,105 +589,106 @@ function parse_tar_list(output::AbstractString)
return Sys.iswindows() ? replace.(lines, ['/' => '\\']) : lines
end

function get_auth_token(url::AbstractString; verbose::Bool = false)
function get_auth_header(url::AbstractString; verbose::Bool = false)
server = pkg_server()
server === nothing && return
startswith(url, server) || return
# find and parse token file
# find and parse auth file
m = match(r"(\w+)://([^\\/]+)$", server)
if m === nothing
@warn "malformed Pkg server value" server=server
return
end
proto, host = m.captures
# refuse to send auth info over unencrypted connections
lowercase(proto) === "https" || return
token_file = joinpath(depots1(), "servers", host, "token.toml")
isfile(token_file) || return
first_time = true
while true
token_info = try
TOML.parsefile(token_file)
catch err
@warn "malformed auth token file" file=token_file err=err
return
end
# construct auth_token string
auth = ""
if haskey(token_info, "refresh_token")
auth = token_info["refresh_token"]
end
if haskey(token_info, "id_token")
isempty(auth) || (auth *= ":")
auth *= token_info["id_token"]
end
if isempty(auth)
@warn "token file without ID or refresh tokens" file=token_file
return
end
auth_token = base64encode(auth)
first_time || return auth_token
first_time = false
# handle token expiration and refresh
if haskey(token_info, "expires")
expires = token_info["expires"]::Integer
elseif haskey(token_info, "expires_in")
expires = mtime(token_file)
expires += token_info["expires_in"]::Integer
end
# renew token if it will expire within the next hour
time_now = time()
if !@isdefined(expires) || expires > time_now + 3600
return base64encode(auth_token)
end
if !haskey(token_info, "refresh_url")
if expires  time_now
@warn "expired token without refresh URL" file=token_file
end
return
end
refresh_url = token_info["refresh_url"]
if !startswith(lowercase(refresh_url), "https://")
@warn "ignoring insecure auth refresh URL" url=refresh_url
return
end
verbose && @info "Refreshing expired auth token..." file=token_file
tmp = tempname()
try download(refresh_url, tmp, auth_token=auth_token, verbose=verbose)
catch err
@warn "token refresh failure" file=token_file url=refresh_url err=err
rm(tmp, force=true)
return
end
new_token_info = try TOML.parsefile(tmp)
catch err
@warn "discarding malformed token file" url=refresh_url err=err
rm(tmp, force=true)
return
auth_file = joinpath(depots1(), "servers", host, "auth.toml")
isfile(auth_file) || return
# TODO: check for insecure auth file permissions
if lowercase(proto) != "https"
@warn "refusing to send auth info over insecure connection" url=url
return
end
# parse the auth file
auth_info = try
TOML.parsefile(auth_file)
catch err
@error "malformed auth file" file=auth_file err=err
return
end
# check for an auth token
if !haskey(auth_info, "access_token")
@warn "auth file without access_token field" file=auth_file
return
end
auth_header = "Authorization: Bearer $(auth_info["access_token"])"
# handle token expiration and refresh
expires = Inf
if haskey(auth_info, "expires")
expires = min(expires, auth_info["expires"]::Integer)
end
if haskey(auth_info, "expires_in")
expires = min(expires, mtime(auth_file) + auth_info["expires_in"]::Integer)
end
# if token is good until ten minutes from now, use it
time_now = time()
if expires time_now + 10*60 # ten minutes
return auth_header
end
if !haskey(auth_info, "refresh_url") || !haskey(auth_info, "refresh_token")
if expires  time_now
@warn "expired auth without refresh keys" file=auth_file
end
if haskey(new_token_info, "expires_in")
expires_in = new_token_info["expires_in"]
if expires_in isa Number
expires = floor(Int64, time_now + expires_in)
# overwrite sent expires value;
# this avoids clock skew issues
new_token_info["expires"] = expires
end
# try it anyway since we can't refresh
return auth_header
end
refresh_url = auth_info["refresh_url"]
if !startswith(lowercase(refresh_url), "https://")
@warn "ignoring insecure auth refresh URL" url=refresh_url
return auth_header
end
verbose && @info "Refreshing expired auth token..." file=auth_file
tmp = tempname()
refresh_auth = "Authorization: Bearer $(auth_info["refresh_token"])"
try download(refresh_url, tmp, auth_header=refresh_auth, verbose=verbose)
catch err
@warn "token refresh failure" file=auth_file url=refresh_url err=err
rm(tmp, force=true)
return
end
auth_info = try TOML.parsefile(tmp)
catch err
@warn "discarding malformed auth file" url=refresh_url err=err
rm(tmp, force=true)
return auth_header
end
if !haskey(auth_info, "access_token")
if haskey(auth_info, "refresh_token")
auth_info["refresh_token"] = "*"^64
end
open(tmp, write=true) do io
TOML.print(io, new_token_info, sorted=true)
@warn "discarding auth file without access token" auth=auth_info
rm(tmp, force=true)
return auth_header
end
if haskey(auth_info, "expires_in")
expires_in = auth_info["expires_in"]
if expires_in isa Number
expires = floor(Int64, time_now + expires_in)
# overwrite expires (avoids clock skew issues)
auth_info["expires"] = expires
end
mv(tmp, token_file, force=true)
# try again from the top
end
open(tmp, write=true) do io
TOML.print(io, auth_info, sorted=true)
end
mv(tmp, auth_file, force=true)
return "Authorization: Bearer $(auth_info["access_token"])"
end

"""
download(
url::AbstractString,
dest::AbstractString;
verbose::Bool = false,
auth_token::Union{AbstractString, Nothing} = nothing,
auth_header::Union{AbstractString, Nothing} = nothing,
)
Download file located at `url`, store it at `dest`, continuing if `dest`
Expand All @@ -697,15 +698,14 @@ function download(
url::AbstractString,
dest::AbstractString;
verbose::Bool = false,
auth_token::Union{AbstractString, Nothing} = nothing,
auth_header::Union{AbstractString, Nothing} = nothing,
)
if auth_token === nothing
auth_token = get_auth_token(url, verbose=verbose)
if auth_header === nothing
auth_header = get_auth_header(url, verbose=verbose)
end
if auth_token === nothing
if auth_header === nothing
download_cmd = gen_download_cmd(url, dest)
else
auth_header = "Authorization: Basic $auth_token"
download_cmd = gen_download_cmd(url, dest, auth_header)
end
if verbose
Expand Down

0 comments on commit c5e71fd

Please sign in to comment.