Skip to content

Commit

Permalink
[C++] Use URL encoded content type for OAuth 2.0 authentication (apac…
Browse files Browse the repository at this point in the history
…he#12341)

Fixes apache#12334

### Motivation

When C++ client sends a HTTP request for the access token from a OAuth 2.0 server, the content type is JSON, which is incorrect and might not work in some cases.

### Modifications

- Replace `generateJsonBody` with `generateParamMap` to create a map that contains the necessary keys from `CredentialsFlow`.
- Add a `buildClientCredentialsBody` method to convert the map to URL encoded string.
- Change the content type from `json` to `x-www-form-urlencoded`.

### Verifying this change

- [x] Make sure that the change passes the CI checks.

This change is already covered by existing tests, such as `AuthPluginTest.testOauth2RequestBody`. This PR changes the test to verify `generateParamMap` because `generateJsonBody` is removed.
  • Loading branch information
BewareMyPower authored Oct 13, 2021
1 parent 06b68bb commit 4ae7f6a
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 39 deletions.
72 changes: 51 additions & 21 deletions pulsar-client-cpp/lib/auth/AuthOauth2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -235,41 +235,71 @@ void ClientCredentialFlow::initialize() {
}
void ClientCredentialFlow::close() {}

std::string ClientCredentialFlow::generateJsonBody() const {
ParamMap ClientCredentialFlow::generateParamMap() const {
if (!keyFile_.isValid()) {
return "";
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_);
ParamMap params;
params.emplace("grant_type", "client_credentials");
params.emplace("client_id", keyFile_.getClientId());
params.emplace("client_secret", keyFile_.getClientSecret());
params.emplace("audience", audience_);
if (!scope_.empty()) {
pt.put("scope", scope_);
params.emplace("scope", scope_);
}
return params;
}

static std::string buildClientCredentialsBody(CURL* curl, const ParamMap& params) {
std::ostringstream oss;
bool addSeparater = false;

for (const auto& kv : params) {
if (addSeparater) {
oss << "&";
} else {
addSeparater = true;
}

char* encodedKey = curl_easy_escape(curl, kv.first.c_str(), kv.first.length());
if (!encodedKey) {
LOG_ERROR("curl_easy_escape for " << kv.first << " failed");
continue;
}
char* encodedValue = curl_easy_escape(curl, kv.second.c_str(), kv.second.length());
if (!encodedValue) {
LOG_ERROR("curl_easy_escape for " << kv.second << " failed");
continue;
}

oss << encodedKey << "=" << encodedValue;
curl_free(encodedKey);
curl_free(encodedValue);
}

std::ostringstream ss;
boost::property_tree::json_parser::write_json(ss, pt);
return ss.str();
return oss.str();
}

Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
Oauth2TokenResultPtr resultPtr = Oauth2TokenResultPtr(new Oauth2TokenResult());
const auto jsonBody = generateJsonBody();
if (jsonBody.empty() || tokenEndPoint_.empty()) {
if (tokenEndPoint_.empty()) {
return resultPtr;
}
LOG_DEBUG("Generate JSON body for ClientCredentialFlow: " << jsonBody);

CURL* handle = curl_easy_init();
const auto postData = buildClientCredentialsBody(handle, generateParamMap());
if (postData.empty()) {
curl_easy_cleanup(handle);
return resultPtr;
}
LOG_DEBUG("Generate URL encoded body for ClientCredentialFlow: " << postData);

CURLcode res;
std::string responseData;

// set header: json, request type: post
struct curl_slist* list = NULL;
list = curl_slist_append(list, "Content-Type: application/json");
list = curl_slist_append(list, "Content-Type: application/x-www-form-urlencoded");
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(handle, CURLOPT_CUSTOMREQUEST, "POST");

Expand All @@ -288,7 +318,7 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);

curl_easy_setopt(handle, CURLOPT_POSTFIELDS, jsonBody.c_str());
curl_easy_setopt(handle, CURLOPT_POSTFIELDS, postData.c_str());

char errorBuffer[CURL_ERROR_SIZE];
curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, errorBuffer);
Expand All @@ -309,7 +339,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: " << jsonBody);
<< e.what() << "\nInput Json = " << responseData << " passedin: " << postData);
break;
}

Expand All @@ -327,12 +357,12 @@ Oauth2TokenResultPtr ClientCredentialFlow::authenticate() {
}
} else {
LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ". response Code "
<< response_code << " passedin: " << jsonBody);
<< response_code << " passedin: " << postData);
}
break;
default:
LOG_ERROR("Response failed for issuerurl " << issuerUrl_ << ". ErrorCode " << res << ": "
<< errorBuffer << " passedin: " << jsonBody);
<< errorBuffer << " passedin: " << postData);
break;
}
// Free header list
Expand Down
2 changes: 1 addition & 1 deletion pulsar-client-cpp/lib/auth/AuthOauth2.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ClientCredentialFlow : public Oauth2Flow {
Oauth2TokenResultPtr authenticate();
void close();

std::string generateJsonBody() const;
ParamMap generateParamMap() const;

private:
std::string tokenEndPoint_;
Expand Down
27 changes: 10 additions & 17 deletions pulsar-client-cpp/tests/AuthPluginTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -395,28 +395,21 @@ TEST(AuthPluginTest, testOauth2RequestBody) {
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\/"
}
)";
auto createExpectedResult = [&] {
auto paramsCopy = params;
paramsCopy.emplace("grant_type", "client_credentials");
paramsCopy.erase("issuer_url");
return paramsCopy;
};

const auto expectedResult1 = createExpectedResult();
ClientCredentialFlow flow1(params);
ASSERT_EQ(flow1.generateJsonBody(), expectedJson);
ASSERT_EQ(flow1.generateParamMap(), expectedResult1);

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"
}
)";
const auto expectedResult2 = createExpectedResult();
ClientCredentialFlow flow2(params);
ASSERT_EQ(flow2.generateJsonBody(), expectedJson);
ASSERT_EQ(flow2.generateParamMap(), expectedResult2);
}

TEST(AuthPluginTest, testOauth2Failure) {
Expand Down

0 comments on commit 4ae7f6a

Please sign in to comment.