Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ for your own plugin. Extend them to fit your particular use case.
* [Normalize a HTTP header on request](samples/normalize_header): Creates a new
HTTP header (client-device-type) to shard requests based on device according
to the existence of HTTP Client Hints or User-Agent header values.
* [Validate client token using HMAC with cookie](samples/hmac_authcookie): Check
the client request for a valid token signed using HMAC provided via a cookie.
* [Block request with particular header](samples/block_request): Check whether
the client's Referer header matches an expected domain. If not, generate a 403
Forbidden response.
Expand Down
27 changes: 27 additions & 0 deletions plugins/samples/hmac_authcookie/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 = [
"@proxy_wasm_cpp_sdk//contrib:contrib_lib",
"@com_google_absl//absl/strings",
"@boringssl//:ssl",
],
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",
)
92 changes: 92 additions & 0 deletions plugins/samples/hmac_authcookie/plugin.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2024 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_authcookie]
#include <openssl/hmac.h>

#include <iomanip>
#include <sstream>
#include <string>

#include "absl/strings/str_split.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 {
const auto token = getTokenFromCookie();
if (!token.has_value()) {
LOG_INFO("Access forbidden - missing HMAC cookie.");
sendLocalResponse(403, "", "Access forbidden - missing HMAC cookie.\n",
{});
return FilterHeadersStatus::ContinueAndEndStream;
}

const auto path = getRequestHeader(":path")->toString();
if (computeHmacSignature(path) != token.value()) {
LOG_INFO("Access forbidden - invalid HMAC cookie.");
sendLocalResponse(403, "", "Access forbidden - invalid HMAC cookie.\n",
{});
return FilterHeadersStatus::ContinueAndEndStream;
}

return FilterHeadersStatus::Continue;
}

private:
// Try to get the HMAC auth token from the Cookie header.
std::optional<std::string> getTokenFromCookie() {
const auto cookies = getRequestHeader("Cookie")->toString();
std::map<std::string, std::string> m;
for (absl::string_view sp : absl::StrSplit(cookies, "; ")) {
const std::pair<std::string, std::string> cookie =
absl::StrSplit(sp, absl::MaxSplits('=', 1));
if (cookie.first == "Authorization") {
return cookie.second;
}
}

return std::nullopt;
}

// Helper function to convert binary data to a hexadecimal string.
std::string toHexString(const unsigned char* data, size_t length) {
std::stringstream ss;
for (size_t i = 0; i < length; ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << (int)data[i];
}
return ss.str();
}

// Function to compute the HMAC signature.
std::string computeHmacSignature(const std::string& data) {
unsigned char* result;
unsigned int len = EVP_MAX_MD_SIZE;

result = HMAC(EVP_sha256(), kSecretKey.c_str(), kSecretKey.length(),
reinterpret_cast<const unsigned char*>(data.c_str()),
data.length(), nullptr, &len);
return toHexString(result, len);
}
};

static RegisterContextFactory register_StaticContext(
CONTEXT_FACTORY(MyHttpContext), ROOT_FACTORY(RootContext));
// [END serviceextensions_plugin_hmac_authcookie]
43 changes: 43 additions & 0 deletions plugins/samples/hmac_authcookie/tests.textpb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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" }
header { key: "Cookie" value: "SomeCookie=SomeValue; Authorization=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 HMAC cookie.\n" }
log { regex: ".+Access forbidden - missing HMAC cookie.$" }
}
}
}
# invalid token, forbidden request.
test {
name: "InvalidToken"
request_headers {
input {
header { key: ":path" value: "/somepage/otherpage?param1=value1" }
header { key: "Cookie" value: "SomeCookie=SomeValue; Authorization=48277f04685e364e0e3f3c4bfa78cb91293d304bbf196829334cb1c4a741d6b0" }
}
result {
immediate { http_status: 403 details: "" }
body { exact: "Access forbidden - invalid HMAC cookie.\n" }
log { regex: ".+Access forbidden - invalid HMAC cookie.$" }
}
}
}