diff options
author | Jenny Shen <[email protected]> | 2023-06-21 17:21:35 -0400 |
---|---|---|
committer | git <[email protected]> | 2023-07-28 16:08:07 +0000 |
commit | 023d0f662b4487c2bd6636c4fcf1e223ef4c8b30 (patch) | |
tree | 87f76b4c37cd9b7ec662c8d60ac007086f644963 /lib/rubygems/gemcutter_utilities.rb | |
parent | 836e4eb3cd4c61823bf812957b555bb0ef79ade5 (diff) |
[rubygems/rubygems] Add Webauthn verification poller to fetch OTP
https://github.com/rubygems/rubygems/commit/39c5e86a67
Diffstat (limited to 'lib/rubygems/gemcutter_utilities.rb')
-rw-r--r-- | lib/rubygems/gemcutter_utilities.rb | 81 |
1 files changed, 69 insertions, 12 deletions
diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 15e61440e3..c43745c504 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -253,36 +253,82 @@ module Gem::GemcutterUtilities def fetch_otp(credentials) options[:otp] = if webauthn_url = webauthn_verification_url(credentials) - wait_for_otp(webauthn_url) + server = TCPServer.new 0 + port = server.addr[1].to_s + + url_with_port = "#{webauthn_url}?port=#{port}" + say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + + threads = [socket_thread(server), poll_thread(webauthn_url, credentials)] + otp_thread = wait_for_otp_thread(*threads) + + threads.each(&:join) + + if error = otp_thread[:error] + alert_error error.message + terminate_interaction(1) + end + + say "You are verified with a security device. You may close the browser window." + otp_thread[:otp] else say "You have enabled multi-factor authentication. Please enter OTP code." ask "Code: " end end - def wait_for_otp(webauthn_url) - server = TCPServer.new 0 - port = server.addr[1].to_s + def wait_for_otp_thread(*threads) + loop do + threads.each do |otp_thread| + return otp_thread unless otp_thread.alive? + end + sleep 0.1 + end + ensure + threads.each(&:exit) + end + def socket_thread(server) thread = Thread.new do Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server) rescue Gem::WebauthnVerificationError => e Thread.current[:error] = e + ensure + server.close end thread.abort_on_exception = true thread.report_on_exception = false - url_with_port = "#{webauthn_url}?port=#{port}" - say "You have enabled multi-factor authentication. Please visit #{url_with_port} to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + thread + end - thread.join - if error = thread[:error] - alert_error error.message - terminate_interaction(1) + def poll_thread(webauthn_url, credentials) + thread = Thread.new do + Timeout.timeout(300) do + loop do + response = webauthn_verification_poll_response(webauthn_url, credentials) + raise Gem::WebauthnVerificationError, response.message unless response.is_a?(Net::HTTPSuccess) + + require "json" + parsed_response = JSON.parse(response.body) + case parsed_response["status"] + when "pending" + sleep 5 + when "success" + Thread.current[:otp] = parsed_response["code"] + break + else + raise Gem::WebauthnVerificationError, parsed_response["message"] + end + end + end + rescue Gem::WebauthnVerificationError, Timeout::Error => e + Thread.current[:error] = e end + thread.abort_on_exception = true + thread.report_on_exception = false - say "You are verified with a security device. You may close the browser window." - thread[:otp] + thread end def webauthn_verification_url(credentials) @@ -296,6 +342,17 @@ module Gem::GemcutterUtilities response.is_a?(Net::HTTPSuccess) ? response.body : nil end + def webauthn_verification_poll_response(webauthn_url, credentials) + webauthn_token = %r{(?<=\/)[^\/]+(?=$)}.match(webauthn_url)[0] + rubygems_api_request(:get, "api/v1/webauthn_verification/#{webauthn_token}/status.json") do |request| + if credentials.empty? + request.add_field "Authorization", api_key + else + request.basic_auth credentials[:email], credentials[:password] + end + end + end + def pretty_host(host) if default_host? "RubyGems.org" |