This is an automated email from the ASF dual-hosted git repository.
guangning pushed a commit to branch branch-2.8
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/branch-2.8 by this push:
new 8ef70a8 [C++] Support configuring optional scope field for OAuth2
authentication (#12305)
8ef70a8 is described below
commit 8ef70a8f009e133830ff6951a97d32941cb049c8
Author: Yunze Xu <[email protected]>
AuthorDate: Sat Oct 9 08:14:40 2021 +0800
[C++] Support configuring optional scope field for OAuth2 authentication
(#12305)
It's a C++ client catchup for https://github.com/apache/pulsar/pull/11931.
- Add a `scope_` field to `ClientCredentialFlow` and load it from
`ParamMap` object whose key is `scope`.
- Refactor `ClientCredentialFlow` to simplify code and make it testable:
- Use only one constructor instead of two overloaded constructors that
might look confused
- Add a `generateJsonBody` public method for generating JSON body for
post fields in `authenticate` so that it can be tested.
- Add a `KeyFile` class like what Java client does to load client id and
client secret from `ParamMap` or file.
- [x] Make sure that the change passes the CI checks.
This change added test `AuthPluginTest.testOauth2RequestBody` for the cases
that scope exists or doesn't exist.
(cherry picked from commit 44dcc04d037511984ec383fb3f2c75170e4cfefc)
---
pulsar-client-cpp/lib/auth/AuthOauth2.cc | 124 ++++++++++++++----------------
pulsar-client-cpp/lib/auth/AuthOauth2.h | 35 +++++++--
pulsar-client-cpp/tests/AuthPluginTest.cc | 32 ++++++++
3 files changed, 118 insertions(+), 73 deletions(-)
diff --git a/pulsar-client-cpp/lib/auth/AuthOauth2.cc
b/pulsar-client-cpp/lib/auth/AuthOauth2.cc
index 33ae450..c7cc2bf 100644
--- a/pulsar-client-cpp/lib/auth/AuthOauth2.cc
+++ b/pulsar-client-cpp/lib/auth/AuthOauth2.cc
@@ -119,57 +119,50 @@ bool Oauth2CachedToken::isExpired() { return expiresAt_ <
currentTimeMillis(); }
Oauth2Flow::Oauth2Flow() {}
Oauth2Flow::~Oauth2Flow() {}
-// ClientCredentialFlow
-static std::string readFromFile(const std::string& credentialsFilePath) {
- std::ifstream input(credentialsFilePath);
- std::stringstream buffer;
- buffer << input.rdbuf();
- return buffer.str();
-}
-
-ClientCredentialFlow::ClientCredentialFlow(const std::string& issuerUrl, const
std::string& clientId,
- const std::string& clientSecret,
const std::string& audience) {
- issuerUrl_ = issuerUrl;
- clientId_ = clientId;
- clientSecret_ = clientSecret;
- audience_ = audience;
- this->initialize();
+KeyFile KeyFile::fromParamMap(ParamMap& params) {
+ const auto it = params.find("private_key");
+ if (it != params.cend()) {
+ return fromFile(it->second);
+ } else {
+ return {params["client_id"], params["client_secret"]};
+ }
}
// read clientId/clientSecret from passed in `credentialsFilePath`
-ClientCredentialFlow::ClientCredentialFlow(const std::string& issuerUrl,
- const std::string&
credentialsFilePath,
- const std::string& audience) {
- issuerUrl_ = issuerUrl;
- audience_ = audience;
-
+KeyFile KeyFile::fromFile(const std::string& credentialsFilePath) {
boost::property_tree::ptree loadPtreeRoot;
try {
boost::property_tree::read_json(credentialsFilePath, loadPtreeRoot);
- } catch (boost::property_tree::json_parser_error& e) {
- LOG_ERROR("Failed to parse json input file for credentialsFilePath: "
<< credentialsFilePath
-
<< "with error:" << e.what());
- return;
+ } catch (const boost::property_tree::json_parser_error& e) {
+ LOG_ERROR("Failed to parse json input file for credentialsFilePath: "
<< credentialsFilePath << ": "
+
<< e.what());
+ return {};
}
- const std::string defaultNotFoundString = "Client Id / Secret Not Found";
-
- clientId_ = loadPtreeRoot.get<std::string>("client_id",
defaultNotFoundString);
- clientSecret_ = loadPtreeRoot.get<std::string>("client_secret",
defaultNotFoundString);
-
- if (clientId_ == defaultNotFoundString || clientSecret_ ==
defaultNotFoundString) {
- LOG_ERROR("Not get valid clientId / clientSecret: " << clientId_ <<
"/" << clientSecret_);
- return;
+ try {
+ return {loadPtreeRoot.get<std::string>("client_id"),
loadPtreeRoot.get<std::string>("client_secret")};
+ } catch (const boost::property_tree::ptree_error& e) {
+ LOG_ERROR("Failed to get client_id or client_secret in " <<
credentialsFilePath << ": " << e.what());
+ return {};
}
- this->initialize();
}
+ClientCredentialFlow::ClientCredentialFlow(ParamMap& params)
+ : issuerUrl_(params["issuer_url"]),
+ keyFile_(KeyFile::fromParamMap(params)),
+ audience_(params["audience"]),
+ scope_(params["scope"]) {}
+
static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb,
void* responseDataPtr) {
((std::string*)responseDataPtr)->append((char*)contents, size * nmemb);
return size * nmemb;
}
void ClientCredentialFlow::initialize() {
+ if (!keyFile_.isValid()) {
+ return;
+ }
+
CURL* handle = curl_easy_init();
CURLcode res;
std::string responseData;
@@ -181,8 +174,7 @@ void ClientCredentialFlow::initialize() {
curl_easy_setopt(handle, CURLOPT_CUSTOMREQUEST, "GET");
// set URL: well-know endpoint
- issuerUrl_.append("/.well-known/openid-configuration");
- curl_easy_setopt(handle, CURLOPT_URL, issuerUrl_.c_str());
+ curl_easy_setopt(handle, CURLOPT_URL, (issuerUrl_ +
"/.well-known/openid-configuration").c_str());
// Write callback
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, curlWriteCallback);
@@ -235,8 +227,33 @@ void ClientCredentialFlow::initialize() {
}
void ClientCredentialFlow::close() {}
+std::string ClientCredentialFlow::generateJsonBody() const {
+ if (!keyFile_.isValid()) {
+ return "";
+ }
+
+ // fill in the request data
+ boost::property_tree::ptree pt;
+ pt.put("grant_type", "client_credentials");
+ pt.put("client_id", keyFile_.getClientId());
+ pt.put("client_secret", keyFile_.getClientSecret());
+ pt.put("audience", audience_);
+ if (!scope_.empty()) {
+ pt.put("scope", scope_);
+ }
+
+ std::ostringstream ss;
+ boost::property_tree::json_parser::write_json(ss, pt);
+ return ss.str();
+}
+
Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new
Oauth2TokenResult());
+ const auto jsonBody = generateJsonBody();
+ if (jsonBody.empty() || tokenEndPoint_.empty()) {
+ return resultPtr;
+ }
+ LOG_DEBUG("Generate JSON body for ClientCredentialFlow: " << jsonBody);
CURL* handle = curl_easy_init();
CURLcode res;
@@ -263,25 +280,11 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate()
{
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
- // fill in the request data
- boost::property_tree::ptree pt;
- pt.put("grant_type", "client_credentials");
- pt.put("client_id", clientId_);
- pt.put("client_secret", clientSecret_);
- pt.put("audience", audience_);
-
- std::stringstream ss;
- boost::property_tree::json_parser::write_json(ss, pt);
- std::string ssString = ss.str();
-
- curl_easy_setopt(handle, CURLOPT_POSTFIELDS, ssString.c_str());
+ curl_easy_setopt(handle, CURLOPT_POSTFIELDS, jsonBody.c_str());
// Make get call to server
res = curl_easy_perform(handle);
- LOG_DEBUG("issuerUrl_ " << issuerUrl_ << " clientid: " << clientId_ << "
client_secret " << clientSecret_
- << " audience " << audience_ << " ssstring " <<
ssString);
-
switch (res) {
case CURLE_OK:
long response_code;
@@ -295,7 +298,7 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
boost::property_tree::read_json(stream, root);
} catch (boost::property_tree::json_parser_error& e) {
LOG_ERROR("Failed to parse json of Oauth2 response: "
- << e.what() << "\nInput Json = " << responseData
<< " passedin: " << ssString);
+ << e.what() << "\nInput Json = " << responseData
<< " passedin: " << jsonBody);
break;
}
@@ -306,12 +309,12 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate()
{
<< " expires_in: " <<
resultPtr->getExpiresIn());
} else {
LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ".
response Code "
- << response_code <<
" passedin: " << ssString);
+ << response_code <<
" passedin: " << jsonBody);
}
break;
default:
LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ".
Error Code " << res
- << " passedin: " <<
ssString);
+ << " passedin: " <<
jsonBody);
break;
}
// Free header list
@@ -323,17 +326,8 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
// AuthOauth2
-AuthOauth2::AuthOauth2(ParamMap& params) {
- std::map<std::string, std::string>::iterator it;
- it = params.find("private_key");
-
- if (it != params.end()) {
- flowPtr_ = FlowPtr(
- new ClientCredentialFlow(params["issuer_url"],
params["private_key"], params["audience"]));
- } else {
- flowPtr_ = FlowPtr(new ClientCredentialFlow(params["issuer_url"],
params["client_id"],
- params["client_secret"],
params["audience"]));
- }
+AuthOauth2::AuthOauth2(ParamMap& params) : flowPtr_(new
ClientCredentialFlow(params)) {
+ flowPtr_->initialize();
}
AuthOauth2::~AuthOauth2() {}
diff --git a/pulsar-client-cpp/lib/auth/AuthOauth2.h
b/pulsar-client-cpp/lib/auth/AuthOauth2.h
index 4de6f53..43f3743 100644
--- a/pulsar-client-cpp/lib/auth/AuthOauth2.h
+++ b/pulsar-client-cpp/lib/auth/AuthOauth2.h
@@ -28,22 +28,41 @@ const std::string OAUTH2_TOKEN_PLUGIN_NAME = "oauth2token";
const std::string OAUTH2_TOKEN_JAVA_PLUGIN_NAME =
"org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2";
+class KeyFile {
+ public:
+ static KeyFile fromParamMap(ParamMap& params);
+
+ const std::string& getClientId() const noexcept { return clientId_; }
+ const std::string& getClientSecret() const noexcept { return
clientSecret_; }
+ bool isValid() const noexcept { return valid_; }
+
+ private:
+ const std::string clientId_;
+ const std::string clientSecret_;
+ const bool valid_;
+
+ KeyFile(const std::string& clientId, const std::string& clientSecret)
+ : clientId_(clientId), clientSecret_(clientSecret), valid_(true) {}
+ KeyFile() : valid_(false) {}
+
+ static KeyFile fromFile(const std::string& filename);
+};
+
class ClientCredentialFlow : public Oauth2Flow {
public:
- ClientCredentialFlow(const std::string& issuerUrl, const std::string&
clientId,
- const std::string& clientSecret, const std::string&
audience);
- ClientCredentialFlow(const std::string& issuerUrl, const std::string&
credentialsFilePath,
- const std::string& audience);
+ ClientCredentialFlow(ParamMap& params);
void initialize();
Oauth2TokenResultPtr authenticate();
void close();
+ std::string generateJsonBody() const;
+
private:
std::string tokenEndPoint_;
- std::string issuerUrl_;
- std::string clientId_;
- std::string clientSecret_;
- std::string audience_;
+ const std::string issuerUrl_;
+ const KeyFile keyFile_;
+ const std::string audience_;
+ const std::string scope_;
};
class Oauth2CachedToken : public CachedToken {
diff --git a/pulsar-client-cpp/tests/AuthPluginTest.cc
b/pulsar-client-cpp/tests/AuthPluginTest.cc
index 2d7aa1d..d15a383 100644
--- a/pulsar-client-cpp/tests/AuthPluginTest.cc
+++ b/pulsar-client-cpp/tests/AuthPluginTest.cc
@@ -23,6 +23,7 @@
#include <boost/algorithm/string.hpp>
#include <thread>
#include <lib/LogUtils.h>
+#include <lib/auth/AuthOauth2.h>
#include "lib/Future.h"
#include "lib/Utils.h"
@@ -395,3 +396,34 @@ TEST(AuthPluginTest, testOauth2CredentialFile) {
ASSERT_EQ(data->hasDataFromCommand(), true);
ASSERT_EQ(data->getCommandData().length(), expectedTokenLength);
}
+
+TEST(AuthPluginTest, testOauth2RequestBody) {
+ ParamMap params;
+ params["issuer_url"] = "https://dev-kt-aa9ne.us.auth0.com";
+ params["client_id"] = "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x";
+ params["client_secret"] =
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb";
+ params["audience"] = "https://dev-kt-aa9ne.us.auth0.com/api/v2/";
+
+ std::string expectedJson = R"({
+ "grant_type": "client_credentials",
+ "client_id": "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x",
+ "client_secret":
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb",
+ "audience": "https:\/\/dev-kt-aa9ne.us.auth0.com\/api\/v2\/"
+}
+)";
+
+ ClientCredentialFlow flow1(params);
+ ASSERT_EQ(flow1.generateJsonBody(), expectedJson);
+
+ params["scope"] = "test-scope";
+ expectedJson = R"({
+ "grant_type": "client_credentials",
+ "client_id": "Xd23RHsUnvUlP7wchjNYOaIfazgeHd9x",
+ "client_secret":
"rT7ps7WY8uhdVuBTKWZkttwLdQotmdEliaM5rLfmgNibvqziZ-g07ZH52N_poGAb",
+ "audience": "https:\/\/dev-kt-aa9ne.us.auth0.com\/api\/v2\/",
+ "scope": "test-scope"
+}
+)";
+ ClientCredentialFlow flow2(params);
+ ASSERT_EQ(flow2.generateJsonBody(), expectedJson);
+}