lib/rubygems/gemcutter_utilities.rb in rubygems-update-3.4.11 vs lib/rubygems/gemcutter_utilities.rb in rubygems-update-3.4.12
- old
+ new
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require_relative "remote_fetcher"
require_relative "text"
+require_relative "webauthn_listener"
##
# Utility methods for using the RubyGems API.
module Gem::GemcutterUtilities
@@ -80,11 +81,11 @@
##
# Creates an RubyGems API to +host+ and +path+ with the given HTTP +method+.
#
# If +allowed_push_host+ metadata is present, then it will only allow that host.
- def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, &block)
+ def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block)
require "net/http"
self.host = host if host
unless self.host
alert_error "You must specify a gem server"
@@ -103,11 +104,11 @@
uri = URI.parse "#{self.host}/#{path}"
response = request_with_otp(method, uri, &block)
if mfa_unauthorized?(response)
- ask_otp
+ fetch_otp(credentials)
response = request_with_otp(method, uri, &block)
end
if api_key_forbidden?(response)
update_scope(scope)
@@ -165,15 +166,16 @@
scope_params = get_scope_params(scope)
profile = get_user_profile(email, password)
mfa_params = get_mfa_params(profile)
all_params = scope_params.merge(mfa_params)
warning = profile["warning"]
+ credentials = { email: email, password: password }
say "#{warning}\n" if warning
response = rubygems_api_request(:post, "api/v1/api_key",
- sign_in_host, scope: scope) do |request|
+ sign_in_host, credentials: credentials, scope: scope) do |request|
request.basic_auth email, password
request["OTP"] = otp if otp
request.body = URI.encode_www_form({ name: key_name }.merge(all_params))
end
@@ -248,12 +250,52 @@
req["OTP"] = otp if otp
block.call(req)
end
end
- def ask_otp
- say "You have enabled multi-factor authentication. Please enter OTP code."
- options[:otp] = ask "Code: "
+ def fetch_otp(credentials)
+ options[:otp] = if webauthn_url = webauthn_verification_url(credentials)
+ wait_for_otp(webauthn_url)
+ 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
+
+ thread = Thread.new do
+ Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server)
+ rescue Gem::WebauthnVerificationError => e
+ Thread.current[:error] = e
+ 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.join
+ if error = thread[:error]
+ alert_error error.message
+ terminate_interaction(1)
+ end
+
+ say "You are verified with a security device. You may close the browser window."
+ thread[:otp]
+ end
+
+ def webauthn_verification_url(credentials)
+ response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request|
+ if credentials.empty?
+ request.add_field "Authorization", api_key
+ else
+ request.basic_auth credentials[:email], credentials[:password]
+ end
+ end
+ response.is_a?(Net::HTTPSuccess) ? response.body : nil
end
def pretty_host(host)
if default_host?
"RubyGems.org"