cli: Clearer error messages on authentication failure
What changed, and why it matters
This commit only changes the wording of error messages shown when bitcoin-cli fails to log in to the RPC server. It does not fix or introduce any security vulnerability; it is a user-experience improvement that tells users more precisely why authentication failed (missing cookie file, cookie disabled, bad cookie contents, or wrong password).
No security action needed; treat as routine UX/refactoring commit.
Security signals we found
No security fix or vulnerability present
Error-message clarity improvement only
No change to authentication logic or credential handling
Evidence from the diff
The patch refactors GetAuthCookie from returning bool to returning an AuthCookieResult enum, and updates bitcoin-cli’s HTTP_UNAUTHORIZED handling to print one of four clearer messages. No cryptographic, authorization, or network behavior is altered. Functional tests are updated to match the new strings.
Changed components
src/bitcoin-cli.cppsrc/rpc/request.cppsrc/rpc/request.htest/functional/interface_bitcoin_cli.pytest/functional/interface_ipc_cli.pyInspect captured patch +41 / −28
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index 24cb0344..2725842f 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -901,15 +901,13 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
evhttp_request_set_error_cb(req.get(), http_error_cb);
// Get credentials
- std::string strRPCUserColonPass;
- bool failedToGetAuthCookie = false;
+ std::string rpc_credentials;
+ std::optional<AuthCookieResult> auth_cookie_result;
if (gArgs.GetArg("-rpcpassword", "") == "") {
// Try fall back to cookie-based authentication if no password is provided
- if (!GetAuthCookie(&strRPCUserColonPass)) {
- failedToGetAuthCookie = true;
- }
+ auth_cookie_result = GetAuthCookie(rpc_credentials);
} else {
- strRPCUserColonPass = username + ":" + gArgs.GetArg("-rpcpassword", "");
+ rpc_credentials = username + ":" + gArgs.GetArg("-rpcpassword", "");
}
struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
@@ -917,7 +915,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
evhttp_add_header(output_headers, "Host", host.c_str());
evhttp_add_header(output_headers, "Connection", "close");
evhttp_add_header(output_headers, "Content-Type", "application/json");
- evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());
+ evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(rpc_credentials)).c_str());
// Attach request data
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
@@ -942,13 +940,24 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
"Use \"bitcoin-cli -help\" for more info.",
host, port, responseErrorMessage));
} else if (response.status == HTTP_UNAUTHORIZED) {
- if (failedToGetAuthCookie) {
- throw std::runtime_error(strprintf(
- "Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set. See -rpcpassword and -stdinrpcpass. Configuration file: (%s)",
- fs::PathToString(gArgs.GetConfigFilePath())));
+ std::string error{"Authorization failed: "};
+ if (auth_cookie_result.has_value()) {
+ switch (*auth_cookie_result) {
+ case AuthCookieResult::Error:
+ error += "Failed to read cookie file and no rpcpassword was specified.";
+ break;
+ case AuthCookieResult::Disabled:
+ error += "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.";
+ break;
+ case AuthCookieResult::Ok:
+ error += "Cookie file credentials were invalid and no rpcpassword was specified.";
+ break;
+ }
} else {
- throw std::runtime_error("Authorization failed: Incorrect rpcuser or rpcpassword");
+ error += "Incorrect rpcuser or rpcpassword were specified.";
}
+ error += strprintf(" Configuration file: (%s)", fs::PathToString(gArgs.GetConfigFilePath()));
+ throw std::runtime_error(error);
} else if (response.status == HTTP_SERVICE_UNAVAILABLE) {
throw std::runtime_error(strprintf("Server response: %s", response.body));
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR)
diff --git a/src/rpc/request.cpp b/src/rpc/request.cpp
index cf7109a4..34a9ff75 100644
--- a/src/rpc/request.cpp
+++ b/src/rpc/request.cpp
@@ -145,23 +145,20 @@ AuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms
return AuthCookieResult::Ok;
}
-bool GetAuthCookie(std::string *cookie_out)
+AuthCookieResult GetAuthCookie(std::string& cookie_out)
{
std::ifstream file;
- std::string cookie;
fs::path filepath = GetAuthCookieFile();
if (filepath.empty()) {
- return true; // -norpccookiefile
+ return AuthCookieResult::Disabled; // -norpccookiefile
}
file.open(filepath.std_path());
- if (!file.is_open())
- return false;
- std::getline(file, cookie);
+ if (!file.is_open()) {
+ return AuthCookieResult::Error;
+ }
+ std::getline(file, cookie_out);
file.close();
-
- if (cookie_out)
- *cookie_out = cookie;
- return true;
+ return AuthCookieResult::Ok;
}
void DeleteAuthCookie()
diff --git a/src/rpc/request.h b/src/rpc/request.h
index 357f5d7e..00216562 100644
--- a/src/rpc/request.h
+++ b/src/rpc/request.h
@@ -43,7 +43,7 @@ AuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms
std::string& pass);
/** Read the RPC authentication cookie from disk */
-bool GetAuthCookie(std::string *cookie_out);
+AuthCookieResult GetAuthCookie(std::string& cookie_out);
/** Delete RPC authentication cookie from disk */
void DeleteAuthCookie();
/** Parse JSON-RPC batch reply into a vector */
diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py
index fbe753a6..953eea3d 100755
--- a/test/functional/interface_bitcoin_cli.py
+++ b/test/functional/interface_bitcoin_cli.py
@@ -139,11 +139,11 @@ class TestBitcoinCli(BitcoinTestFramework):
self.log.info("Test -stdinrpcpass option")
assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input=password).getblockcount())
- assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo)
+ assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword were specified', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo)
self.log.info("Test -stdin and -stdinrpcpass")
assert_equal(['foo', 'bar'], self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input=f'{password}\nfoo\nbar').echo())
- assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo)
+ assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword were specified', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo)
self.log.info("Test connecting to a non-existing server")
assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo)
@@ -196,7 +196,14 @@ class TestBitcoinCli(BitcoinTestFramework):
self.nodes[0].replace_in_config([("#" + conf_rpcport, conf_rpcport)])
self.log.info("Test connecting with non-existing RPC cookie file")
- assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)
+ assert_raises_process_error(1, "Failed to read cookie file and no rpcpassword was specified.", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)
+
+ self.log.info("Test connecting with neither cookie file, nor password")
+ assert_raises_process_error(1, "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.", self.nodes[0].cli("-norpccookiefile").echo)
+ assert_raises_process_error(1, "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.", self.nodes[0].cli("-norpccookiefile", "-rpcpassword=").echo)
+
+ self.log.info("Test connecting with invalid cookie file")
+ assert_raises_process_error(1, "Cookie file credentials were invalid and no rpcpassword was specified.", self.nodes[0].cli(f"-rpccookiefile={self.nodes[0].datadir_path / 'bitcoin.conf'}").echo)
self.log.info("Test connecting without RPC cookie file and with password arg")
assert_equal(BLOCKS, self.nodes[0].cli('-norpccookiefile', f'-rpcuser={user}', f'-rpcpassword={password}').getblockcount())
diff --git a/test/functional/interface_ipc_cli.py b/test/functional/interface_ipc_cli.py
index 92ccc879..9ad704d3 100755
--- a/test/functional/interface_ipc_cli.py
+++ b/test/functional/interface_ipc_cli.py
@@ -31,7 +31,7 @@ class TestBitcoinIpcCli(BitcoinTestFramework):
if error is None:
assert_equal(result.stdout, '[\n "foo"\n]\n')
else:
- assert_equal(result.stdout, error)
+ assert result.stdout.startswith(error), f"Output didn't start with the expected error {error!r}:\n{result.stdout}"
assert_equal(result.stderr, None)
assert_equal(result.returncode, 0 if error is None else 1)
@@ -40,7 +40,7 @@ class TestBitcoinIpcCli(BitcoinTestFramework):
if node.ipc_tmp_dir:
self.log.info("Skipping a few checks because temporary directory path is too long")
- http_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword\n"
+ http_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword were specified."
http_connect_error = f"error: timeout on transient error: Could not connect to the server 127.0.0.1:{rpc_port(node.index)}\n\nMake sure the bitcoind server is running and that you are connecting to the correct RPC port.\nUse \"bitcoin-cli -help\" for more info.\n"
ipc_connect_error = "error: timeout on transient error: Connection refused\n\nProbably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n bitcoin-node -chain=regtest -ipcbind=unix\n"
ipc_http_conflict = "error: -rpcconnect and -ipcconnect options cannot both be enabled\n"
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.