[rubygems/rubygems] Add Webauthn verification poller to fetch OTP
https://github.com/rubygems/rubygems/commit/39c5e86a67
This commit is contained in:
parent
836e4eb3cd
commit
023d0f662b
@ -253,36 +253,82 @@ module Gem::GemcutterUtilities
|
|||||||
|
|
||||||
def fetch_otp(credentials)
|
def fetch_otp(credentials)
|
||||||
options[:otp] = if webauthn_url = webauthn_verification_url(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
|
else
|
||||||
say "You have enabled multi-factor authentication. Please enter OTP code."
|
say "You have enabled multi-factor authentication. Please enter OTP code."
|
||||||
ask "Code: "
|
ask "Code: "
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_otp(webauthn_url)
|
def wait_for_otp_thread(*threads)
|
||||||
server = TCPServer.new 0
|
loop do
|
||||||
port = server.addr[1].to_s
|
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 = Thread.new do
|
||||||
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server)
|
Thread.current[:otp] = Gem::WebauthnListener.wait_for_otp_code(host, server)
|
||||||
rescue Gem::WebauthnVerificationError => e
|
rescue Gem::WebauthnVerificationError => e
|
||||||
Thread.current[:error] = e
|
Thread.current[:error] = e
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
thread.abort_on_exception = true
|
||||||
|
thread.report_on_exception = false
|
||||||
|
|
||||||
|
thread
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
end
|
||||||
thread.abort_on_exception = true
|
thread.abort_on_exception = true
|
||||||
thread.report_on_exception = false
|
thread.report_on_exception = false
|
||||||
|
|
||||||
url_with_port = "#{webauthn_url}?port=#{port}"
|
thread
|
||||||
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
|
end
|
||||||
|
|
||||||
def webauthn_verification_url(credentials)
|
def webauthn_verification_url(credentials)
|
||||||
@ -296,6 +342,17 @@ module Gem::GemcutterUtilities
|
|||||||
response.is_a?(Net::HTTPSuccess) ? response.body : nil
|
response.is_a?(Net::HTTPSuccess) ? response.body : nil
|
||||||
end
|
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)
|
def pretty_host(host)
|
||||||
if default_host?
|
if default_host?
|
||||||
"RubyGems.org"
|
"RubyGems.org"
|
||||||
|
@ -374,6 +374,11 @@ EOF
|
|||||||
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
TCPServer.stub(:new, server) do
|
TCPServer.stub(:new, server) do
|
||||||
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
||||||
@ -405,6 +410,11 @@ EOF
|
|||||||
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
TCPServer.stub(:new, server) do
|
TCPServer.stub(:new, server) do
|
||||||
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
|
||||||
@ -425,6 +435,79 @@ EOF
|
|||||||
refute_match response_success, @stub_ui.output
|
refute_match response_success, @stub_ui.output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_success_with_polling
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
response_success = "Owner added successfully."
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @stub_ui do
|
||||||
|
@cmd.add_owners("freewill", ["user-new1@example.com"])
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match "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.", @stub_ui.output
|
||||||
|
assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
|
||||||
|
assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"]
|
||||||
|
assert_match response_success, @stub_ui.output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_failure_with_polling
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
response_success = "Owner added successfully."
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @stub_ui do
|
||||||
|
@cmd.add_owners("freewill", ["user-new1@example.com"])
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
|
||||||
|
assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
||||||
|
assert_match "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.", @stub_ui.output
|
||||||
|
assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \
|
||||||
|
"or been used already.", @stub_ui.error
|
||||||
|
refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output
|
||||||
|
refute_match response_success, @stub_ui.output
|
||||||
|
end
|
||||||
|
|
||||||
def test_remove_owners_unathorized_api_key
|
def test_remove_owners_unathorized_api_key
|
||||||
response_forbidden = "The API key doesn't have access"
|
response_forbidden = "The API key doesn't have access"
|
||||||
response_success = "Owner removed successfully."
|
response_success = "Owner removed successfully."
|
||||||
|
@ -438,6 +438,11 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|||||||
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
TCPServer.stub(:new, server) do
|
TCPServer.stub(:new, server) do
|
||||||
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
Gem::WebauthnListener.stub(:wait_for_otp_code, "Uvh6T57tkWuUnWYo") do
|
||||||
@ -469,6 +474,11 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|||||||
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
error = assert_raise Gem::MockGemUi::TermError do
|
error = assert_raise Gem::MockGemUi::TermError do
|
||||||
TCPServer.stub(:new, server) do
|
TCPServer.stub(:new, server) do
|
||||||
@ -491,6 +501,81 @@ class TestGemCommandsPushCommand < Gem::TestCase
|
|||||||
refute_match response_success, @ui.output
|
refute_match response_success, @ui.output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_success_with_polling
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
response_success = "Successfully registered gem: freewill (1.0.0)"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/gems"] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @ui do
|
||||||
|
@cmd.send_gem(@path)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match "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.", @ui.output
|
||||||
|
assert_match "You are verified with a security device. You may close the browser window.", @ui.output
|
||||||
|
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
||||||
|
assert_match response_success, @ui.output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_failure_with_polling
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
response_success = "Successfully registered gem: freewill (1.0.0)"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/gems"] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: response_success, code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data["#{Gem.host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
error = assert_raise Gem::MockGemUi::TermError do
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @ui do
|
||||||
|
@cmd.send_gem(@path)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert_equal 1, error.exit_code
|
||||||
|
|
||||||
|
assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match "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.", @ui.output
|
||||||
|
assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \
|
||||||
|
"or been used already.", @ui.error
|
||||||
|
refute_match "You are verified with a security device. You may close the browser window.", @ui.output
|
||||||
|
refute_match response_success, @ui.output
|
||||||
|
end
|
||||||
|
|
||||||
def test_sending_gem_unathorized_api_key_with_mfa_enabled
|
def test_sending_gem_unathorized_api_key_with_mfa_enabled
|
||||||
response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
response_forbidden = "The API key doesn't have access"
|
response_forbidden = "The API key doesn't have access"
|
||||||
|
@ -121,6 +121,7 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|||||||
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
yank_uri = "http://example/api/v1/gems/yank"
|
yank_uri = "http://example/api/v1/gems/yank"
|
||||||
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
||||||
|
status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"
|
||||||
port = 5678
|
port = 5678
|
||||||
server = TCPServer.new(port)
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
@ -129,6 +130,11 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|||||||
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
|
@fetcher.data[status_uri] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
@cmd.options[:args] = %w[a]
|
@cmd.options[:args] = %w[a]
|
||||||
@cmd.options[:added_platform] = true
|
@cmd.options[:added_platform] = true
|
||||||
@ -157,6 +163,7 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|||||||
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
yank_uri = "http://example/api/v1/gems/yank"
|
yank_uri = "http://example/api/v1/gems/yank"
|
||||||
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
||||||
|
status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"
|
||||||
port = 5678
|
port = 5678
|
||||||
server = TCPServer.new(port)
|
server = TCPServer.new(port)
|
||||||
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
raise_error = ->(*_args) { raise Gem::WebauthnVerificationError, "Something went wrong" }
|
||||||
@ -166,6 +173,11 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|||||||
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
||||||
]
|
]
|
||||||
|
@fetcher.data[status_uri] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
@cmd.options[:args] = %w[a]
|
@cmd.options[:args] = %w[a]
|
||||||
@cmd.options[:added_platform] = true
|
@cmd.options[:added_platform] = true
|
||||||
@ -194,6 +206,96 @@ class TestGemCommandsYankCommand < Gem::TestCase
|
|||||||
refute_match "Successfully yanked", @ui.output
|
refute_match "Successfully yanked", @ui.output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_success_with_polling
|
||||||
|
webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
yank_uri = "http://example/api/v1/gems/yank"
|
||||||
|
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
||||||
|
status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data[yank_uri] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@fetcher.data[status_uri] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"success\",\"code\":\"Uvh6T57tkWuUnWYo\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmd.options[:args] = %w[a]
|
||||||
|
@cmd.options[:added_platform] = true
|
||||||
|
@cmd.options[:version] = req("= 1.0")
|
||||||
|
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @ui do
|
||||||
|
@cmd.execute
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match %r{Yanking gem from http://example}, @ui.output
|
||||||
|
assert_match "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.", @ui.output
|
||||||
|
assert_match "You are verified with a security device. You may close the browser window.", @ui.output
|
||||||
|
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
||||||
|
assert_match "Successfully yanked", @ui.output
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_with_webauthn_enabled_failure_with_polling
|
||||||
|
webauthn_verification_url = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
response_fail = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry."
|
||||||
|
yank_uri = "http://example/api/v1/gems/yank"
|
||||||
|
webauthn_uri = "http://example/api/v1/webauthn_verification"
|
||||||
|
status_uri = "http://example/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
|
||||||
|
@fetcher.data[webauthn_uri] = HTTPResponseFactory.create(body: webauthn_verification_url, code: 200, msg: "OK")
|
||||||
|
@fetcher.data[yank_uri] = [
|
||||||
|
HTTPResponseFactory.create(body: response_fail, code: 401, msg: "Unauthorized"),
|
||||||
|
HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK"),
|
||||||
|
]
|
||||||
|
@fetcher.data[status_uri] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: "{\"status\":\"expired\",\"message\":\"The token in the link you used has either expired or been used already.\"}",
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmd.options[:args] = %w[a]
|
||||||
|
@cmd.options[:added_platform] = true
|
||||||
|
@cmd.options[:version] = req("= 1.0")
|
||||||
|
|
||||||
|
error = assert_raise Gem::MockGemUi::TermError do
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
use_ui @ui do
|
||||||
|
@cmd.execute
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert_equal 1, error.exit_code
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
|
||||||
|
assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key
|
||||||
|
assert_match %r{Yanking gem from http://example}, @ui.output
|
||||||
|
assert_match "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.", @ui.output
|
||||||
|
assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \
|
||||||
|
"or been used already.", @ui.error
|
||||||
|
refute_match "You are verified with a security device. You may close the browser window.", @ui.output
|
||||||
|
refute_match "Successfully yanked", @ui.output
|
||||||
|
end
|
||||||
|
|
||||||
def test_execute_key
|
def test_execute_key
|
||||||
yank_uri = "http://example/api/v1/gems/yank"
|
yank_uri = "http://example/api/v1/gems/yank"
|
||||||
@fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
|
@fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
|
||||||
|
@ -268,6 +268,52 @@ class TestGemGemcutterUtilities < Gem::TestCase
|
|||||||
refute_match "Signed in with API key:", @sign_in_ui.output
|
refute_match "Signed in with API key:", @sign_in_ui.output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_sign_in_with_webauthn_enabled_with_polling
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
@fetcher.respond_with_require_otp
|
||||||
|
@fetcher.respond_with_webauthn_url(webauthn_verification_url)
|
||||||
|
@fetcher.respond_with_webauthn_polling("Uvh6T57tkWuUnWYo")
|
||||||
|
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
util_sign_in
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match "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.", @sign_in_ui.output
|
||||||
|
assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output
|
||||||
|
assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sign_in_with_webauthn_enabled_with_polling_failure
|
||||||
|
webauthn_verification_url = "rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY"
|
||||||
|
port = 5678
|
||||||
|
server = TCPServer.new(port)
|
||||||
|
@fetcher.respond_with_require_otp
|
||||||
|
@fetcher.respond_with_webauthn_url(webauthn_verification_url)
|
||||||
|
@fetcher.respond_with_webauthn_polling_failure
|
||||||
|
|
||||||
|
assert_raise Gem::MockGemUi::TermError do
|
||||||
|
TCPServer.stub(:new, server) do
|
||||||
|
util_sign_in
|
||||||
|
ensure
|
||||||
|
server.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
url_with_port = "#{webauthn_verification_url}?port=#{port}"
|
||||||
|
assert_match "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.", @sign_in_ui.output
|
||||||
|
assert_match "ERROR: Security device verification failed: " \
|
||||||
|
"The token in the link you used has either expired or been used already.", @sign_in_ui.error
|
||||||
|
end
|
||||||
|
|
||||||
def util_sign_in(args: [], extra_input: "")
|
def util_sign_in(args: [], extra_input: "")
|
||||||
email = "you@example.com"
|
email = "you@example.com"
|
||||||
password = "secret"
|
password = "secret"
|
||||||
@ -320,7 +366,34 @@ class TestGemGemcutterUtilities < Gem::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def respond_with_webauthn_url(url)
|
def respond_with_webauthn_url(url)
|
||||||
|
require "json"
|
||||||
@data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create(body: url, code: 200, msg: "OK")
|
@data["#{@host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create(body: url, code: 200, msg: "OK")
|
||||||
|
@data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: { status: "pending", message: "Security device authentication is still pending." }.to_json,
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_with_webauthn_polling(code)
|
||||||
|
require "json"
|
||||||
|
@data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: { status: "success", code: code }.to_json,
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def respond_with_webauthn_polling_failure
|
||||||
|
require "json"
|
||||||
|
@data["#{@host}/api/v1/webauthn_verification/odow34b93t6aPCdY/status.json"] = Gem::HTTPResponseFactory.create(
|
||||||
|
body: {
|
||||||
|
status: "expired",
|
||||||
|
message: "The token in the link you used has either expired or been used already.",
|
||||||
|
}.to_json,
|
||||||
|
code: 200,
|
||||||
|
msg: "OK"
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def respond_with_require_otp
|
def respond_with_require_otp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user