Skip to content

Commit 9ce1b5e

Browse files
deivid-rodriguezmatzbot
authored andcommitted
[rubygems/rubygems] Fix commands with 2 MFA requests when webauthn is enabled
If a command requires two MFA authenticated requests, and webauthn is enabled, then first one will succeed but the second one will fail because it tries to reuse the OTP code from the first request and that does not work. This happens when you have not yet logged in to rubygems.org, or when you have an API key with invalid scopes for the current operation. In that case, we need: * An API request to get a token or change scopes for the one that you have. * Another API request to perform the actual operation. Instead of trying to reuse the token, make sure it's cleared so we are asked to authenticate again. We only do this when webauthn is enabled because reusing TOPT tokens otherwise is allowed and I don't want to break that. ruby/rubygems@669e343935
1 parent 1b190b3 commit 9ce1b5e

File tree

3 files changed

+57
-4
lines changed

3 files changed

+57
-4
lines changed

lib/rubygems/gemcutter_utilities.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def otp
6262
options[:otp] || ENV["GEM_HOST_OTP_CODE"]
6363
end
6464

65+
def webauthn_enabled?
66+
options[:webauthn]
67+
end
68+
6569
##
6670
# The host to connect to either from the RUBYGEMS_HOST environment variable
6771
# or from the user's configuration
@@ -249,6 +253,8 @@ def request_with_otp(method, uri, &block)
249253
req["OTP"] = otp if otp
250254
block.call(req)
251255
end
256+
ensure
257+
options[:otp] = nil if webauthn_enabled?
252258
end
253259

254260
def fetch_otp(credentials)
@@ -269,6 +275,8 @@ def fetch_otp(credentials)
269275
terminate_interaction(1)
270276
end
271277

278+
options[:webauthn] = true
279+
272280
say "You are verified with a security device. You may close the browser window."
273281
otp_thread[:otp]
274282
else

test/rubygems/test_gem_commands_owner_command.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,47 @@ def test_remove_owners_unathorized_api_key
496496
assert_match response_success, @stub_ui.output
497497
end
498498

499+
def test_add_owners_no_api_key_webauthn_enabled_does_not_reuse_otp_codes
500+
response_profile = "mfa: ui_and_api\n"
501+
response_mfa_enabled = "You have enabled multifactor authentication but no OTP code provided. Please fill it and retry."
502+
response_not_found = "Owner could not be found."
503+
Gem.configuration.rubygems_api_key = nil
504+
505+
path_token = "odow34b93t6aPCdY"
506+
webauthn_url = "#{Gem.host}/webauthn_verification/#{path_token}"
507+
508+
@stub_fetcher.data["#{Gem.host}/api/v1/profile/me.yaml"] = HTTPResponseFactory.create(body: response_profile, code: 200, msg: "OK")
509+
@stub_fetcher.data["#{Gem.host}/api/v1/api_key"] = [
510+
HTTPResponseFactory.create(body: response_mfa_enabled, code: 401, msg: "Unauthorized"),
511+
HTTPResponseFactory.create(body: "", code: 200, msg: "OK"),
512+
]
513+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification"] = Gem::HTTPResponseFactory.create(body: webauthn_url, code: 200, msg: "OK")
514+
@stub_fetcher.data["#{Gem.host}/api/v1/webauthn_verification/#{path_token}/status.json"] = [
515+
Gem::HTTPResponseFactory.create(body: { status: "success", code: "Uvh6T57tkWuUnWYo" }.to_json, code: 200, msg: "OK"),
516+
Gem::HTTPResponseFactory.create(body: { status: "success", code: "Uvh6T57tkWuUnWYz" }.to_json, code: 200, msg: "OK"),
517+
]
518+
@stub_fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [
519+
HTTPResponseFactory.create(body: response_mfa_enabled, code: 401, msg: "Unauthorized"),
520+
HTTPResponseFactory.create(body: response_not_found, code: 404, msg: "Not Found"),
521+
]
522+
@cmd.handle_options %W[--add some@example freewill]
523+
524+
@stub_ui = Gem::MockGemUi.new "[email protected]\npass\n"
525+
526+
server = Gem::MockTCPServer.new
527+
528+
assert_raise Gem::MockGemUi::TermError do
529+
TCPServer.stub(:new, server) do
530+
use_ui @stub_ui do
531+
@cmd.execute
532+
end
533+
end
534+
end
535+
536+
reused_otp_codes = @stub_fetcher.requests.filter_map {|req| req["OTP"] }.tally.filter_map {|el, count| el if count > 1 }
537+
assert_empty reused_otp_codes
538+
end
539+
499540
def test_add_owners_unathorized_api_key
500541
response_forbidden = "The API key doesn't have access"
501542
response_success = "Owner added successfully."

test/rubygems/utilities.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@
3030
# See RubyGems' tests for more examples of FakeFetcher.
3131

3232
class Gem::FakeFetcher
33-
attr_reader :data
34-
attr_reader :last_request
33+
attr_reader :data, :requests
3534
attr_accessor :paths
3635

3736
def initialize
3837
@data = {}
3938
@paths = []
39+
@requests = []
4040
end
4141

4242
def find_data(path)
@@ -99,9 +99,13 @@ def open_uri_or_path(path)
9999
create_response(uri)
100100
end
101101

102+
def last_request
103+
@requests.last
104+
end
105+
102106
def request(uri, request_class, last_modified = nil)
103-
@last_request = request_class.new uri.request_uri
104-
yield @last_request if block_given?
107+
@requests << request_class.new(uri.request_uri)
108+
yield last_request if block_given?
105109

106110
create_response(uri)
107111
end

0 commit comments

Comments
 (0)