diff --git a/features/authentication.feature b/features/authentication.feature index 923978173..16403e981 100644 --- a/features/authentication.feature +++ b/features/authentication.feature @@ -97,3 +97,32 @@ Feature: OAuth authentication Then the stderr should contain "Error creating repository: Unauthorized (HTTP 401)" And the exit status should be 1 And the file "../home/.config/hub" should not exist + + Scenario: Two-factor authentication + Given the GitHub API server: + """ + require 'rack/auth/basic' + get('/authorizations') { + auth = Rack::Auth::Basic::Request.new(env) + halt 401 unless auth.credentials == %w[mislav kitty] + if request.env['HTTP_X_GITHUB_OTP'] != "112233" + response.headers['X-GitHub-OTP'] = "required;application" + halt 401 + end + json [ {:token => 'OTOKEN', :app => {:url => 'http://hub.github.com/'}} ] + } + get('/user') { + json :login => 'mislav' + } + post('/user/repos') { + json :full_name => 'mislav/dotfiles' + } + """ + When I run `hub create` interactively + When I type "mislav" + And I type "kitty" + And I type "112233" + Then the output should contain "github.com password for mislav (never stored):" + Then the output should contain "two-factor authentication code:" + And the exit status should be 0 + And the file "../home/.config/hub" should contain "oauth_token: OTOKEN" diff --git a/lib/hub/github_api.rb b/lib/hub/github_api.rb index f8cf93423..49d9e7d65 100644 --- a/lib/hub/github_api.rb +++ b/lib/hub/github_api.rb @@ -257,10 +257,19 @@ def apply_authentication req, url end end - def obtain_oauth_token host, user + def obtain_oauth_token host, user, two_factor_code = nil # first try to fetch existing authorization - res = get "https://#{user}@#{host}/authorizations" - res.error! unless res.success? + res = get "https://#{user}@#{host}/authorizations" do |req| + req['X-GitHub-OTP'] = two_factor_code if two_factor_code + end + unless res.success? + if !two_factor_code && res['X-GitHub-OTP'].to_s.include?('required') + two_factor_code = config.prompt_auth_code + return obtain_oauth_token(host, user, two_factor_code) + else + res.error! + end + end if found = res.data.find {|auth| auth['app']['url'] == oauth_app_url } found['token'] @@ -401,6 +410,13 @@ def prompt_password host, user abort end + def prompt_auth_code + print "two-factor authentication code: " + $stdin.gets.chomp + rescue Interrupt + abort + end + NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL'