Copilot commented on code in PR #575:
URL: https://github.com/apache/pulsar-client-cpp/pull/575#discussion_r3218107294
##########
tests/AuthPluginTest.cc:
##########
@@ -518,6 +541,84 @@ TEST(AuthPluginTest, testAuthFactoryAthenz) {
}
}
+namespace testOauth2Tls {
+class MockOauth2Server {
+ public:
+ MockOauth2Server(const std::string& responseBody, const std::string&
responseContentType, int listenPort,
+ bool requireClientCert = true)
+ : responseBody_(responseBody),
+ responseContentType_(responseContentType),
+ acceptor_(io_, ASIO::ip::tcp::endpoint(ASIO::ip::tcp::v4(),
static_cast<uint16_t>(listenPort))),
+ sslCtx_(ASIO::ssl::context::sslv23) {
+ sslCtx_.set_options(ASIO::ssl::context::default_workarounds |
ASIO::ssl::context::no_sslv2 |
+ ASIO::ssl::context::no_sslv3);
+ sslCtx_.use_certificate_chain_file(brokerPublicKeyPath);
+ sslCtx_.use_private_key_file(brokerPrivateKeyPath,
ASIO::ssl::context::pem);
+ sslCtx_.load_verify_file(caPath);
+ sslCtx_.set_verify_mode(requireClientCert
+ ? (ASIO::ssl::verify_peer |
ASIO::ssl::verify_fail_if_no_peer_cert)
+ : ASIO::ssl::verify_none);
+ }
+
+ const std::string& request() const { return request_; }
+
+ bool mockServe() {
+ ASIO::ip::tcp::socket socket(io_);
+ acceptor_.accept(socket);
+ ASIO_ERROR error;
+ ASIO::ssl::stream<ASIO::ip::tcp::socket&> sslStream(socket, sslCtx_);
+ sslStream.handshake(ASIO::ssl::stream_base::server, error);
+ if (error) return false;
+ if (!readRequest(sslStream)) return false;
+
+ const std::string response = "HTTP/1.1 200 OK\nContent-Type: " +
responseContentType_ +
+ "\nContent-Length: " +
std::to_string(responseBody_.size()) +
+ "\nConnection: close\n\n" + responseBody_;
Review Comment:
The mock server returns HTTP headers separated by `\\n` rather than the
HTTP-required `\\r\\n` (and header/body delimiter should be `\\r\\n\\r\\n`).
Some HTTP clients (including libcurl depending on settings/build) can be strict
here, leading to flaky tests. Use CRLF for all header lines and the header
terminator.
##########
tests/AuthPluginTest.cc:
##########
@@ -668,6 +773,205 @@ TEST(AuthPluginTest, testOauth2Failure) {
client5.close();
}
+TEST(AuthPluginTest, testOauth2TlsClientAuth) {
+ const int tokenServerPort = 58081;
+ const int wellKnownServerPort = 58082;
+ const std::string tokenBody =
R"({"access_token":"mockToken","expires_in":3600,"token_type":"Bearer"})";
+ std::unique_ptr<testOauth2Tls::MockOauth2Server> tokenServer;
+ try {
+ tokenServer =
+ std::make_unique<testOauth2Tls::MockOauth2Server>(tokenBody,
"application/json", tokenServerPort);
+ } catch (const std::exception& e) {
+ FAIL() << "Failed to bind local mock token server: " << e.what();
+ }
+
+ std::promise<bool> tokenPromise;
+ auto tokenFuture = tokenPromise.get_future();
+ std::thread tokenThread(
+ [&tokenServer, &tokenPromise]() {
tokenPromise.set_value(tokenServer->mockServe()); });
+
+ std::ostringstream wellKnownBody;
+ wellKnownBody << R"({"token_endpoint":"https://localhost:)" <<
tokenServerPort << R"(/oauth/token"})";
+ std::unique_ptr<testOauth2Tls::MockOauth2Server> wellKnownServer;
+ try {
+ wellKnownServer = std::make_unique<testOauth2Tls::MockOauth2Server>(
+ wellKnownBody.str(), "application/json", wellKnownServerPort,
false);
+ } catch (const std::exception& e) {
+ tokenThread.join();
+ FAIL() << "Failed to bind local mock well-known server: " << e.what();
+ }
+
+ std::promise<bool> wellKnownPromise;
+ auto wellKnownFuture = wellKnownPromise.get_future();
+ std::thread wellKnownThread([&wellKnownServer, &wellKnownPromise]() {
+ wellKnownPromise.set_value(wellKnownServer->mockServe());
+ });
+
+ ParamMap params;
+ params["tokenEndpointAuthMethod"] = "tls_client_auth";
+ params["issuer_url"] = "https://localhost:" +
std::to_string(wellKnownServerPort);
+ params["client_id"] = "test-client";
+ params["tls_cert_file"] = clientPublicKeyPath;
+ params["tls_key_file"] = clientPrivateKeyPath;
+
+ AuthenticationDataPtr data =
+
std::static_pointer_cast<AuthenticationDataProvider>(std::make_shared<InitialAuthData>(caPath));
+ AuthenticationPtr auth = AuthOauth2::create(params);
+ ASSERT_EQ(auth->getAuthData(data), ResultOk);
+ ASSERT_TRUE(data->hasDataFromCommand());
+ ASSERT_EQ(data->getCommandData(), "mockToken");
+
+ ASSERT_TRUE(wellKnownFuture.get());
+ ASSERT_TRUE(tokenFuture.get());
Review Comment:
These tests can hang indefinitely if the mock server thread blocks (e.g.,
accept/handshake/read never completes) because `future.get()` has no timeout
and the threads are joined only afterward. To prevent CI deadlocks, add bounded
waits (e.g., `wait_for` with a reasonable timeout) and fail the test if the
server doesn’t complete in time (optionally closing the acceptor/socket to
unblock).
##########
tests/AuthPluginTest.cc:
##########
@@ -668,6 +773,205 @@ TEST(AuthPluginTest, testOauth2Failure) {
client5.close();
}
+TEST(AuthPluginTest, testOauth2TlsClientAuth) {
+ const int tokenServerPort = 58081;
+ const int wellKnownServerPort = 58082;
+ const std::string tokenBody =
R"({"access_token":"mockToken","expires_in":3600,"token_type":"Bearer"})";
+ std::unique_ptr<testOauth2Tls::MockOauth2Server> tokenServer;
+ try {
+ tokenServer =
+ std::make_unique<testOauth2Tls::MockOauth2Server>(tokenBody,
"application/json", tokenServerPort);
+ } catch (const std::exception& e) {
+ FAIL() << "Failed to bind local mock token server: " << e.what();
+ }
+
+ std::promise<bool> tokenPromise;
+ auto tokenFuture = tokenPromise.get_future();
+ std::thread tokenThread(
+ [&tokenServer, &tokenPromise]() {
tokenPromise.set_value(tokenServer->mockServe()); });
+
+ std::ostringstream wellKnownBody;
+ wellKnownBody << R"({"token_endpoint":"https://localhost:)" <<
tokenServerPort << R"(/oauth/token"})";
+ std::unique_ptr<testOauth2Tls::MockOauth2Server> wellKnownServer;
+ try {
+ wellKnownServer = std::make_unique<testOauth2Tls::MockOauth2Server>(
+ wellKnownBody.str(), "application/json", wellKnownServerPort,
false);
+ } catch (const std::exception& e) {
+ tokenThread.join();
+ FAIL() << "Failed to bind local mock well-known server: " << e.what();
+ }
+
+ std::promise<bool> wellKnownPromise;
+ auto wellKnownFuture = wellKnownPromise.get_future();
+ std::thread wellKnownThread([&wellKnownServer, &wellKnownPromise]() {
+ wellKnownPromise.set_value(wellKnownServer->mockServe());
+ });
+
+ ParamMap params;
+ params["tokenEndpointAuthMethod"] = "tls_client_auth";
+ params["issuer_url"] = "https://localhost:" +
std::to_string(wellKnownServerPort);
+ params["client_id"] = "test-client";
+ params["tls_cert_file"] = clientPublicKeyPath;
+ params["tls_key_file"] = clientPrivateKeyPath;
+
+ AuthenticationDataPtr data =
+
std::static_pointer_cast<AuthenticationDataProvider>(std::make_shared<InitialAuthData>(caPath));
+ AuthenticationPtr auth = AuthOauth2::create(params);
+ ASSERT_EQ(auth->getAuthData(data), ResultOk);
+ ASSERT_TRUE(data->hasDataFromCommand());
+ ASSERT_EQ(data->getCommandData(), "mockToken");
+
+ ASSERT_TRUE(wellKnownFuture.get());
+ ASSERT_TRUE(tokenFuture.get());
+ ASSERT_NE(wellKnownServer->request().find("GET
/.well-known/openid-configuration "), std::string::npos);
+ ASSERT_NE(tokenServer->request().find("POST /oauth/token "),
std::string::npos);
+ ASSERT_NE(tokenServer->request().find("grant_type=client_credentials"),
std::string::npos);
+ wellKnownThread.join();
+ tokenThread.join();
Review Comment:
These tests can hang indefinitely if the mock server thread blocks (e.g.,
accept/handshake/read never completes) because `future.get()` has no timeout
and the threads are joined only afterward. To prevent CI deadlocks, add bounded
waits (e.g., `wait_for` with a reasonable timeout) and fail the test if the
server doesn’t complete in time (optionally closing the acceptor/socket to
unblock).
##########
lib/auth/AuthOauth2.cc:
##########
@@ -199,80 +232,186 @@ KeyFile KeyFile::fromBase64(const std::string& encoded) {
}
}
-ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
- : issuerUrl_(params["issuer_url"]),
- keyFile_(KeyFile::fromParamMap(params)),
- audience_(params["audience"]),
- scope_(params["scope"]) {}
+static std::string getWellKnownUrl(const std::string& issuerUrl) {
+ std::string wellKnownUrl = issuerUrl;
+ if (!wellKnownUrl.empty() && wellKnownUrl.back() == '/') {
+ wellKnownUrl.pop_back();
+ }
+ wellKnownUrl.append("/.well-known/openid-configuration");
+ return wellKnownUrl;
+}
-std::string ClientCredentialFlow::getTokenEndPoint() const { return
tokenEndPoint_; }
+static std::unique_ptr<CurlWrapper::TlsContext> createTlsContext(const
std::string& tlsTrustCertsFilePath,
+ const
std::string& tlsCertFilePath,
+ const
std::string& tlsKeyFilePath,
+
OAuth2TokenEndpointAuthMethod authMethod) {
+ if (tlsTrustCertsFilePath.empty() && tlsCertFilePath.empty() &&
tlsKeyFilePath.empty()) {
+ return nullptr;
+ }
-void ClientCredentialFlow::initialize() {
- if (issuerUrl_.empty()) {
- LOG_ERROR("Failed to initialize ClientCredentialFlow: issuer_url is
not set");
- return;
+ auto tlsContext = std::unique_ptr<CurlWrapper::TlsContext>(new
CurlWrapper::TlsContext);
+ if (!tlsTrustCertsFilePath.empty()) {
+ tlsContext->trustCertsFilePath = tlsTrustCertsFilePath;
}
- if (!keyFile_.isValid()) {
- return;
+ if (!tlsCertFilePath.empty() && !tlsKeyFilePath.empty()) {
+ tlsContext->certPath = tlsCertFilePath;
+ tlsContext->keyPath = tlsKeyFilePath;
+ } else if (authMethod == OAuth2TokenEndpointAuthMethod::TlsClientAuth) {
+ LOG_WARN("Ignore incomplete mTLS settings: both tls_cert_file and
tls_key_file are required");
}
+ return tlsContext;
+}
- // set URL: well-know endpoint
- std::string wellKnownUrl = issuerUrl_;
- if (wellKnownUrl.back() == '/') {
- wellKnownUrl.pop_back();
+static std::string fetchTokenEndpoint(const std::string& issuerUrl,
+ const CurlWrapper::TlsContext*
tlsContext) {
+ const auto wellKnownUrl = getWellKnownUrl(issuerUrl);
+ CurlWrapper curl;
+ if (!curl.init()) {
+ LOG_ERROR("Failed to initialize curl");
+ return "";
+ }
+
+ auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext);
+ if (!result.error.empty()) {
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return "";
+ }
+
+ const auto res = result.code;
+ const auto responseCode = result.responseCode;
+ const auto& responseData = result.responseData;
+ const auto& errorBuffer = result.serverError;
+
+ switch (res) {
+ case CURLE_OK:
+ LOG_DEBUG("Received well-known configuration data " << issuerUrl
<< " code " << responseCode);
+ if (responseCode == 200) {
+ boost::property_tree::ptree root;
+ std::stringstream stream;
+ stream << responseData;
+ try {
+ boost::property_tree::read_json(stream, root);
+ return root.get<std::string>("token_endpoint");
+ } catch (boost::property_tree::json_parser_error& e) {
+ LOG_ERROR("Failed to parse well-known configuration data
response: "
+ << e.what() << "\nInput Json = " <<
responseData);
+ return "";
+ }
+ } else {
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". response Code " << responseCode);
+ }
+ break;
+ default:
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". Error Code " << res << ": " <<
errorBuffer);
+ break;
+ }
+ return "";
+}
+
+static Oauth2TokenResultPtr fetchOauth2Token(const std::string& issuerUrl,
const std::string& tokenEndpoint,
+ const ParamMap& params,
+ const CurlWrapper::TlsContext*
tlsContext,
+ OAuth2TokenEndpointAuthMethod
authMethod) {
+ Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new
Oauth2TokenResult());
+ if (tokenEndpoint.empty()) {
+ return resultPtr;
}
- wellKnownUrl.append("/.well-known/openid-configuration");
CurlWrapper curl;
if (!curl.init()) {
LOG_ERROR("Failed to initialize curl");
- return;
+ return resultPtr;
}
- std::unique_ptr<CurlWrapper::TlsContext> tlsContext;
- if (!tlsTrustCertsFilePath_.empty()) {
- tlsContext.reset(new CurlWrapper::TlsContext);
- tlsContext->trustCertsFilePath = tlsTrustCertsFilePath_;
+
+ auto postData = buildClientCredentialsBody(curl, params);
+ if (postData.empty()) {
+ return resultPtr;
}
+ LOG_DEBUG("Generate URL encoded body for " << toFlowName(authMethod) << ":
" << postData);
- auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext.get());
+ CurlWrapper::Options options;
+ options.postFields = std::move(postData);
+ auto result =
+ curl.get(tokenEndpoint, "Content-Type:
application/x-www-form-urlencoded", options, tlsContext);
if (!result.error.empty()) {
- LOG_ERROR("Failed to get the well-known configuration " << issuerUrl_
<< ": " << result.error);
- return;
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return resultPtr;
}
Review Comment:
This error message is misleading in `fetchOauth2Token()`: the request here
targets the token endpoint, not the well-known configuration. Please update the
message to indicate token retrieval failure (and ideally include the token
endpoint URL rather than `issuerUrl`).
##########
lib/auth/AuthOauth2.cc:
##########
@@ -199,80 +232,186 @@ KeyFile KeyFile::fromBase64(const std::string& encoded) {
}
}
-ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
- : issuerUrl_(params["issuer_url"]),
- keyFile_(KeyFile::fromParamMap(params)),
- audience_(params["audience"]),
- scope_(params["scope"]) {}
+static std::string getWellKnownUrl(const std::string& issuerUrl) {
+ std::string wellKnownUrl = issuerUrl;
+ if (!wellKnownUrl.empty() && wellKnownUrl.back() == '/') {
+ wellKnownUrl.pop_back();
+ }
+ wellKnownUrl.append("/.well-known/openid-configuration");
+ return wellKnownUrl;
+}
-std::string ClientCredentialFlow::getTokenEndPoint() const { return
tokenEndPoint_; }
+static std::unique_ptr<CurlWrapper::TlsContext> createTlsContext(const
std::string& tlsTrustCertsFilePath,
+ const
std::string& tlsCertFilePath,
+ const
std::string& tlsKeyFilePath,
+
OAuth2TokenEndpointAuthMethod authMethod) {
+ if (tlsTrustCertsFilePath.empty() && tlsCertFilePath.empty() &&
tlsKeyFilePath.empty()) {
+ return nullptr;
+ }
-void ClientCredentialFlow::initialize() {
- if (issuerUrl_.empty()) {
- LOG_ERROR("Failed to initialize ClientCredentialFlow: issuer_url is
not set");
- return;
+ auto tlsContext = std::unique_ptr<CurlWrapper::TlsContext>(new
CurlWrapper::TlsContext);
+ if (!tlsTrustCertsFilePath.empty()) {
+ tlsContext->trustCertsFilePath = tlsTrustCertsFilePath;
}
- if (!keyFile_.isValid()) {
- return;
+ if (!tlsCertFilePath.empty() && !tlsKeyFilePath.empty()) {
+ tlsContext->certPath = tlsCertFilePath;
+ tlsContext->keyPath = tlsKeyFilePath;
+ } else if (authMethod == OAuth2TokenEndpointAuthMethod::TlsClientAuth) {
+ LOG_WARN("Ignore incomplete mTLS settings: both tls_cert_file and
tls_key_file are required");
}
+ return tlsContext;
+}
Review Comment:
If only one of `tls_cert_file`/`tls_key_file` is set (and
`tlsTrustCertsFilePath` is empty), this returns a non-null `TlsContext` with
all-empty fields. Depending on how `CurlWrapper` applies options, passing a
non-null-but-empty TLS context can change behavior or even break requests.
Consider only allocating/returning a context when at least one concrete field
is actually set (trust certs, or both cert+key); otherwise return `nullptr`
(and optionally warn for incomplete mTLS regardless of auth method).
##########
lib/auth/AuthOauth2.cc:
##########
@@ -199,80 +232,186 @@ KeyFile KeyFile::fromBase64(const std::string& encoded) {
}
}
-ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
- : issuerUrl_(params["issuer_url"]),
- keyFile_(KeyFile::fromParamMap(params)),
- audience_(params["audience"]),
- scope_(params["scope"]) {}
+static std::string getWellKnownUrl(const std::string& issuerUrl) {
+ std::string wellKnownUrl = issuerUrl;
+ if (!wellKnownUrl.empty() && wellKnownUrl.back() == '/') {
+ wellKnownUrl.pop_back();
+ }
+ wellKnownUrl.append("/.well-known/openid-configuration");
+ return wellKnownUrl;
+}
-std::string ClientCredentialFlow::getTokenEndPoint() const { return
tokenEndPoint_; }
+static std::unique_ptr<CurlWrapper::TlsContext> createTlsContext(const
std::string& tlsTrustCertsFilePath,
+ const
std::string& tlsCertFilePath,
+ const
std::string& tlsKeyFilePath,
+
OAuth2TokenEndpointAuthMethod authMethod) {
+ if (tlsTrustCertsFilePath.empty() && tlsCertFilePath.empty() &&
tlsKeyFilePath.empty()) {
+ return nullptr;
+ }
-void ClientCredentialFlow::initialize() {
- if (issuerUrl_.empty()) {
- LOG_ERROR("Failed to initialize ClientCredentialFlow: issuer_url is
not set");
- return;
+ auto tlsContext = std::unique_ptr<CurlWrapper::TlsContext>(new
CurlWrapper::TlsContext);
+ if (!tlsTrustCertsFilePath.empty()) {
+ tlsContext->trustCertsFilePath = tlsTrustCertsFilePath;
}
- if (!keyFile_.isValid()) {
- return;
+ if (!tlsCertFilePath.empty() && !tlsKeyFilePath.empty()) {
+ tlsContext->certPath = tlsCertFilePath;
+ tlsContext->keyPath = tlsKeyFilePath;
+ } else if (authMethod == OAuth2TokenEndpointAuthMethod::TlsClientAuth) {
+ LOG_WARN("Ignore incomplete mTLS settings: both tls_cert_file and
tls_key_file are required");
}
+ return tlsContext;
+}
- // set URL: well-know endpoint
- std::string wellKnownUrl = issuerUrl_;
- if (wellKnownUrl.back() == '/') {
- wellKnownUrl.pop_back();
+static std::string fetchTokenEndpoint(const std::string& issuerUrl,
+ const CurlWrapper::TlsContext*
tlsContext) {
+ const auto wellKnownUrl = getWellKnownUrl(issuerUrl);
+ CurlWrapper curl;
+ if (!curl.init()) {
+ LOG_ERROR("Failed to initialize curl");
+ return "";
+ }
+
+ auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext);
+ if (!result.error.empty()) {
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return "";
+ }
+
+ const auto res = result.code;
+ const auto responseCode = result.responseCode;
+ const auto& responseData = result.responseData;
+ const auto& errorBuffer = result.serverError;
+
+ switch (res) {
+ case CURLE_OK:
+ LOG_DEBUG("Received well-known configuration data " << issuerUrl
<< " code " << responseCode);
+ if (responseCode == 200) {
+ boost::property_tree::ptree root;
+ std::stringstream stream;
+ stream << responseData;
+ try {
+ boost::property_tree::read_json(stream, root);
+ return root.get<std::string>("token_endpoint");
+ } catch (boost::property_tree::json_parser_error& e) {
+ LOG_ERROR("Failed to parse well-known configuration data
response: "
+ << e.what() << "\nInput Json = " <<
responseData);
+ return "";
+ }
+ } else {
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". response Code " << responseCode);
+ }
+ break;
+ default:
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". Error Code " << res << ": " <<
errorBuffer);
+ break;
+ }
+ return "";
+}
+
+static Oauth2TokenResultPtr fetchOauth2Token(const std::string& issuerUrl,
const std::string& tokenEndpoint,
+ const ParamMap& params,
+ const CurlWrapper::TlsContext*
tlsContext,
+ OAuth2TokenEndpointAuthMethod
authMethod) {
+ Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new
Oauth2TokenResult());
+ if (tokenEndpoint.empty()) {
+ return resultPtr;
}
- wellKnownUrl.append("/.well-known/openid-configuration");
CurlWrapper curl;
if (!curl.init()) {
LOG_ERROR("Failed to initialize curl");
- return;
+ return resultPtr;
}
- std::unique_ptr<CurlWrapper::TlsContext> tlsContext;
- if (!tlsTrustCertsFilePath_.empty()) {
- tlsContext.reset(new CurlWrapper::TlsContext);
- tlsContext->trustCertsFilePath = tlsTrustCertsFilePath_;
+
+ auto postData = buildClientCredentialsBody(curl, params);
+ if (postData.empty()) {
+ return resultPtr;
}
+ LOG_DEBUG("Generate URL encoded body for " << toFlowName(authMethod) << ":
" << postData);
- auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext.get());
+ CurlWrapper::Options options;
+ options.postFields = std::move(postData);
+ auto result =
+ curl.get(tokenEndpoint, "Content-Type:
application/x-www-form-urlencoded", options, tlsContext);
if (!result.error.empty()) {
- LOG_ERROR("Failed to get the well-known configuration " << issuerUrl_
<< ": " << result.error);
- return;
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return resultPtr;
}
const auto res = result.code;
- const auto response_code = result.responseCode;
+ const auto responseCode = result.responseCode;
const auto& responseData = result.responseData;
const auto& errorBuffer = result.serverError;
switch (res) {
case CURLE_OK:
- LOG_DEBUG("Received well-known configuration data " << issuerUrl_
<< " code " << response_code);
- if (response_code == 200) {
+ LOG_DEBUG("Response received for issuerurl " << issuerUrl << "
code " << responseCode);
+ if (responseCode == 200) {
boost::property_tree::ptree root;
std::stringstream stream;
stream << responseData;
try {
boost::property_tree::read_json(stream, root);
} catch (boost::property_tree::json_parser_error& e) {
- LOG_ERROR("Failed to parse well-known configuration data
response: "
- << e.what() << "\nInput Json = " <<
responseData);
+ LOG_ERROR("Failed to parse json of Oauth2 response: "
+ << e.what() << "\nInput Json = " << responseData
+ << " passedin: " << options.postFields);
break;
}
- this->tokenEndPoint_ = root.get<std::string>("token_endpoint");
+
resultPtr->setAccessToken(root.get<std::string>("access_token", ""));
+ resultPtr->setExpiresIn(
+ root.get<uint32_t>("expires_in",
Oauth2TokenResult::undefined_expiration));
+
resultPtr->setRefreshToken(root.get<std::string>("refresh_token", ""));
+ resultPtr->setIdToken(root.get<std::string>("id_token", ""));
- LOG_DEBUG("Get token endpoint: " << this->tokenEndPoint_);
+ if (!resultPtr->getAccessToken().empty()) {
+ LOG_DEBUG("access_token: " << resultPtr->getAccessToken()
+ << " expires_in: " <<
resultPtr->getExpiresIn());
+ } else {
+ LOG_ERROR("Response doesn't contain access_token, the
response is: " << responseData);
+ }
} else {
- LOG_ERROR("Response failed for getting the well-known
configuration "
- << issuerUrl_ << ". response Code " <<
response_code);
+ LOG_ERROR("Response failed for issuerurl " << issuerUrl << ".
response Code " << responseCode
+ << " passedin: " <<
options.postFields);
}
break;
default:
- LOG_ERROR("Response failed for getting the well-known
configuration "
- << issuerUrl_ << ". Error Code " << res << ": " <<
errorBuffer);
+ LOG_ERROR("Response failed for issuerurl " << issuerUrl << ".
ErrorCode " << res << ": "
+ << errorBuffer << "
passedin: " << options.postFields);
Review Comment:
Logging `options.postFields` risks leaking sensitive credentials (e.g.,
`client_secret`, assertions, etc.) into logs—especially problematic at
`LOG_ERROR` where logs are more likely to be retained/forwarded. Please remove
`passedin: ...` from error logs or redact sensitive parameters before logging
(e.g., log only parameter names, or mask known sensitive keys).
##########
lib/auth/AuthOauth2.cc:
##########
@@ -199,80 +232,186 @@ KeyFile KeyFile::fromBase64(const std::string& encoded) {
}
}
-ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
- : issuerUrl_(params["issuer_url"]),
- keyFile_(KeyFile::fromParamMap(params)),
- audience_(params["audience"]),
- scope_(params["scope"]) {}
+static std::string getWellKnownUrl(const std::string& issuerUrl) {
+ std::string wellKnownUrl = issuerUrl;
+ if (!wellKnownUrl.empty() && wellKnownUrl.back() == '/') {
+ wellKnownUrl.pop_back();
+ }
+ wellKnownUrl.append("/.well-known/openid-configuration");
+ return wellKnownUrl;
+}
-std::string ClientCredentialFlow::getTokenEndPoint() const { return
tokenEndPoint_; }
+static std::unique_ptr<CurlWrapper::TlsContext> createTlsContext(const
std::string& tlsTrustCertsFilePath,
+ const
std::string& tlsCertFilePath,
+ const
std::string& tlsKeyFilePath,
+
OAuth2TokenEndpointAuthMethod authMethod) {
+ if (tlsTrustCertsFilePath.empty() && tlsCertFilePath.empty() &&
tlsKeyFilePath.empty()) {
+ return nullptr;
+ }
-void ClientCredentialFlow::initialize() {
- if (issuerUrl_.empty()) {
- LOG_ERROR("Failed to initialize ClientCredentialFlow: issuer_url is
not set");
- return;
+ auto tlsContext = std::unique_ptr<CurlWrapper::TlsContext>(new
CurlWrapper::TlsContext);
+ if (!tlsTrustCertsFilePath.empty()) {
+ tlsContext->trustCertsFilePath = tlsTrustCertsFilePath;
}
- if (!keyFile_.isValid()) {
- return;
+ if (!tlsCertFilePath.empty() && !tlsKeyFilePath.empty()) {
+ tlsContext->certPath = tlsCertFilePath;
+ tlsContext->keyPath = tlsKeyFilePath;
+ } else if (authMethod == OAuth2TokenEndpointAuthMethod::TlsClientAuth) {
+ LOG_WARN("Ignore incomplete mTLS settings: both tls_cert_file and
tls_key_file are required");
}
+ return tlsContext;
+}
- // set URL: well-know endpoint
- std::string wellKnownUrl = issuerUrl_;
- if (wellKnownUrl.back() == '/') {
- wellKnownUrl.pop_back();
+static std::string fetchTokenEndpoint(const std::string& issuerUrl,
+ const CurlWrapper::TlsContext*
tlsContext) {
+ const auto wellKnownUrl = getWellKnownUrl(issuerUrl);
+ CurlWrapper curl;
+ if (!curl.init()) {
+ LOG_ERROR("Failed to initialize curl");
+ return "";
+ }
+
+ auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext);
+ if (!result.error.empty()) {
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return "";
+ }
+
+ const auto res = result.code;
+ const auto responseCode = result.responseCode;
+ const auto& responseData = result.responseData;
+ const auto& errorBuffer = result.serverError;
+
+ switch (res) {
+ case CURLE_OK:
+ LOG_DEBUG("Received well-known configuration data " << issuerUrl
<< " code " << responseCode);
+ if (responseCode == 200) {
+ boost::property_tree::ptree root;
+ std::stringstream stream;
+ stream << responseData;
+ try {
+ boost::property_tree::read_json(stream, root);
+ return root.get<std::string>("token_endpoint");
+ } catch (boost::property_tree::json_parser_error& e) {
+ LOG_ERROR("Failed to parse well-known configuration data
response: "
+ << e.what() << "\nInput Json = " <<
responseData);
+ return "";
+ }
+ } else {
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". response Code " << responseCode);
+ }
+ break;
+ default:
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". Error Code " << res << ": " <<
errorBuffer);
+ break;
+ }
+ return "";
+}
+
+static Oauth2TokenResultPtr fetchOauth2Token(const std::string& issuerUrl,
const std::string& tokenEndpoint,
+ const ParamMap& params,
+ const CurlWrapper::TlsContext*
tlsContext,
+ OAuth2TokenEndpointAuthMethod
authMethod) {
+ Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new
Oauth2TokenResult());
+ if (tokenEndpoint.empty()) {
+ return resultPtr;
}
- wellKnownUrl.append("/.well-known/openid-configuration");
CurlWrapper curl;
if (!curl.init()) {
LOG_ERROR("Failed to initialize curl");
- return;
+ return resultPtr;
}
- std::unique_ptr<CurlWrapper::TlsContext> tlsContext;
- if (!tlsTrustCertsFilePath_.empty()) {
- tlsContext.reset(new CurlWrapper::TlsContext);
- tlsContext->trustCertsFilePath = tlsTrustCertsFilePath_;
+
+ auto postData = buildClientCredentialsBody(curl, params);
+ if (postData.empty()) {
+ return resultPtr;
}
+ LOG_DEBUG("Generate URL encoded body for " << toFlowName(authMethod) << ":
" << postData);
- auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext.get());
+ CurlWrapper::Options options;
+ options.postFields = std::move(postData);
+ auto result =
+ curl.get(tokenEndpoint, "Content-Type:
application/x-www-form-urlencoded", options, tlsContext);
if (!result.error.empty()) {
- LOG_ERROR("Failed to get the well-known configuration " << issuerUrl_
<< ": " << result.error);
- return;
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return resultPtr;
}
const auto res = result.code;
- const auto response_code = result.responseCode;
+ const auto responseCode = result.responseCode;
const auto& responseData = result.responseData;
const auto& errorBuffer = result.serverError;
switch (res) {
case CURLE_OK:
- LOG_DEBUG("Received well-known configuration data " << issuerUrl_
<< " code " << response_code);
- if (response_code == 200) {
+ LOG_DEBUG("Response received for issuerurl " << issuerUrl << "
code " << responseCode);
+ if (responseCode == 200) {
boost::property_tree::ptree root;
std::stringstream stream;
stream << responseData;
try {
boost::property_tree::read_json(stream, root);
} catch (boost::property_tree::json_parser_error& e) {
- LOG_ERROR("Failed to parse well-known configuration data
response: "
- << e.what() << "\nInput Json = " <<
responseData);
+ LOG_ERROR("Failed to parse json of Oauth2 response: "
+ << e.what() << "\nInput Json = " << responseData
+ << " passedin: " << options.postFields);
Review Comment:
Logging `options.postFields` risks leaking sensitive credentials (e.g.,
`client_secret`, assertions, etc.) into logs—especially problematic at
`LOG_ERROR` where logs are more likely to be retained/forwarded. Please remove
`passedin: ...` from error logs or redact sensitive parameters before logging
(e.g., log only parameter names, or mask known sensitive keys).
##########
lib/auth/AuthOauth2.cc:
##########
@@ -199,80 +232,186 @@ KeyFile KeyFile::fromBase64(const std::string& encoded) {
}
}
-ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
- : issuerUrl_(params["issuer_url"]),
- keyFile_(KeyFile::fromParamMap(params)),
- audience_(params["audience"]),
- scope_(params["scope"]) {}
+static std::string getWellKnownUrl(const std::string& issuerUrl) {
+ std::string wellKnownUrl = issuerUrl;
+ if (!wellKnownUrl.empty() && wellKnownUrl.back() == '/') {
+ wellKnownUrl.pop_back();
+ }
+ wellKnownUrl.append("/.well-known/openid-configuration");
+ return wellKnownUrl;
+}
-std::string ClientCredentialFlow::getTokenEndPoint() const { return
tokenEndPoint_; }
+static std::unique_ptr<CurlWrapper::TlsContext> createTlsContext(const
std::string& tlsTrustCertsFilePath,
+ const
std::string& tlsCertFilePath,
+ const
std::string& tlsKeyFilePath,
+
OAuth2TokenEndpointAuthMethod authMethod) {
+ if (tlsTrustCertsFilePath.empty() && tlsCertFilePath.empty() &&
tlsKeyFilePath.empty()) {
+ return nullptr;
+ }
-void ClientCredentialFlow::initialize() {
- if (issuerUrl_.empty()) {
- LOG_ERROR("Failed to initialize ClientCredentialFlow: issuer_url is
not set");
- return;
+ auto tlsContext = std::unique_ptr<CurlWrapper::TlsContext>(new
CurlWrapper::TlsContext);
+ if (!tlsTrustCertsFilePath.empty()) {
+ tlsContext->trustCertsFilePath = tlsTrustCertsFilePath;
}
- if (!keyFile_.isValid()) {
- return;
+ if (!tlsCertFilePath.empty() && !tlsKeyFilePath.empty()) {
+ tlsContext->certPath = tlsCertFilePath;
+ tlsContext->keyPath = tlsKeyFilePath;
+ } else if (authMethod == OAuth2TokenEndpointAuthMethod::TlsClientAuth) {
+ LOG_WARN("Ignore incomplete mTLS settings: both tls_cert_file and
tls_key_file are required");
}
+ return tlsContext;
+}
- // set URL: well-know endpoint
- std::string wellKnownUrl = issuerUrl_;
- if (wellKnownUrl.back() == '/') {
- wellKnownUrl.pop_back();
+static std::string fetchTokenEndpoint(const std::string& issuerUrl,
+ const CurlWrapper::TlsContext*
tlsContext) {
+ const auto wellKnownUrl = getWellKnownUrl(issuerUrl);
+ CurlWrapper curl;
+ if (!curl.init()) {
+ LOG_ERROR("Failed to initialize curl");
+ return "";
+ }
+
+ auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext);
+ if (!result.error.empty()) {
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return "";
+ }
+
+ const auto res = result.code;
+ const auto responseCode = result.responseCode;
+ const auto& responseData = result.responseData;
+ const auto& errorBuffer = result.serverError;
+
+ switch (res) {
+ case CURLE_OK:
+ LOG_DEBUG("Received well-known configuration data " << issuerUrl
<< " code " << responseCode);
+ if (responseCode == 200) {
+ boost::property_tree::ptree root;
+ std::stringstream stream;
+ stream << responseData;
+ try {
+ boost::property_tree::read_json(stream, root);
+ return root.get<std::string>("token_endpoint");
+ } catch (boost::property_tree::json_parser_error& e) {
+ LOG_ERROR("Failed to parse well-known configuration data
response: "
+ << e.what() << "\nInput Json = " <<
responseData);
+ return "";
+ }
+ } else {
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". response Code " << responseCode);
+ }
+ break;
+ default:
+ LOG_ERROR("Response failed for getting the well-known
configuration "
+ << issuerUrl << ". Error Code " << res << ": " <<
errorBuffer);
+ break;
+ }
+ return "";
+}
+
+static Oauth2TokenResultPtr fetchOauth2Token(const std::string& issuerUrl,
const std::string& tokenEndpoint,
+ const ParamMap& params,
+ const CurlWrapper::TlsContext*
tlsContext,
+ OAuth2TokenEndpointAuthMethod
authMethod) {
+ Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new
Oauth2TokenResult());
+ if (tokenEndpoint.empty()) {
+ return resultPtr;
}
- wellKnownUrl.append("/.well-known/openid-configuration");
CurlWrapper curl;
if (!curl.init()) {
LOG_ERROR("Failed to initialize curl");
- return;
+ return resultPtr;
}
- std::unique_ptr<CurlWrapper::TlsContext> tlsContext;
- if (!tlsTrustCertsFilePath_.empty()) {
- tlsContext.reset(new CurlWrapper::TlsContext);
- tlsContext->trustCertsFilePath = tlsTrustCertsFilePath_;
+
+ auto postData = buildClientCredentialsBody(curl, params);
+ if (postData.empty()) {
+ return resultPtr;
}
+ LOG_DEBUG("Generate URL encoded body for " << toFlowName(authMethod) << ":
" << postData);
- auto result = curl.get(wellKnownUrl, "Accept: application/json", {},
tlsContext.get());
+ CurlWrapper::Options options;
+ options.postFields = std::move(postData);
+ auto result =
+ curl.get(tokenEndpoint, "Content-Type:
application/x-www-form-urlencoded", options, tlsContext);
if (!result.error.empty()) {
- LOG_ERROR("Failed to get the well-known configuration " << issuerUrl_
<< ": " << result.error);
- return;
+ LOG_ERROR("Failed to get the well-known configuration " << issuerUrl
<< ": " << result.error);
+ return resultPtr;
}
const auto res = result.code;
- const auto response_code = result.responseCode;
+ const auto responseCode = result.responseCode;
const auto& responseData = result.responseData;
const auto& errorBuffer = result.serverError;
switch (res) {
case CURLE_OK:
- LOG_DEBUG("Received well-known configuration data " << issuerUrl_
<< " code " << response_code);
- if (response_code == 200) {
+ LOG_DEBUG("Response received for issuerurl " << issuerUrl << "
code " << responseCode);
+ if (responseCode == 200) {
boost::property_tree::ptree root;
std::stringstream stream;
stream << responseData;
try {
boost::property_tree::read_json(stream, root);
} catch (boost::property_tree::json_parser_error& e) {
- LOG_ERROR("Failed to parse well-known configuration data
response: "
- << e.what() << "\nInput Json = " <<
responseData);
+ LOG_ERROR("Failed to parse json of Oauth2 response: "
+ << e.what() << "\nInput Json = " << responseData
+ << " passedin: " << options.postFields);
break;
}
- this->tokenEndPoint_ = root.get<std::string>("token_endpoint");
+
resultPtr->setAccessToken(root.get<std::string>("access_token", ""));
+ resultPtr->setExpiresIn(
+ root.get<uint32_t>("expires_in",
Oauth2TokenResult::undefined_expiration));
+
resultPtr->setRefreshToken(root.get<std::string>("refresh_token", ""));
+ resultPtr->setIdToken(root.get<std::string>("id_token", ""));
- LOG_DEBUG("Get token endpoint: " << this->tokenEndPoint_);
+ if (!resultPtr->getAccessToken().empty()) {
+ LOG_DEBUG("access_token: " << resultPtr->getAccessToken()
+ << " expires_in: " <<
resultPtr->getExpiresIn());
+ } else {
+ LOG_ERROR("Response doesn't contain access_token, the
response is: " << responseData);
+ }
} else {
- LOG_ERROR("Response failed for getting the well-known
configuration "
- << issuerUrl_ << ". response Code " <<
response_code);
+ LOG_ERROR("Response failed for issuerurl " << issuerUrl << ".
response Code " << responseCode
+ << " passedin: " <<
options.postFields);
Review Comment:
Logging `options.postFields` risks leaking sensitive credentials (e.g.,
`client_secret`, assertions, etc.) into logs—especially problematic at
`LOG_ERROR` where logs are more likely to be retained/forwarded. Please remove
`passedin: ...` from error logs or redact sensitive parameters before logging
(e.g., log only parameter names, or mask known sensitive keys).
##########
include/pulsar/Authentication.h:
##########
@@ -515,11 +515,22 @@ typedef std::shared_ptr<CachedToken> CachedTokenPtr;
* Passed in parameter would be like:
* ```
* "type": "client_credentials",
+ * "tokenEndpointAuthMethod": "client_secret_post",
* "issuer_url": "https://accounts.google.com",
* "client_id": "d9ZyX97q1ef8Cr81WHVC4hFQ64vSlDK3",
* "client_secret": "on1uJ...k6F6R",
Review Comment:
The doc example uses `client_secret`, but the constructor documentation
below states the required key is `private_key` for `client_secret_post`, and
the implementation uses `KeyFile::fromParamMap()` which expects `private_key`.
Please align the example and the documented required keys with the actual
supported parameters to avoid confusing API consumers.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]