Skip to content

Commit 894693a

Browse files
committed
Use new Oauth flow
The username/password exchange mechanism is (rightfully) deprecated, the device flow is now in beta, and seems to be the perfect replacement. This change includes a best-guess of how this might work with GitHub Enterprise but I haven't tested that, so GitHub Enterprise will continue to default to the deprecated flow. Fixes #315
1 parent 75fd67a commit 894693a

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

README.md

+20-3
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,26 @@ To read a gist and print it to STDOUT
8484

8585
## Login
8686

87-
If you want to associate your gists with your GitHub account, you need to login
88-
with gist. It doesn't store your username and password, it just uses them to get
89-
an OAuth2 token (with the "gist" permission).
87+
Before you use `gist` for the first time you will need to log in. There are two supported login flows:
88+
89+
1. The Github device-code Oauth flow. This is the default for authenticating to github.com, and can be enabled for Github Enterprise by creating an Oauth app, and exporting the environment variable `GIST_CLIENT_ID` with the client id of the Oauth app.
90+
2. The (deprecated) username and password token exchange flow. This is the default for GitHub Enterprise, and can be used to log into github.com by exporting the environment variable `GIST_USE_USERNAME_AND_PASSWORD`.
91+
92+
### The device-code flow
93+
94+
This flow allows you to obtain a token by logging into GitHub in the browser and typing a verification code. This is the preferred mechanism.
95+
96+
gist --login
97+
Requesting login parameters...
98+
Please sign in at https://github.com/login/device
99+
and enter code: XXXX-XXXX
100+
Success! https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e
101+
102+
The returned access_token is stored in `~/.gist` and used for all future gisting. If you need to you can revoke access from https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e.
103+
104+
### The username-password flow
105+
106+
This flow asks for your GitHub username and password (and 2FA code), and exchanges them for a token with the "gist" permission (your username and password are not stored). This mechanism is deprecated by GitHub, but may still work with GitHub Enterprise.
90107

91108
gist --login
92109
Obtaining OAuth2 access_token from GitHub.

lib/gist.rb

+73-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
module Gist
1313
extend self
1414

15-
VERSION = '5.1.0'
15+
VERSION = '6.0.0'
1616

1717
# A list of clipboard commands with copy and paste support.
1818
CLIPBOARD_COMMANDS = {
@@ -23,12 +23,16 @@ module Gist
2323
}
2424

2525
GITHUB_API_URL = URI("https://api.github.com/")
26+
GITHUB_URL = URI("https://github.com/")
2627
GIT_IO_URL = URI("https://git.io")
2728

2829
GITHUB_BASE_PATH = ""
2930
GHE_BASE_PATH = "/api/v3"
3031

32+
GITHUB_CLIENT_ID = '4f7ec0d4eab38e74384e'
33+
3134
URL_ENV_NAME = "GITHUB_URL"
35+
CLIENT_ID_ENV_NAME = "GIST_CLIENT_ID"
3236

3337
USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
3438

@@ -329,15 +333,71 @@ def rawify(url)
329333

330334
# Log the user into gist.
331335
#
336+
def login!(credentials={})
337+
if (login_url == GITHUB_URL || ENV.key?(CLIENT_ID_ENV_NAME)) && credentials.empty? && !ENV.key?('GIST_USE_USERNAME_AND_PASSWORD')
338+
device_flow_login!
339+
else
340+
access_token_login!(credentials)
341+
end
342+
end
343+
344+
def device_flow_login!
345+
puts "Requesting login parameters..."
346+
request = Net::HTTP::Post.new("/login/device/code")
347+
request.body = JSON.dump({
348+
:scope => 'gist',
349+
:client_id => client_id,
350+
})
351+
request.content_type = 'application/json'
352+
request['accept'] = "application/json"
353+
response = http(login_url, request)
354+
355+
if response.code != '200'
356+
raise Error, "HTTP #{response.code}: #{response.body}"
357+
end
358+
359+
body = JSON.parse(response.body)
360+
361+
puts "Please sign in at #{body['verification_uri']}"
362+
puts " and enter code: #{body['user_code']}"
363+
device_code = body['device_code']
364+
interval = body['interval']
365+
366+
loop do
367+
sleep(interval.to_i)
368+
request = Net::HTTP::Post.new("/login/oauth/access_token")
369+
request.body = JSON.dump({
370+
:client_id => client_id,
371+
:grant_type => 'urn:ietf:params:oauth:grant-type:device_code',
372+
:device_code => device_code
373+
})
374+
request.content_type = 'application/json'
375+
request['Accept'] = 'application/json'
376+
response = http(login_url, request)
377+
if response.code != '200'
378+
raise Error, "HTTP #{response.code}: #{response.body}"
379+
end
380+
body = JSON.parse(response.body)
381+
break unless body['error'] == 'authorization_pending'
382+
end
383+
384+
if body['error']
385+
raise Error, body['error_description']
386+
end
387+
388+
AuthTokenFile.write JSON.parse(response.body)['access_token']
389+
390+
puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/connections/applications/#{client_id}"
391+
end
392+
393+
# Logs the user into gist.
394+
#
332395
# This method asks the user for a username and password, and tries to obtain
333396
# and OAuth2 access token, which is then stored in ~/.gist
334397
#
335398
# @raise [Gist::Error] if something went wrong
336-
# @param [Hash] credentials login details
337-
# @option credentials [String] :username
338-
# @option credentials [String] :password
339399
# @see http://developer.github.com/v3/oauth/
340-
def login!(credentials={})
400+
def access_token_login!(credentials={})
341401
puts "Obtaining OAuth2 access_token from GitHub."
342402
loop do
343403
print "GitHub username: "
@@ -548,11 +608,19 @@ def base_path
548608
ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
549609
end
550610

611+
def login_url
612+
ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_URL
613+
end
614+
551615
# Get the API URL
552616
def api_url
553617
ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
554618
end
555619

620+
def client_id
621+
ENV.key?(CLIENT_ID_ENV_NAME) ? URI(ENV[CLIENT_ID_ENV_NAME]) : GITHUB_CLIENT_ID
622+
end
623+
556624
def legacy_private_gister?
557625
return unless which('git')
558626
`git config --global gist.private` =~ /\Ayes|1|true|on\z/i

spec/ghe_spec.rb

+12-5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
MOCK_USER = 'foo'
66
MOCK_PASSWORD = 'bar'
77

8-
MOCK_AUTHZ_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_USER}:#{MOCK_PASSWORD}@#{MOCK_GHE_HOST}/api/v3/"
8+
MOCK_AUTHZ_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_GHE_HOST}/api/v3/"
99
MOCK_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_GHE_HOST}/api/v3/"
10-
MOCK_AUTHZ_GITHUB_URL = "https://#{MOCK_USER}:#{MOCK_PASSWORD}@api.github.com/"
1110
MOCK_GITHUB_URL = "https://api.github.com/"
11+
MOCK_LOGIN_URL = "https://github.com/"
1212

1313
before do
1414
@saved_env = ENV[Gist::URL_ENV_NAME]
@@ -20,8 +20,15 @@
2020
# stub requests for /authorizations
2121
stub_request(:post, /#{MOCK_AUTHZ_GHE_URL}authorizations/).
2222
to_return(:status => 201, :body => '{"token": "asdf"}')
23-
stub_request(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/).
23+
stub_request(:post, /#{MOCK_GITHUB_URL}authorizations/).
24+
with(headers: {'Authorization'=>'Basic Zm9vOmJhcg=='}).
2425
to_return(:status => 201, :body => '{"token": "asdf"}')
26+
27+
stub_request(:post, /#{MOCK_LOGIN_URL}login\/device\/code/).
28+
to_return(:status => 200, :body => '{"interval": "0.1", "user_code":"XXXX-XXXX", "device_code": "xxxx", "verification_uri": "https://github.com/login/device"}')
29+
30+
stub_request(:post, /#{MOCK_LOGIN_URL}login\/oauth\/access_token/).
31+
to_return(:status => 200, :body => '{"access_token":"zzzz"}')
2532
end
2633

2734
after do
@@ -48,7 +55,7 @@
4855

4956
Gist.login!
5057

51-
assert_requested(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/)
58+
assert_requested(:post, /#{MOCK_LOGIN_URL}login\/oauth\/access_token/)
5259
end
5360

5461
it "should access to #{MOCK_GHE_HOST} when $#{Gist::URL_ENV_NAME} was set" do
@@ -65,7 +72,7 @@
6572
$stdin = StringIO.new "#{MOCK_USER}_wrong\n#{MOCK_PASSWORD}_wrong\n"
6673
Gist.login! :username => MOCK_USER, :password => MOCK_PASSWORD
6774

68-
assert_requested(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/)
75+
assert_requested(:post, /#{MOCK_GITHUB_URL}authorizations/)
6976
end
7077

7178
end

0 commit comments

Comments
 (0)