aboutsummaryrefslogtreecommitdiffstats
path: root/test/rubygems
diff options
context:
space:
mode:
authorJenny Shen <jenny.shen@shopify.com>2023-06-21 17:21:35 -0400
committergit <svn-admin@ruby-lang.org>2023-07-28 16:08:07 +0000
commit023d0f662b4487c2bd6636c4fcf1e223ef4c8b30 (patch)
tree87f76b4c37cd9b7ec662c8d60ac007086f644963 /test/rubygems
parent836e4eb3cd4c61823bf812957b555bb0ef79ade5 (diff)
downloadruby-023d0f662b4487c2bd6636c4fcf1e223ef4c8b30.tar.gz
[rubygems/rubygems] Add Webauthn verification poller to fetch OTP
https://github.com/rubygems/rubygems/commit/39c5e86a67
Diffstat (limited to 'test/rubygems')
-rw-r--r--test/rubygems/test_gem_commands_owner_command.rb83
-rw-r--r--test/rubygems/test_gem_commands_push_command.rb85
-rw-r--r--test/rubygems/test_gem_commands_yank_command.rb102
-rw-r--r--test/rubygems/test_gem_gemcutter_utilities.rb73
4 files changed, 343 insertions, 0 deletions
diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb
index 091335ab4b..e89f160a96 100644
--- a/test/rubygems/test_gem_commands_owner_command.rb
+++ b/test/rubygems/test_gem_commands_owner_command.rb
@@ -374,6 +374,11 @@ EOF
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\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
+ code: 200,
+ msg: "OK"
+ )
TCPServer.stub(:new, server) 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_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
Gem::WebauthnListener.stub(:wait_for_otp_code, raise_error) do
@@ -425,6 +435,79 @@ EOF
refute_match response_success, @stub_ui.output
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
response_forbidden = "The API key doesn't have access"
response_success = "Owner removed successfully."
diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb
index baaf8e85c2..33f2481feb 100644
--- a/test/rubygems/test_gem_commands_push_command.rb
+++ b/test/rubygems/test_gem_commands_push_command.rb
@@ -438,6 +438,11 @@ class TestGemCommandsPushCommand < Gem::TestCase
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\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
+ code: 200,
+ msg: "OK"
+ )
TCPServer.stub(:new, server) 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"),
]
@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
TCPServer.stub(:new, server) do
@@ -491,6 +501,81 @@ class TestGemCommandsPushCommand < Gem::TestCase
refute_match response_success, @ui.output
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
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"
diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb
index e5e234e0f8..6145e928ae 100644
--- a/test/rubygems/test_gem_commands_yank_command.rb
+++ b/test/rubygems/test_gem_commands_yank_command.rb
@@ -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."
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)
@@ -129,6 +130,11 @@ class TestGemCommandsYankCommand < Gem::TestCase
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\":\"pending\",\"message\":\"Security device authentication is still pending.\"}",
+ code: 200,
+ msg: "OK"
+ )
@cmd.options[:args] = %w[a]
@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."
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)
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: "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[:added_platform] = true
@@ -194,6 +206,96 @@ class TestGemCommandsYankCommand < Gem::TestCase
refute_match "Successfully yanked", @ui.output
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
yank_uri = "http://example/api/v1/gems/yank"
@fetcher.data[yank_uri] = HTTPResponseFactory.create(body: "Successfully yanked", code: 200, msg: "OK")
diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb
index cc698c76ba..5bbc65b3da 100644
--- a/test/rubygems/test_gem_gemcutter_utilities.rb
+++ b/test/rubygems/test_gem_gemcutter_utilities.rb
@@ -268,6 +268,52 @@ class TestGemGemcutterUtilities < Gem::TestCase
refute_match "Signed in with API key:", @sign_in_ui.output
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: "")
email = "you@example.com"
password = "secret"
@@ -320,7 +366,34 @@ class TestGemGemcutterUtilities < Gem::TestCase
end
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/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
def respond_with_require_otp