Skip to content

Commit

Permalink
feat: create a C++ sample plugin for HMAC token authorization. (Googl…
Browse files Browse the repository at this point in the history
…eCloudPlatform#114)

* feat: adding c++ sample for HMAC token validation on querystring
  • Loading branch information
walves-cit authored Jan 22, 2025
1 parent 479f4bd commit 09369ff
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 0 deletions.
2 changes: 2 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ for your own plugin. Extend them to fit your particular use case.
* [Check for PII on response](samples/check_pii): Checks the response HTTP
headers and body for the presence of credit card numbers. If found, the
initial numbers will be masked.
* [Validate client token on query string using HMAC](samples/hmac_authtoken):
Check the client request URL for a valid token signed using HMAC.

# Feature set / ABI

Expand Down
28 changes: 28 additions & 0 deletions plugins/samples/hmac_authtoken/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("//:plugins.bzl", "proxy_wasm_plugin_cpp", "proxy_wasm_plugin_rust", "proxy_wasm_tests")

licenses(["notice"]) # Apache 2

proxy_wasm_plugin_cpp(
name = "plugin_cpp.wasm",
srcs = ["plugin.cc"],
deps = [
"@com_google_absl//absl/strings",
"@boringssl//:ssl",
"@boost//:url",
"//:boost_exception",
],
linkopts = [
# To avoid the error:
# library_pthread.js:26: #error "STANDALONE_WASM does not support shared memories yet".
# Disabling the pthreads avoids the inclusion of the library_pthread.js.
"-sUSE_PTHREADS=0",
],
)

proxy_wasm_tests(
name = "tests",
plugins = [
":plugin_cpp.wasm",
],
tests = ":tests.textpb",
)
82 changes: 82 additions & 0 deletions plugins/samples/hmac_authtoken/plugin.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START serviceextensions_plugin_hmac_authtoken]
#include <openssl/hmac.h>

#include <boost/url/parse.hpp>
#include <boost/url/url.hpp>
#include <string>

#include "absl/strings/escaping.h"
#include "proxy_wasm_intrinsics.h"

// Replace with your desired secret key.
const std::string kSecretKey = "your_secret_key";

class MyHttpContext : public Context {
public:
explicit MyHttpContext(uint32_t id, RootContext* root) : Context(id, root) {}

FilterHeadersStatus onRequestHeaders(uint32_t headers,
bool end_of_stream) override {
boost::system::result<boost::urls::url> url =
boost::urls::parse_relative_ref(getRequestHeader(":path")->toString());

if (!url) {
LOG_ERROR("Error parsing the :path HTTP header: " +
url.error().message());
sendLocalResponse(400, "", "Error parsing the :path HTTP header.\n", {});
return FilterHeadersStatus::ContinueAndEndStream;
}

auto it = url->params().find("token");
// Check if the HMAC token exists.
if (it == url->params().end()) {
LOG_INFO("Access forbidden - missing token.");
sendLocalResponse(403, "", "Access forbidden - missing token.\n", {});
return FilterHeadersStatus::ContinueAndEndStream;
}

const std::string token = (*it).value;
// Strip the token from the URL.
url->params().erase(it);
const std::string path = url->buffer();
// Compare if the generated signature matches the token sent.
// In this sample the signature is generated using the request :path.
if (computeHmacSignature(path) != token) {
LOG_INFO("Access forbidden - invalid token.");
sendLocalResponse(403, "", "Access forbidden - invalid token.\n", {});
return FilterHeadersStatus::ContinueAndEndStream;
}

replaceRequestHeader(":path", path);
return FilterHeadersStatus::Continue;
}

private:
// Function to compute the HMAC signature.
std::string computeHmacSignature(std::string_view data) {
unsigned char result[EVP_MAX_MD_SIZE];
unsigned int len;
HMAC(EVP_sha256(), kSecretKey.c_str(), kSecretKey.length(),
reinterpret_cast<const unsigned char*>(std::string{data}.c_str()),
data.length(), result, &len);
return absl::BytesToHexString(std::string(result, result + len));
}
};

static RegisterContextFactory register_StaticContext(
CONTEXT_FACTORY(MyHttpContext), ROOT_FACTORY(RootContext));
// [END serviceextensions_plugin_hmac_authtoken]
53 changes: 53 additions & 0 deletions plugins/samples/hmac_authtoken/tests.textpb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# With a valid token, request allowed and token removed from :path.
test {
name: "WithValidHMACToken"
benchmark: false
request_headers {
input { header { key: ":path" value: "/somepage/otherpage?param1=value1&param2=value2&token=48277f04685e364e0e3f3c4bfa78cb91293d304bbf196829334cb1c4a741d6b0" } }
result {
has_header { key: ":path" value: "/somepage/otherpage?param1=value1&param2=value2" }
}
}
}
# No token set, forbidden request.
test {
name: "NoToken"
request_headers {
input {
header { key: ":path" value: "/admin" }
}
result {
immediate { http_status: 403 details: "" }
body { exact: "Access forbidden - missing token.\n" }
log { regex: ".+Access forbidden - missing token.$" }
}
}
}
# invalid token, forbidden request.
test {
name: "InvalidToken"
request_headers {
input {
header { key: ":path" value: "/admin?token=ddssdsdsddfdffddsssd" }
}
result {
immediate { http_status: 403 details: "" }
body { exact: "Access forbidden - invalid token.\n" }
log { regex: ".+Access forbidden - invalid token.$" }
}
}
}
# invalid :path header, return bad request
test {
name: "InvalidPathHeader"
request_headers {
input {
header { key: ":path" value: "foo:bar" }
}
result {
immediate { http_status: 400 details: "" }
body { exact: "Error parsing the :path HTTP header.\n" }
log { regex: ".*Error parsing the :path HTTP header: mismatch$" }
}
}
}

0 comments on commit 09369ff

Please sign in to comment.