Skip to content

Commit 9ea88e5

Browse files
Kenofredrikekregiordanostaticfloat
authored
Implement new GitHub device authentication flow (#937)
Co-authored-by: Fredrik Ekre <[email protected]> Co-authored-by: Mosè Giordano <[email protected]> Co-authored-by: Mosè Giordano <[email protected]> Co-authored-by: Elliot Saba <[email protected]>
1 parent f7110ce commit 9ea88e5

File tree

2 files changed

+64
-59
lines changed

2 files changed

+64
-59
lines changed

src/wizard/github.jl

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -27,77 +27,85 @@ function github_auth(;allow_anonymous::Bool=true)
2727
return _github_auth[]
2828
end
2929

30-
function obtain_token(; ins=stdin, outs=stdout, github_api=GitHub.DEFAULT_API)
30+
function obtain_token(; outs=stdout, github_api=GitHub.DEFAULT_API)
3131
println(outs)
32-
printstyled(outs, "Creating a github access token\n", bold=true)
32+
printstyled(outs, "Authenticating with GitHub\n", bold=true)
33+
34+
35+
@label retry
36+
37+
headers = Dict{String, String}("User-Agent"=>"BinaryBuilder-jl",
38+
"Accept"=>"application/json")
39+
40+
# Request device authentication flow for BinaryBuilder OAauth APP
41+
resp = HTTP.post("https://github.com/login/device/code", headers,
42+
"client_id=2a955f9ca1a7c5b720f3&scope=public_repo")
43+
if resp.status != 200
44+
GitHub.handle_response_error(resp)
45+
end
46+
reply = JSON.parse(HTTP.payload(resp, String))
47+
3348
println(outs, """
34-
To continue, we need to create a GitHub access token, so that we can do
35-
things like fork Yggdrasil and create pull requests against it. To create
36-
an access token, you will be asked to enter your GitHub credentials:
37-
""")
49+
To continue, we need to authenticate you with GitHub. Please navigate to
50+
the following page in your browser and enter the code below:
3851
39-
params = Dict(
40-
# The only thing we really need to do is write to public repos
41-
"scopes" => [
42-
"public_repo",
43-
],
44-
"note" => "BinaryBuilder.jl generated token at $(now())",
45-
"note_url" => "https://github.com/JuliaPackaging/BinaryBuilder.jl",
46-
"fingerprint" => randstring(40)
47-
)
52+
$(HTTP.URIs.unescapeuri(reply["verification_uri"]))
53+
54+
#############
55+
# $(reply["user_code"]) #
56+
#############
57+
""")
4858

59+
interval = reply["interval"]
60+
device_code = reply["device_code"]
4961
while true
50-
if !isopen(ins) || (!Sys.iswindows() && !isopen(stdin))
51-
# We have to check also `stdin` because `_getpass` ignores `ins` and
52-
# always uses `stdin` on Unices.
53-
error("Cannot read from input stream")
54-
end
62+
# Poll for completion
63+
sleep(interval)
64+
resp = HTTP.post("https://github.com/login/oauth/access_token", headers,
65+
"client_id=2a955f9ca1a7c5b720f3&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$device_code")
5566

56-
user = nonempty_line_prompt("Username", "GitHub username:", ins=ins, outs=outs)
57-
password = nonempty_line_prompt("Password", "GitHub password:"; ins=ins, outs=outs, echo=false)
58-
59-
# Shuffle this junk off to the GH API
60-
headers = Dict{String, String}("User-Agent"=>"BinaryBuilder-jl")
61-
auth = GitHub.UsernamePassAuth(user, password)
62-
resp = GitHub.gh_post(
63-
github_api,
64-
"/authorizations";
65-
headers=headers,
66-
params=params,
67-
handle_error=false,
68-
auth=auth,
69-
)
70-
if resp.status == 401 && startswith(strip(HTTP.getkv(resp.headers, "X-GitHub-OTP", "")), "required")
71-
otp_code = nonempty_line_prompt("Authorization code", "Two-factor authentication in use, enter auth code:")
72-
resp = GitHub.gh_post(
73-
github_api,
74-
"/authorizations";
75-
headers=merge(headers, Dict("X-GitHub-OTP" => otp_code)),
76-
params=params,
77-
handle_error=false,
78-
auth = auth,
79-
)
80-
end
81-
if resp.status == 401
82-
printstyled(outs, "Invalid credentials!", color=:red)
83-
println(outs)
84-
continue
85-
end
8667
if resp.status != 200
8768
GitHub.handle_response_error(resp)
8869
end
89-
token = JSON.parse(HTTP.payload(resp, String))["token"]
70+
71+
72+
token_reply = JSON.parse(HTTP.payload(resp, String))
73+
if haskey(token_reply, "error")
74+
error_kind = token_reply["error"]
75+
if error_kind == "authorization_pending"
76+
continue
77+
elseif error_kind == "slow_down"
78+
@warn "GitHub Auth rate limit exceeded. Waiting 10s. (This shouldn't happen)"
79+
sleep(10)
80+
elseif error_kind == "expired_token"
81+
@error "Token request expired. Starting over!"
82+
@goto retry
83+
elseif error_kind == "access_denied"
84+
@error "Authentication request canceled by user. Starting over!"
85+
@goto retry
86+
elseif error_kind in ("unsupported_grant_type",
87+
"incorrect_client_credentials", "incorrect_device_code")
88+
error("Received error kind $(error_kind). Please file an issue.")
89+
else
90+
error("Unexpected GitHub login error $(error_kind)")
91+
end
92+
end
93+
94+
token = token_reply["access_token"]
9095

9196
print(outs, strip("""
9297
Successfully obtained GitHub authorization token!
93-
This token will be used for the rest of this BB session, however if you
94-
wish to use this token permanently, you may set it in your environment
95-
as via the following line in your """))
98+
This token will be used for the rest of this BB session.
99+
You will have to re-authenticate for any future session.
100+
However, if you wish to bypass this step, you may create a
101+
personal access token at """))
102+
printstyled("https://github.com/settings/tokens"; bold=true)
103+
println("\n and add the token to the")
96104
printstyled(outs, "~/.julia/config/startup.jl"; bold=true)
97-
println(outs, "file:")
105+
println(outs, " file as:")
98106
println(outs)
99107

100-
printstyled(outs, " ENV[\"GITHUB_TOKEN\"] = \"$(token)\""; bold=true)
108+
printstyled(outs, " ENV[\"GITHUB_TOKEN\"] = <token>"; bold=true)
101109
println(outs)
102110

103111
println(outs, "This token is sensitive, so only do this in a computing environment you trust.")

test/wizard.jl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,5 @@ end
417417
@testset "GitHub - authentication" begin
418418
withenv("GITHUB_TOKEN" => "") do
419419
@test Wizard.github_auth(allow_anonymous=true) isa GitHub.AnonymousAuth
420-
input_stream = IOBuffer()
421-
close(input_stream)
422-
@test_throws ErrorException Wizard.obtain_token(ins=input_stream)
423420
end
424421
end

0 commit comments

Comments
 (0)