Skip to content

Commit

Permalink
[C++] Support configuring optional scope field for OAuth2 authenticat…
Browse files Browse the repository at this point in the history
…ion (#12305)

### Motivation

It's a C++ client catchup for #11931.

### Modifications

- 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.

### Verifying this change

- [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.
  • Loading branch information
BewareMyPower authored Oct 9, 2021
1 parent 7b14e56 commit 44dcc04
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 65 deletions.
116 changes: 59 additions & 57 deletions pulsar-client-cpp/lib/auth/AuthOauth2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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
Expand All @@ -316,17 +327,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() {}
Expand Down
35 changes: 27 additions & 8 deletions pulsar-client-cpp/lib/auth/AuthOauth2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions pulsar-client-cpp/tests/AuthPluginTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
}

0 comments on commit 44dcc04

Please sign in to comment.