From f9db0f07fb7f6d6ccee0fd9dbfe35213ca1fa7b1 Mon Sep 17 00:00:00 2001 From: victorwoli Date: Wed, 29 Oct 2025 13:58:14 -0300 Subject: [PATCH 1/6] Addign new Drafting Token with JWT plugin --- plugins/samples/drafting_jwt_token/BUILD | 35 ++ .../samples/drafting_jwt_token/config.data | 43 ++ plugins/samples/drafting_jwt_token/plugin.cc | 437 ++++++++++++++++++ .../samples/drafting_jwt_token/tests.textpb | 180 ++++++++ 4 files changed, 695 insertions(+) create mode 100644 plugins/samples/drafting_jwt_token/BUILD create mode 100644 plugins/samples/drafting_jwt_token/config.data create mode 100644 plugins/samples/drafting_jwt_token/plugin.cc create mode 100644 plugins/samples/drafting_jwt_token/tests.textpb diff --git a/plugins/samples/drafting_jwt_token/BUILD b/plugins/samples/drafting_jwt_token/BUILD new file mode 100644 index 000000000..1dffabfac --- /dev/null +++ b/plugins/samples/drafting_jwt_token/BUILD @@ -0,0 +1,35 @@ +load("//:plugins.bzl", "proxy_wasm_plugin_cpp", "proxy_wasm_tests") + +licenses(["notice"]) # Apache 2 + +proxy_wasm_plugin_cpp( + name = "plugin_cpp.wasm", + srcs = ["plugin.cc"], + deps = [ + "@jwt_verify_lib//:jwt_verify_lib", + "//:boost_exception", + "@boost//:url", + ], + linkopts = [ + # BoringSSL crypto assumes -pthread is OK to use: + # https://github.com/google/boringssl/blob/9ac494a171014fa0f06dbf2b0e08abf1d7ec85aa/BUILD.bazel#L87 + # + # Using -pthread results in linker errors like: + # "error: --shared-memory is disallowed by lto.tmp because it was not + # compiled with 'atomics' or 'bulk-memory' features." + # + # Override the -pthread option to avoid the error. + # Consider moving this to ProxyWasmCppSdk. + "-sUSE_PTHREADS=0", + ], +) + +proxy_wasm_tests( + name = "tests", + config = ":publickey.pem", + plugins = [ + ":plugin_cpp.wasm", + ":plugin_go.wasm", + ], + tests = ":tests.textpb", +) diff --git a/plugins/samples/drafting_jwt_token/config.data b/plugins/samples/drafting_jwt_token/config.data new file mode 100644 index 000000000..be877a91c --- /dev/null +++ b/plugins/samples/drafting_jwt_token/config.data @@ -0,0 +1,43 @@ +{ + "secret_key": "your-256-bit-secret-key-change-this-in-production", + "default_expiration_minutes": 60, + "data": { + "user-451": { + "plan": "pro", + "permissions": [ + "read:data", + "write:data", + "delete:data" + ], + "roles": ["developer", "admin"] + }, + "user-123": { + "plan": "free", + "permissions": [ + "read:data" + ], + "roles": ["viewer"] + }, + "user-789": { + "plan": "enterprise", + "permissions": [ + "read:data", + "write:data", + "delete:data", + "admin:users", + "admin:billing" + ], + "roles": ["super_admin"] + }, + "user-999": { + "plan": "pro", + "permissions": [ + "read:data", + "write:data", + "read:reports", + "export:data" + ], + "roles": ["analyst", "developer"] + } + } +} \ No newline at end of file diff --git a/plugins/samples/drafting_jwt_token/plugin.cc b/plugins/samples/drafting_jwt_token/plugin.cc new file mode 100644 index 000000000..0174ceae9 --- /dev/null +++ b/plugins/samples/drafting_jwt_token/plugin.cc @@ -0,0 +1,437 @@ +// 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_drafting_jwt_token] +#include +#include +#include +#include +#include +#include +#include +#include +#include "proxy_wasm_intrinsics.h" +#include "json.hpp" + +using json = nlohmann::json; + +// Base64 URL-safe encoding +std::string base64_url_encode(const unsigned char* buffer, size_t length) { + BIO *bio, *b64; + BUF_MEM *bufferPtr; + + b64 = BIO_new(BIO_f_base64()); + bio = BIO_new(BIO_s_mem()); + bio = BIO_push(b64, bio); + + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); + BIO_write(bio, buffer, length); + BIO_flush(bio); + BIO_get_mem_ptr(bio, &bufferPtr); + + std::string result(bufferPtr->data, bufferPtr->length); + BIO_free_all(bio); + + // Convert to URL-safe base64 + for (char& c : result) { + if (c == '+') c = '-'; + if (c == '/') c = '_'; + } + + // Remove padding + result.erase(std::remove(result.begin(), result.end(), '='), result.end()); + + return result; +} + +// Base64 URL-safe decoding +std::string base64_url_decode(const std::string& input) { + std::string base64 = input; + + // Convert from URL-safe to standard base64 + for (char& c : base64) { + if (c == '-') c = '+'; + if (c == '_') c = '/'; + } + + // Add padding if needed + while (base64.length() % 4) { + base64 += '='; + } + + BIO *bio, *b64; + int decodeLen = base64.length(); + unsigned char* buffer = new unsigned char[decodeLen]; + + bio = BIO_new_mem_buf(base64.c_str(), -1); + b64 = BIO_new(BIO_f_base64()); + bio = BIO_push(b64, bio); + + BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); + int length = BIO_read(bio, buffer, decodeLen); + BIO_free_all(bio); + + std::string result(reinterpret_cast(buffer), length); + delete[] buffer; + + return result; +} + +// HMAC SHA256 signing +std::string hmac_sha256(const std::string& key, const std::string& data) { + unsigned char* digest; + unsigned int digest_len; + + digest = HMAC(EVP_sha256(), + key.c_str(), key.length(), + reinterpret_cast(data.c_str()), data.length(), + nullptr, &digest_len); + + return std::string(reinterpret_cast(digest), digest_len); +} + +class JWTPluginContext : public Context { +public: + explicit JWTPluginContext(uint32_t id, RootContext* root) : Context(id, root) {} + + FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override; + +private: + std::string generateJWT(const std::string& user_id, int expiration_minutes); + std::string verifyJWT(const std::string& token); + json getUserEntitlements(const std::string& user_id); +}; + +class JWTPluginRootContext : public RootContext { +public: + explicit JWTPluginRootContext(uint32_t id, std::string_view root_id) + : RootContext(id, root_id) {} + + bool onConfigure(size_t config_size) override; + bool onStart(size_t vm_configuration_size) override; + + std::string secret_key_; + std::map kv_store_; + int default_expiration_ = 60; // minutes +}; + +bool JWTPluginRootContext::onConfigure(size_t config_size) { + auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration, 0, config_size); + + try { + auto config = json::parse(configuration_data->view()); + + // Load secret key + if (config.contains("secret_key")) { + secret_key_ = config["secret_key"].get(); + } else { + LOG_WARN("No secret_key configured, using default (INSECURE)"); + secret_key_ = "default_secret_key_change_me"; + } + + // Load default expiration + if (config.contains("default_expiration_minutes")) { + default_expiration_ = config["default_expiration_minutes"].get(); + } + + // Load KV Store data + if (config.contains("data")) { + kv_store_ = config["data"].get>(); + LOG_INFO("Loaded " + std::to_string(kv_store_.size()) + " entries into KV store"); + } + + LOG_INFO("JWT Plugin configured successfully"); + return true; + + } catch (const std::exception& e) { + LOG_ERROR("Configuration error: " + std::string(e.what())); + return false; + } +} + +bool JWTPluginRootContext::onStart(size_t vm_configuration_size) { + LOG_INFO("JWT Plugin started"); + return true; +} + +json JWTPluginContext::getUserEntitlements(const std::string& user_id) { + auto* root = dynamic_cast(this->root()); + + if (root->kv_store_.find(user_id) != root->kv_store_.end()) { + return root->kv_store_[user_id]; + } + + // Return default/free tier if user not found + json default_entitlements = { + {"plan", "free"}, + {"permissions", json::array()} + }; + + return default_entitlements; +} + +std::string JWTPluginContext::generateJWT(const std::string& user_id, int expiration_minutes) { + auto* root = dynamic_cast(this->root()); + + // Get current time + auto now = std::chrono::system_clock::now(); + auto now_seconds = std::chrono::duration_cast(now.time_since_epoch()).count(); + + // Calculate expiration and not-before times + long exp = now_seconds + (expiration_minutes * 60); + long nbf = now_seconds; + + // Create JWT header + json header = { + {"alg", "HS256"}, + {"typ", "JWT"} + }; + + // Get user entitlements from KV store + json entitlements = getUserEntitlements(user_id); + + // Create JWT payload with registered and public claims + json payload = { + {"sub", user_id}, + {"exp", exp}, + {"nbf", nbf}, + {"iat", now_seconds} + }; + + // Merge user entitlements (public claims) + if (entitlements.contains("plan")) { + payload["plan"] = entitlements["plan"]; + } + if (entitlements.contains("permissions")) { + payload["permissions"] = entitlements["permissions"]; + } + if (entitlements.contains("roles")) { + payload["roles"] = entitlements["roles"]; + } + + // Encode header and payload + std::string header_str = header.dump(); + std::string payload_str = payload.dump(); + + std::string header_encoded = base64_url_encode( + reinterpret_cast(header_str.c_str()), + header_str.length() + ); + + std::string payload_encoded = base64_url_encode( + reinterpret_cast(payload_str.c_str()), + payload_str.length() + ); + + // Create signature + std::string signing_input = header_encoded + "." + payload_encoded; + std::string signature = hmac_sha256(root->secret_key_, signing_input); + std::string signature_encoded = base64_url_encode( + reinterpret_cast(signature.c_str()), + signature.length() + ); + + // Assemble final JWT + return signing_input + "." + signature_encoded; +} + +std::string JWTPluginContext::verifyJWT(const std::string& token) { + auto* root = dynamic_cast(this->root()); + + // Split token into parts + std::vector parts; + std::stringstream ss(token); + std::string part; + + while (std::getline(ss, part, '.')) { + parts.push_back(part); + } + + if (parts.size() != 3) { + return "Invalid token format"; + } + + // Verify signature + std::string signing_input = parts[0] + "." + parts[1]; + std::string expected_signature = hmac_sha256(root->secret_key_, signing_input); + std::string expected_signature_encoded = base64_url_encode( + reinterpret_cast(expected_signature.c_str()), + expected_signature.length() + ); + + if (parts[2] != expected_signature_encoded) { + return "Invalid signature"; + } + + // Decode and verify payload + try { + std::string payload_str = base64_url_decode(parts[1]); + json payload = json::parse(payload_str); + + // Verify expiration + auto now = std::chrono::system_clock::now(); + auto now_seconds = std::chrono::duration_cast( + now.time_since_epoch() + ).count(); + + if (payload.contains("exp") && payload["exp"].get() < now_seconds) { + return "Token expired"; + } + + // Verify not-before + if (payload.contains("nbf") && payload["nbf"].get() > now_seconds) { + return "Token not yet valid"; + } + + return "valid"; + + } catch (const std::exception& e) { + return "Token verification failed: " + std::string(e.what()); + } +} + +FilterHeadersStatus JWTPluginContext::onRequestHeaders(uint32_t headers, bool end_of_stream) { + auto* root = dynamic_cast(this->root()); + auto path = getRequestHeader(":path"); + auto method = getRequestHeader(":method"); + + // Token generation endpoint + if (path->view() == "/generate-token" && method->view() == "POST") { + // Get user_id from header or body + auto user_id_header = getRequestHeader("x-user-id"); + + if (!user_id_header || user_id_header->view().empty()) { + sendLocalResponse(400, "", "Missing x-user-id header", {}); + return FilterHeadersStatus::StopIteration; + } + + std::string user_id(user_id_header->view()); + + // Get optional expiration override + int expiration = root->default_expiration_; + auto exp_header = getRequestHeader("x-expiration-minutes"); + if (exp_header) { + try { + expiration = std::stoi(std::string(exp_header->view())); + } catch (...) { + // Use default if invalid + } + } + + // Generate JWT + std::string jwt = generateJWT(user_id, expiration); + + // Return JWT in response + json response = { + {"token", jwt}, + {"expires_in", expiration * 60}, + {"token_type", "Bearer"} + }; + + sendLocalResponse(200, "", response.dump(), { + {"content-type", "application/json"} + }); + + return FilterHeadersStatus::StopIteration; + } + + // Token verification endpoint + if (path->view() == "/verify-token" && method->view() == "GET") { + auto auth_header = getRequestHeader("authorization"); + + if (!auth_header || auth_header->view().empty()) { + sendLocalResponse(401, "", "Missing Authorization header", {}); + return FilterHeadersStatus::StopIteration; + } + + std::string auth(auth_header->view()); + + // Extract Bearer token + if (auth.substr(0, 7) != "Bearer ") { + sendLocalResponse(401, "", "Invalid Authorization format", {}); + return FilterHeadersStatus::StopIteration; + } + + std::string token = auth.substr(7); + std::string verification_result = verifyJWT(token); + + if (verification_result == "valid") { + json response = { + {"valid", true}, + {"message", "Token is valid"} + }; + sendLocalResponse(200, "", response.dump(), { + {"content-type", "application/json"} + }); + } else { + json response = { + {"valid", false}, + {"message", verification_result} + }; + sendLocalResponse(401, "", response.dump(), { + {"content-type", "application/json"} + }); + } + + return FilterHeadersStatus::StopIteration; + } + + // For other requests, validate token if present + auto auth_header = getRequestHeader("authorization"); + if (auth_header && !auth_header->view().empty()) { + std::string auth(auth_header->view()); + + if (auth.substr(0, 7) == "Bearer ") { + std::string token = auth.substr(7); + std::string verification_result = verifyJWT(token); + + if (verification_result != "valid") { + sendLocalResponse(401, "", "Unauthorized: " + verification_result, {}); + return FilterHeadersStatus::StopIteration; + } + + // Token is valid, add user info to headers for downstream services + try { + std::vector parts; + std::stringstream ss(token); + std::string part; + + while (std::getline(ss, part, '.')) { + parts.push_back(part); + } + + if (parts.size() == 3) { + std::string payload_str = base64_url_decode(parts[1]); + json payload = json::parse(payload_str); + + if (payload.contains("sub")) { + addRequestHeader("x-jwt-user", payload["sub"].get()); + } + if (payload.contains("plan")) { + addRequestHeader("x-jwt-plan", payload["plan"].get()); + } + } + } catch (...) { + // Continue even if header extraction fails + } + } + } + + return FilterHeadersStatus::Continue; +} + +static RegisterContextFactory register_JWTPlugin( + CONTEXT_FACTORY(JWTPluginContext), + ROOT_FACTORY(JWTPluginRootContext) +); \ No newline at end of file diff --git a/plugins/samples/drafting_jwt_token/tests.textpb b/plugins/samples/drafting_jwt_token/tests.textpb new file mode 100644 index 000000000..39660c5ee --- /dev/null +++ b/plugins/samples/drafting_jwt_token/tests.textpb @@ -0,0 +1,180 @@ +# Generate JWT Token for Pro User +test { + name: "generate_jwt_token_pro_user" + description: "Generate a valid JWT token for a pro-tier user with permissions lookup from KV store" + + config { + vm_config { + code { + local { + filename: "jwt_plugin.wasm" + } + } + runtime: "envoy.wasm.runtime.v8" + vm_id: "jwt_test_vm" + configuration { + value: '{ + "secret_key": "test-secret-key-for-jwt-signing-256bit", + "default_expiration_minutes": 60, + "data": { + "user-451": { + "plan": "pro", + "permissions": ["read:data", "write:data", "delete:data"], + "roles": ["developer", "admin"] + }, + "user-123": { + "plan": "free", + "permissions": ["read:data"], + "roles": ["viewer"] + } + } + }' + } + } + } + + request { + headers { + key: ":method" + value: "POST" + } + headers { + key: ":path" + value: "/generate-token" + } + headers { + key: ":authority" + value: "api.example.com" + } + headers { + key: "x-user-id" + value: "user-451" + } + headers { + key: "x-expiration-minutes" + value: "120" + } + } + + expected_response { + status: 200 + headers { + key: "content-type" + value: "application/json" + } + body_contains: "token" + body_contains: "expires_in" + body_contains: "Bearer" + body_json_path { + path: "$.expires_in" + value: "7200" + } + body_json_path { + path: "$.token_type" + value: "Bearer" + } + } +} + +# Generate JWT Token with Default Expiration +test { + name: "generate_jwt_token_default_expiration" + description: "Generate JWT token using default expiration time from config" + + config { + vm_config { + code { + local { + filename: "jwt_plugin.wasm" + } + } + runtime: "envoy.wasm.runtime.v8" + vm_id: "jwt_test_vm" + configuration { + value: '{ + "secret_key": "test-secret-key-for-jwt-signing-256bit", + "default_expiration_minutes": 60, + "data": { + "user-789": { + "plan": "enterprise", + "permissions": ["read:data", "write:data", "delete:data", "admin:users"], + "roles": ["super_admin"] + } + } + }' + } + } + } + + request { + headers { + key: ":method" + value: "POST" + } + headers { + key: ":path" + value: "/generate-token" + } + headers { + key: ":authority" + value: "api.example.com" + } + headers { + key: "x-user-id" + value: "user-789" + } + } + + expected_response { + status: 200 + body_json_path { + path: "$.expires_in" + value: "3600" + } + } +} + +# Generate Token - Missing User ID +test { + name: "generate_token_missing_user_id" + description: "Attempt to generate token without providing x-user-id header" + + config { + vm_config { + code { + local { + filename: "jwt_plugin.wasm" + } + } + runtime: "envoy.wasm.runtime.v8" + vm_id: "jwt_test_vm" + configuration { + value: '{ + "secret_key": "test-secret-key-for-jwt-signing-256bit", + "default_expiration_minutes": 60, + "data": {} + }' + } + } + } + + request { + headers { + key: ":method" + value: "POST" + } + headers { + key: ":path" + value: "/generate-token" + } + headers { + key: ":authority" + value: "api.example.com" + } + } + + expected_response { + status: 400 + body_contains: "Missing x-user-id header" + } +} \ No newline at end of file From 48b1d1fc6a873f324e76b0bdbb1e0e00fe488a1f Mon Sep 17 00:00:00 2001 From: victorwoli Date: Wed, 29 Oct 2025 14:11:50 -0300 Subject: [PATCH 2/6] Adjusting commits convetion errors --- plugins/samples/drafting_jwt_token/config.data | 2 +- plugins/samples/drafting_jwt_token/plugin.cc | 2 +- plugins/samples/drafting_jwt_token/tests.textpb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/samples/drafting_jwt_token/config.data b/plugins/samples/drafting_jwt_token/config.data index be877a91c..d8fb9dd62 100644 --- a/plugins/samples/drafting_jwt_token/config.data +++ b/plugins/samples/drafting_jwt_token/config.data @@ -40,4 +40,4 @@ "roles": ["analyst", "developer"] } } -} \ No newline at end of file +} diff --git a/plugins/samples/drafting_jwt_token/plugin.cc b/plugins/samples/drafting_jwt_token/plugin.cc index 0174ceae9..942799ddf 100644 --- a/plugins/samples/drafting_jwt_token/plugin.cc +++ b/plugins/samples/drafting_jwt_token/plugin.cc @@ -434,4 +434,4 @@ FilterHeadersStatus JWTPluginContext::onRequestHeaders(uint32_t headers, bool en static RegisterContextFactory register_JWTPlugin( CONTEXT_FACTORY(JWTPluginContext), ROOT_FACTORY(JWTPluginRootContext) -); \ No newline at end of file +); diff --git a/plugins/samples/drafting_jwt_token/tests.textpb b/plugins/samples/drafting_jwt_token/tests.textpb index 39660c5ee..c172ac971 100644 --- a/plugins/samples/drafting_jwt_token/tests.textpb +++ b/plugins/samples/drafting_jwt_token/tests.textpb @@ -177,4 +177,4 @@ test { status: 400 body_contains: "Missing x-user-id header" } -} \ No newline at end of file +} From de8403345ad6d656f6c92289380844d45037276e Mon Sep 17 00:00:00 2001 From: victorwoli Date: Wed, 29 Oct 2025 14:14:24 -0300 Subject: [PATCH 3/6] Including the end tag for the new plugin --- plugins/samples/drafting_jwt_token/plugin.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/samples/drafting_jwt_token/plugin.cc b/plugins/samples/drafting_jwt_token/plugin.cc index 942799ddf..9f4d41fd4 100644 --- a/plugins/samples/drafting_jwt_token/plugin.cc +++ b/plugins/samples/drafting_jwt_token/plugin.cc @@ -435,3 +435,4 @@ static RegisterContextFactory register_JWTPlugin( CONTEXT_FACTORY(JWTPluginContext), ROOT_FACTORY(JWTPluginRootContext) ); +// [END serviceextensions_plugin_drafting_jwt_token] From ffdbc04cc9e8ef666c60ec3f29cf7c5220fb317d Mon Sep 17 00:00:00 2001 From: victorwoli Date: Mon, 10 Nov 2025 11:15:15 -0300 Subject: [PATCH 4/6] Adding plugin GO version --- plugins/samples/drafting_jwt_token/BUILD | 10 +- plugins/samples/drafting_jwt_token/plugin.go | 447 +++++++++++++++++++ 2 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 plugins/samples/drafting_jwt_token/plugin.go diff --git a/plugins/samples/drafting_jwt_token/BUILD b/plugins/samples/drafting_jwt_token/BUILD index 1dffabfac..5c817afb6 100644 --- a/plugins/samples/drafting_jwt_token/BUILD +++ b/plugins/samples/drafting_jwt_token/BUILD @@ -1,4 +1,4 @@ -load("//:plugins.bzl", "proxy_wasm_plugin_cpp", "proxy_wasm_tests") +load("//:plugins.bzl", "proxy_wasm_plugin_cpp", "proxy_wasm_plugin_go", "proxy_wasm_tests") licenses(["notice"]) # Apache 2 @@ -24,6 +24,14 @@ proxy_wasm_plugin_cpp( ], ) +proxy_wasm_plugin_go( + name = "plugin_go.wasm", + srcs = ["plugin.go"], + deps = [ + "@com_github_golang_jwt_jwt_v5//:jwt", + ], +) + proxy_wasm_tests( name = "tests", config = ":publickey.pem", diff --git a/plugins/samples/drafting_jwt_token/plugin.go b/plugins/samples/drafting_jwt_token/plugin.go new file mode 100644 index 000000000..416f095b4 --- /dev/null +++ b/plugins/samples/drafting_jwt_token/plugin.go @@ -0,0 +1,447 @@ +// 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_drafting_jwt_token] +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" +) + +func main() { + proxywasm.SetVMContext(&vmContext{}) +} + +type vmContext struct { + types.DefaultVMContext +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +type pluginContext struct { + types.DefaultPluginContext + secretKey string + defaultExpiration int + kvStore map[string]*UserEntitlements +} + +// UserEntitlements represents user permissions and roles from KV store +type UserEntitlements struct { + Plan string `json:"plan"` + Permissions []string `json:"permissions"` + Roles []string `json:"roles,omitempty"` +} + +// Config represents the plugin configuration +type Config struct { + SecretKey string `json:"secret_key"` + DefaultExpirationMinutes int `json:"default_expiration_minutes"` + Data map[string]*UserEntitlements `json:"data"` +} + +// JWTHeader represents the JWT header structure +type JWTHeader struct { + Alg string `json:"alg"` + Typ string `json:"typ"` +} + +// JWTPayload represents the JWT payload with registered and public claims +type JWTPayload struct { + Sub string `json:"sub"` + Exp int64 `json:"exp"` + Nbf int64 `json:"nbf"` + Iat int64 `json:"iat"` + Plan string `json:"plan,omitempty"` + Permissions []string `json:"permissions,omitempty"` + Roles []string `json:"roles,omitempty"` +} + +// TokenResponse represents the token generation response +type TokenResponse struct { + Token string `json:"token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` +} + +// VerifyResponse represents the token verification response +type VerifyResponse struct { + Valid bool `json:"valid"` + Message string `json:"message"` +} + +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + // Load plugin configuration + configData, err := proxywasm.GetPluginConfiguration() + if err != nil { + proxywasm.LogErrorf("failed to get plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + var config Config + if err := json.Unmarshal(configData, &config); err != nil { + proxywasm.LogErrorf("failed to parse configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + // Set default values + ctx.secretKey = config.SecretKey + if ctx.secretKey == "" { + proxywasm.LogWarn("no secret_key configured, using default (INSECURE)") + ctx.secretKey = "default_secret_key_change_me" + } + + ctx.defaultExpiration = config.DefaultExpirationMinutes + if ctx.defaultExpiration == 0 { + ctx.defaultExpiration = 60 + } + + // Load KV store data + ctx.kvStore = config.Data + if ctx.kvStore == nil { + ctx.kvStore = make(map[string]*UserEntitlements) + } + + proxywasm.LogInfof("JWT Plugin configured successfully with %d KV store entries", len(ctx.kvStore)) + return types.OnPluginStartStatusOK +} + +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpContext{ + contextID: contextID, + plugin: ctx, + } +} + +type httpContext struct { + types.DefaultHttpContext + contextID uint32 + plugin *pluginContext +} + +func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + path, err := proxywasm.GetHttpRequestHeader(":path") + if err != nil { + proxywasm.LogErrorf("failed to get :path header: %v", err) + return types.ActionContinue + } + + method, err := proxywasm.GetHttpRequestHeader(":method") + if err != nil { + proxywasm.LogErrorf("failed to get :method header: %v", err) + return types.ActionContinue + } + + // Handle token generation endpoint + if path == "/generate-token" && method == "POST" { + return ctx.handleGenerateToken() + } + + // Handle token verification endpoint + if path == "/verify-token" && method == "GET" { + return ctx.handleVerifyToken() + } + + // For other requests, validate token if present + return ctx.handleProtectedEndpoint() +} + +func (ctx *httpContext) handleGenerateToken() types.Action { + // Get user_id from header + userID, err := proxywasm.GetHttpRequestHeader("x-user-id") + if err != nil || userID == "" { + ctx.sendJSONResponse(400, map[string]string{ + "error": "Missing x-user-id header", + }) + return types.ActionPause + } + + // Get optional expiration override + expirationMinutes := ctx.plugin.defaultExpiration + if expHeader, err := proxywasm.GetHttpRequestHeader("x-expiration-minutes"); err == nil && expHeader != "" { + var exp int + if _, err := fmt.Sscanf(expHeader, "%d", &exp); err == nil { + expirationMinutes = exp + } + } + + // Generate JWT + token, err := ctx.generateJWT(userID, expirationMinutes) + if err != nil { + proxywasm.LogErrorf("failed to generate JWT: %v", err) + ctx.sendJSONResponse(500, map[string]string{ + "error": "Failed to generate token", + }) + return types.ActionPause + } + + // Send response + response := TokenResponse{ + Token: token, + ExpiresIn: expirationMinutes * 60, + TokenType: "Bearer", + } + + ctx.sendJSONResponse(200, response) + return types.ActionPause +} + +func (ctx *httpContext) handleVerifyToken() types.Action { + // Get Authorization header + authHeader, err := proxywasm.GetHttpRequestHeader("authorization") + if err != nil || authHeader == "" { + ctx.sendJSONResponse(401, VerifyResponse{ + Valid: false, + Message: "Missing Authorization header", + }) + return types.ActionPause + } + + // Extract Bearer token + if !strings.HasPrefix(authHeader, "Bearer ") { + ctx.sendJSONResponse(401, VerifyResponse{ + Valid: false, + Message: "Invalid Authorization format", + }) + return types.ActionPause + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + // Verify token + if err := ctx.verifyJWT(token); err != nil { + ctx.sendJSONResponse(401, VerifyResponse{ + Valid: false, + Message: err.Error(), + }) + return types.ActionPause + } + + ctx.sendJSONResponse(200, VerifyResponse{ + Valid: true, + Message: "Token is valid", + }) + return types.ActionPause +} + +func (ctx *httpContext) handleProtectedEndpoint() types.Action { + // Get Authorization header + authHeader, err := proxywasm.GetHttpRequestHeader("authorization") + if err != nil || authHeader == "" { + // No token present, allow request to continue + return types.ActionContinue + } + + // Check if it's a Bearer token + if !strings.HasPrefix(authHeader, "Bearer ") { + return types.ActionContinue + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + // Verify token + if err := ctx.verifyJWT(token); err != nil { + ctx.sendTextResponse(401, fmt.Sprintf("Unauthorized: %s", err.Error())) + return types.ActionPause + } + + // Extract payload and add headers for downstream services + parts := strings.Split(token, ".") + if len(parts) == 3 { + if payload, err := ctx.decodePayload(parts[1]); err == nil { + if payload.Sub != "" { + proxywasm.AddHttpRequestHeader("x-jwt-user", payload.Sub) + } + if payload.Plan != "" { + proxywasm.AddHttpRequestHeader("x-jwt-plan", payload.Plan) + } + } + } + + return types.ActionContinue +} + +func (ctx *httpContext) generateJWT(userID string, expirationMinutes int) (string, error) { + now := time.Now().Unix() + exp := now + int64(expirationMinutes*60) + + // Create JWT header + header := JWTHeader{ + Alg: "HS256", + Typ: "JWT", + } + + // Get user entitlements from KV store + entitlements := ctx.getUserEntitlements(userID) + + // Create JWT payload with registered and public claims + payload := JWTPayload{ + Sub: userID, + Exp: exp, + Nbf: now, + Iat: now, + Plan: entitlements.Plan, + Permissions: entitlements.Permissions, + Roles: entitlements.Roles, + } + + // Encode header + headerJSON, err := json.Marshal(header) + if err != nil { + return "", fmt.Errorf("failed to marshal header: %w", err) + } + headerEncoded := base64URLEncode(headerJSON) + + // Encode payload + payloadJSON, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("failed to marshal payload: %w", err) + } + payloadEncoded := base64URLEncode(payloadJSON) + + // Create signature + signingInput := headerEncoded + "." + payloadEncoded + signature := ctx.hmacSHA256(ctx.plugin.secretKey, signingInput) + signatureEncoded := base64URLEncode(signature) + + // Assemble final JWT + return signingInput + "." + signatureEncoded, nil +} + +func (ctx *httpContext) verifyJWT(token string) error { + // Split token into parts + parts := strings.Split(token, ".") + if len(parts) != 3 { + return fmt.Errorf("invalid token format") + } + + // Verify signature + signingInput := parts[0] + "." + parts[1] + expectedSignature := ctx.hmacSHA256(ctx.plugin.secretKey, signingInput) + expectedSignatureEncoded := base64URLEncode(expectedSignature) + + if parts[2] != expectedSignatureEncoded { + return fmt.Errorf("invalid signature") + } + + // Decode and verify payload + payload, err := ctx.decodePayload(parts[1]) + if err != nil { + return fmt.Errorf("token verification failed: %w", err) + } + + // Verify expiration + now := time.Now().Unix() + if payload.Exp < now { + return fmt.Errorf("token expired") + } + + // Verify not-before + if payload.Nbf > now { + return fmt.Errorf("token not yet valid") + } + + return nil +} + +func (ctx *httpContext) decodePayload(encodedPayload string) (*JWTPayload, error) { + payloadJSON, err := base64URLDecode(encodedPayload) + if err != nil { + return nil, fmt.Errorf("failed to decode payload: %w", err) + } + + var payload JWTPayload + if err := json.Unmarshal(payloadJSON, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal payload: %w", err) + } + + return &payload, nil +} + +func (ctx *httpContext) getUserEntitlements(userID string) *UserEntitlements { + if entitlements, ok := ctx.plugin.kvStore[userID]; ok { + return entitlements + } + + // Return default/free tier if user not found + return &UserEntitlements{ + Plan: "free", + Permissions: []string{}, + } +} + +func (ctx *httpContext) hmacSHA256(key, data string) []byte { + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(data)) + return h.Sum(nil) +} + +func (ctx *httpContext) sendJSONResponse(statusCode uint32, data interface{}) { + body, err := json.Marshal(data) + if err != nil { + proxywasm.LogErrorf("failed to marshal response: %v", err) + body = []byte(`{"error":"internal server error"}`) + statusCode = 500 + } + + if err := proxywasm.SendHttpResponse(statusCode, [][2]string{ + {"content-type", "application/json"}, + }, body, -1); err != nil { + proxywasm.LogErrorf("failed to send response: %v", err) + } +} + +func (ctx *httpContext) sendTextResponse(statusCode uint32, message string) { + if err := proxywasm.SendHttpResponse(statusCode, [][2]string{ + {"content-type", "text/plain"}, + }, []byte(message), -1); err != nil { + proxywasm.LogErrorf("failed to send response: %v", err) + } +} + +// base64URLEncode encodes bytes to base64 URL-safe format +func base64URLEncode(data []byte) string { + encoded := base64.StdEncoding.EncodeToString(data) + encoded = strings.ReplaceAll(encoded, "+", "-") + encoded = strings.ReplaceAll(encoded, "/", "_") + encoded = strings.TrimRight(encoded, "=") + return encoded +} + +// base64URLDecode decodes base64 URL-safe format to bytes +func base64URLDecode(data string) ([]byte, error) { + data = strings.ReplaceAll(data, "-", "+") + data = strings.ReplaceAll(data, "_", "/") + + // Add padding if needed + switch len(data) % 4 { + case 2: + data += "==" + case 3: + data += "=" + } + + return base64.StdEncoding.DecodeString(data) +} +// [END serviceextensions_plugin_drafting_jwt_token] From 09949924c3928f1279d27a14b79834c47f697b64 Mon Sep 17 00:00:00 2001 From: victorwoli Date: Wed, 19 Nov 2025 15:48:38 -0300 Subject: [PATCH 5/6] Adding the GO version for the Drafting JWT Token plugin --- plugins/samples/drafting_jwt_token/BUILD | 24 +- plugins/samples/drafting_jwt_token/plugin.cc | 438 ------------------- 2 files changed, 1 insertion(+), 461 deletions(-) delete mode 100644 plugins/samples/drafting_jwt_token/plugin.cc diff --git a/plugins/samples/drafting_jwt_token/BUILD b/plugins/samples/drafting_jwt_token/BUILD index 5c817afb6..792151ae2 100644 --- a/plugins/samples/drafting_jwt_token/BUILD +++ b/plugins/samples/drafting_jwt_token/BUILD @@ -1,29 +1,7 @@ -load("//:plugins.bzl", "proxy_wasm_plugin_cpp", "proxy_wasm_plugin_go", "proxy_wasm_tests") +load("//:plugins.bzl", "proxy_wasm_plugin_go", "proxy_wasm_tests") licenses(["notice"]) # Apache 2 -proxy_wasm_plugin_cpp( - name = "plugin_cpp.wasm", - srcs = ["plugin.cc"], - deps = [ - "@jwt_verify_lib//:jwt_verify_lib", - "//:boost_exception", - "@boost//:url", - ], - linkopts = [ - # BoringSSL crypto assumes -pthread is OK to use: - # https://github.com/google/boringssl/blob/9ac494a171014fa0f06dbf2b0e08abf1d7ec85aa/BUILD.bazel#L87 - # - # Using -pthread results in linker errors like: - # "error: --shared-memory is disallowed by lto.tmp because it was not - # compiled with 'atomics' or 'bulk-memory' features." - # - # Override the -pthread option to avoid the error. - # Consider moving this to ProxyWasmCppSdk. - "-sUSE_PTHREADS=0", - ], -) - proxy_wasm_plugin_go( name = "plugin_go.wasm", srcs = ["plugin.go"], diff --git a/plugins/samples/drafting_jwt_token/plugin.cc b/plugins/samples/drafting_jwt_token/plugin.cc deleted file mode 100644 index 9f4d41fd4..000000000 --- a/plugins/samples/drafting_jwt_token/plugin.cc +++ /dev/null @@ -1,438 +0,0 @@ -// 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_drafting_jwt_token] -#include -#include -#include -#include -#include -#include -#include -#include -#include "proxy_wasm_intrinsics.h" -#include "json.hpp" - -using json = nlohmann::json; - -// Base64 URL-safe encoding -std::string base64_url_encode(const unsigned char* buffer, size_t length) { - BIO *bio, *b64; - BUF_MEM *bufferPtr; - - b64 = BIO_new(BIO_f_base64()); - bio = BIO_new(BIO_s_mem()); - bio = BIO_push(b64, bio); - - BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); - BIO_write(bio, buffer, length); - BIO_flush(bio); - BIO_get_mem_ptr(bio, &bufferPtr); - - std::string result(bufferPtr->data, bufferPtr->length); - BIO_free_all(bio); - - // Convert to URL-safe base64 - for (char& c : result) { - if (c == '+') c = '-'; - if (c == '/') c = '_'; - } - - // Remove padding - result.erase(std::remove(result.begin(), result.end(), '='), result.end()); - - return result; -} - -// Base64 URL-safe decoding -std::string base64_url_decode(const std::string& input) { - std::string base64 = input; - - // Convert from URL-safe to standard base64 - for (char& c : base64) { - if (c == '-') c = '+'; - if (c == '_') c = '/'; - } - - // Add padding if needed - while (base64.length() % 4) { - base64 += '='; - } - - BIO *bio, *b64; - int decodeLen = base64.length(); - unsigned char* buffer = new unsigned char[decodeLen]; - - bio = BIO_new_mem_buf(base64.c_str(), -1); - b64 = BIO_new(BIO_f_base64()); - bio = BIO_push(b64, bio); - - BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); - int length = BIO_read(bio, buffer, decodeLen); - BIO_free_all(bio); - - std::string result(reinterpret_cast(buffer), length); - delete[] buffer; - - return result; -} - -// HMAC SHA256 signing -std::string hmac_sha256(const std::string& key, const std::string& data) { - unsigned char* digest; - unsigned int digest_len; - - digest = HMAC(EVP_sha256(), - key.c_str(), key.length(), - reinterpret_cast(data.c_str()), data.length(), - nullptr, &digest_len); - - return std::string(reinterpret_cast(digest), digest_len); -} - -class JWTPluginContext : public Context { -public: - explicit JWTPluginContext(uint32_t id, RootContext* root) : Context(id, root) {} - - FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override; - -private: - std::string generateJWT(const std::string& user_id, int expiration_minutes); - std::string verifyJWT(const std::string& token); - json getUserEntitlements(const std::string& user_id); -}; - -class JWTPluginRootContext : public RootContext { -public: - explicit JWTPluginRootContext(uint32_t id, std::string_view root_id) - : RootContext(id, root_id) {} - - bool onConfigure(size_t config_size) override; - bool onStart(size_t vm_configuration_size) override; - - std::string secret_key_; - std::map kv_store_; - int default_expiration_ = 60; // minutes -}; - -bool JWTPluginRootContext::onConfigure(size_t config_size) { - auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration, 0, config_size); - - try { - auto config = json::parse(configuration_data->view()); - - // Load secret key - if (config.contains("secret_key")) { - secret_key_ = config["secret_key"].get(); - } else { - LOG_WARN("No secret_key configured, using default (INSECURE)"); - secret_key_ = "default_secret_key_change_me"; - } - - // Load default expiration - if (config.contains("default_expiration_minutes")) { - default_expiration_ = config["default_expiration_minutes"].get(); - } - - // Load KV Store data - if (config.contains("data")) { - kv_store_ = config["data"].get>(); - LOG_INFO("Loaded " + std::to_string(kv_store_.size()) + " entries into KV store"); - } - - LOG_INFO("JWT Plugin configured successfully"); - return true; - - } catch (const std::exception& e) { - LOG_ERROR("Configuration error: " + std::string(e.what())); - return false; - } -} - -bool JWTPluginRootContext::onStart(size_t vm_configuration_size) { - LOG_INFO("JWT Plugin started"); - return true; -} - -json JWTPluginContext::getUserEntitlements(const std::string& user_id) { - auto* root = dynamic_cast(this->root()); - - if (root->kv_store_.find(user_id) != root->kv_store_.end()) { - return root->kv_store_[user_id]; - } - - // Return default/free tier if user not found - json default_entitlements = { - {"plan", "free"}, - {"permissions", json::array()} - }; - - return default_entitlements; -} - -std::string JWTPluginContext::generateJWT(const std::string& user_id, int expiration_minutes) { - auto* root = dynamic_cast(this->root()); - - // Get current time - auto now = std::chrono::system_clock::now(); - auto now_seconds = std::chrono::duration_cast(now.time_since_epoch()).count(); - - // Calculate expiration and not-before times - long exp = now_seconds + (expiration_minutes * 60); - long nbf = now_seconds; - - // Create JWT header - json header = { - {"alg", "HS256"}, - {"typ", "JWT"} - }; - - // Get user entitlements from KV store - json entitlements = getUserEntitlements(user_id); - - // Create JWT payload with registered and public claims - json payload = { - {"sub", user_id}, - {"exp", exp}, - {"nbf", nbf}, - {"iat", now_seconds} - }; - - // Merge user entitlements (public claims) - if (entitlements.contains("plan")) { - payload["plan"] = entitlements["plan"]; - } - if (entitlements.contains("permissions")) { - payload["permissions"] = entitlements["permissions"]; - } - if (entitlements.contains("roles")) { - payload["roles"] = entitlements["roles"]; - } - - // Encode header and payload - std::string header_str = header.dump(); - std::string payload_str = payload.dump(); - - std::string header_encoded = base64_url_encode( - reinterpret_cast(header_str.c_str()), - header_str.length() - ); - - std::string payload_encoded = base64_url_encode( - reinterpret_cast(payload_str.c_str()), - payload_str.length() - ); - - // Create signature - std::string signing_input = header_encoded + "." + payload_encoded; - std::string signature = hmac_sha256(root->secret_key_, signing_input); - std::string signature_encoded = base64_url_encode( - reinterpret_cast(signature.c_str()), - signature.length() - ); - - // Assemble final JWT - return signing_input + "." + signature_encoded; -} - -std::string JWTPluginContext::verifyJWT(const std::string& token) { - auto* root = dynamic_cast(this->root()); - - // Split token into parts - std::vector parts; - std::stringstream ss(token); - std::string part; - - while (std::getline(ss, part, '.')) { - parts.push_back(part); - } - - if (parts.size() != 3) { - return "Invalid token format"; - } - - // Verify signature - std::string signing_input = parts[0] + "." + parts[1]; - std::string expected_signature = hmac_sha256(root->secret_key_, signing_input); - std::string expected_signature_encoded = base64_url_encode( - reinterpret_cast(expected_signature.c_str()), - expected_signature.length() - ); - - if (parts[2] != expected_signature_encoded) { - return "Invalid signature"; - } - - // Decode and verify payload - try { - std::string payload_str = base64_url_decode(parts[1]); - json payload = json::parse(payload_str); - - // Verify expiration - auto now = std::chrono::system_clock::now(); - auto now_seconds = std::chrono::duration_cast( - now.time_since_epoch() - ).count(); - - if (payload.contains("exp") && payload["exp"].get() < now_seconds) { - return "Token expired"; - } - - // Verify not-before - if (payload.contains("nbf") && payload["nbf"].get() > now_seconds) { - return "Token not yet valid"; - } - - return "valid"; - - } catch (const std::exception& e) { - return "Token verification failed: " + std::string(e.what()); - } -} - -FilterHeadersStatus JWTPluginContext::onRequestHeaders(uint32_t headers, bool end_of_stream) { - auto* root = dynamic_cast(this->root()); - auto path = getRequestHeader(":path"); - auto method = getRequestHeader(":method"); - - // Token generation endpoint - if (path->view() == "/generate-token" && method->view() == "POST") { - // Get user_id from header or body - auto user_id_header = getRequestHeader("x-user-id"); - - if (!user_id_header || user_id_header->view().empty()) { - sendLocalResponse(400, "", "Missing x-user-id header", {}); - return FilterHeadersStatus::StopIteration; - } - - std::string user_id(user_id_header->view()); - - // Get optional expiration override - int expiration = root->default_expiration_; - auto exp_header = getRequestHeader("x-expiration-minutes"); - if (exp_header) { - try { - expiration = std::stoi(std::string(exp_header->view())); - } catch (...) { - // Use default if invalid - } - } - - // Generate JWT - std::string jwt = generateJWT(user_id, expiration); - - // Return JWT in response - json response = { - {"token", jwt}, - {"expires_in", expiration * 60}, - {"token_type", "Bearer"} - }; - - sendLocalResponse(200, "", response.dump(), { - {"content-type", "application/json"} - }); - - return FilterHeadersStatus::StopIteration; - } - - // Token verification endpoint - if (path->view() == "/verify-token" && method->view() == "GET") { - auto auth_header = getRequestHeader("authorization"); - - if (!auth_header || auth_header->view().empty()) { - sendLocalResponse(401, "", "Missing Authorization header", {}); - return FilterHeadersStatus::StopIteration; - } - - std::string auth(auth_header->view()); - - // Extract Bearer token - if (auth.substr(0, 7) != "Bearer ") { - sendLocalResponse(401, "", "Invalid Authorization format", {}); - return FilterHeadersStatus::StopIteration; - } - - std::string token = auth.substr(7); - std::string verification_result = verifyJWT(token); - - if (verification_result == "valid") { - json response = { - {"valid", true}, - {"message", "Token is valid"} - }; - sendLocalResponse(200, "", response.dump(), { - {"content-type", "application/json"} - }); - } else { - json response = { - {"valid", false}, - {"message", verification_result} - }; - sendLocalResponse(401, "", response.dump(), { - {"content-type", "application/json"} - }); - } - - return FilterHeadersStatus::StopIteration; - } - - // For other requests, validate token if present - auto auth_header = getRequestHeader("authorization"); - if (auth_header && !auth_header->view().empty()) { - std::string auth(auth_header->view()); - - if (auth.substr(0, 7) == "Bearer ") { - std::string token = auth.substr(7); - std::string verification_result = verifyJWT(token); - - if (verification_result != "valid") { - sendLocalResponse(401, "", "Unauthorized: " + verification_result, {}); - return FilterHeadersStatus::StopIteration; - } - - // Token is valid, add user info to headers for downstream services - try { - std::vector parts; - std::stringstream ss(token); - std::string part; - - while (std::getline(ss, part, '.')) { - parts.push_back(part); - } - - if (parts.size() == 3) { - std::string payload_str = base64_url_decode(parts[1]); - json payload = json::parse(payload_str); - - if (payload.contains("sub")) { - addRequestHeader("x-jwt-user", payload["sub"].get()); - } - if (payload.contains("plan")) { - addRequestHeader("x-jwt-plan", payload["plan"].get()); - } - } - } catch (...) { - // Continue even if header extraction fails - } - } - } - - return FilterHeadersStatus::Continue; -} - -static RegisterContextFactory register_JWTPlugin( - CONTEXT_FACTORY(JWTPluginContext), - ROOT_FACTORY(JWTPluginRootContext) -); -// [END serviceextensions_plugin_drafting_jwt_token] From 6237c440e97afce18a0e619ee738319339db51a7 Mon Sep 17 00:00:00 2001 From: victorwoli Date: Tue, 7 Apr 2026 11:09:05 -0300 Subject: [PATCH 6/6] Adjustmets base on the PR comments --- plugins/samples/drafting_jwt_token/plugin.go | 51 +++++++------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/plugins/samples/drafting_jwt_token/plugin.go b/plugins/samples/drafting_jwt_token/plugin.go index 416f095b4..9b45fc9a3 100644 --- a/plugins/samples/drafting_jwt_token/plugin.go +++ b/plugins/samples/drafting_jwt_token/plugin.go @@ -42,9 +42,9 @@ func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { type pluginContext struct { types.DefaultPluginContext - secretKey string - defaultExpiration int - kvStore map[string]*UserEntitlements + secretKey string + defaultExpiration int + kvStore map[string]*UserEntitlements } // UserEntitlements represents user permissions and roles from KV store @@ -56,9 +56,9 @@ type UserEntitlements struct { // Config represents the plugin configuration type Config struct { - SecretKey string `json:"secret_key"` - DefaultExpirationMinutes int `json:"default_expiration_minutes"` - Data map[string]*UserEntitlements `json:"data"` + SecretKey string `json:"secret_key"` + DefaultExpirationMinutes int `json:"default_expiration_minutes"` + Data map[string]*UserEntitlements `json:"data"` } // JWTHeader represents the JWT header structure @@ -320,13 +320,12 @@ func (ctx *httpContext) generateJWT(userID string, expirationMinutes int) (strin } payloadEncoded := base64URLEncode(payloadJSON) - // Create signature - signingInput := headerEncoded + "." + payloadEncoded + signingInput := strings.Join([]string{headerEncoded, payloadEncoded}, ".") signature := ctx.hmacSHA256(ctx.plugin.secretKey, signingInput) signatureEncoded := base64URLEncode(signature) // Assemble final JWT - return signingInput + "." + signatureEncoded, nil + return strings.Join([]string{signingInput, signatureEncoded}, "."), nil } func (ctx *httpContext) verifyJWT(token string) error { @@ -337,7 +336,7 @@ func (ctx *httpContext) verifyJWT(token string) error { } // Verify signature - signingInput := parts[0] + "." + parts[1] + signingInput := strings.Join([]string{parts[0], parts[1]}, ".") expectedSignature := ctx.hmacSHA256(ctx.plugin.secretKey, signingInput) expectedSignatureEncoded := base64URLEncode(expectedSignature) @@ -351,14 +350,16 @@ func (ctx *httpContext) verifyJWT(token string) error { return fmt.Errorf("token verification failed: %w", err) } - // Verify expiration + const clockSkewBuffer = int64(60) now := time.Now().Unix() - if payload.Exp < now { + + // Verify expiration (allow 1-minute buffer) + if payload.Exp+clockSkewBuffer < now { return fmt.Errorf("token expired") } - // Verify not-before - if payload.Nbf > now { + // Verify not-before (allow 1-minute buffer) + if payload.Nbf > now+clockSkewBuffer { return fmt.Errorf("token not yet valid") } @@ -420,28 +421,12 @@ func (ctx *httpContext) sendTextResponse(statusCode uint32, message string) { } } -// base64URLEncode encodes bytes to base64 URL-safe format func base64URLEncode(data []byte) string { - encoded := base64.StdEncoding.EncodeToString(data) - encoded = strings.ReplaceAll(encoded, "+", "-") - encoded = strings.ReplaceAll(encoded, "/", "_") - encoded = strings.TrimRight(encoded, "=") - return encoded + return base64.RawURLEncoding.EncodeToString(data) } -// base64URLDecode decodes base64 URL-safe format to bytes func base64URLDecode(data string) ([]byte, error) { - data = strings.ReplaceAll(data, "-", "+") - data = strings.ReplaceAll(data, "_", "/") - - // Add padding if needed - switch len(data) % 4 { - case 2: - data += "==" - case 3: - data += "=" - } - - return base64.StdEncoding.DecodeString(data) + return base64.RawURLEncoding.DecodeString(data) } + // [END serviceextensions_plugin_drafting_jwt_token]