diff --git a/pulsar-client-cpp/lib/auth/AuthOauth2.cc b/pulsar-client-cpp/lib/auth/AuthOauth2.cc index 1dbfc12259a53..953dd8eff156f 100644 --- a/pulsar-client-cpp/lib/auth/AuthOauth2.cc +++ b/pulsar-client-cpp/lib/auth/AuthOauth2.cc @@ -120,49 +120,50 @@ bool Oauth2CachedToken::isExpired() { return expiresAt_ < currentTimeMillis(); } Oauth2Flow::Oauth2Flow() {} Oauth2Flow::~Oauth2Flow() {} -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("client_id", defaultNotFoundString); - clientSecret_ = loadPtreeRoot.get("client_secret", defaultNotFoundString); - - if (clientId_ == defaultNotFoundString || clientSecret_ == defaultNotFoundString) { - LOG_ERROR("Not get valid clientId / clientSecret: " << clientId_ << "/" << clientSecret_); - return; + try { + return {loadPtreeRoot.get("client_id"), loadPtreeRoot.get("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; @@ -174,8 +175,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); @@ -228,8 +228,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; @@ -256,25 +281,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; @@ -288,7 +299,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; } @@ -299,12 +310,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 @@ -316,17 +327,8 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() { // AuthOauth2 -AuthOauth2::AuthOauth2(ParamMap& params) { - std::map::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 4de6f53776668..43f3743c25221 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 066b065d78b4f..db448627ccc89 100644 --- a/pulsar-client-cpp/tests/AuthPluginTest.cc +++ b/pulsar-client-cpp/tests/AuthPluginTest.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include "lib/Future.h" #include "lib/Utils.h" @@ -388,3 +389,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); +}