From 729ef4f931e024010729a66647fc9408173aaf83 Mon Sep 17 00:00:00 2001 From: GrantBirki Date: Wed, 11 Jun 2025 23:19:38 -0700 Subject: [PATCH 1/4] Add HTTP method configuration support for endpoints - Updated configuration documentation to include the `method` field. - Modified API to handle dynamic HTTP methods for endpoints. - Enhanced config validator to accept and validate HTTP methods. - Added tests for method validation and handling in endpoint configurations. --- docs/configuration.md | 15 ++++++ lib/hooks/app/api.rb | 3 +- lib/hooks/core/config_validator.rb | 1 + .../acceptance/config/endpoints/boomtown.yaml | 1 + .../config/endpoints/okta_setup.yaml | 3 ++ .../plugins/handlers/okta_setup_handler.rb | 43 ++++++++++++++++ .../lib/hooks/core/config_validator_spec.rb | 51 +++++++++++++++++++ 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 spec/acceptance/config/endpoints/okta_setup.yaml create mode 100644 spec/acceptance/plugins/handlers/okta_setup_handler.rb diff --git a/docs/configuration.md b/docs/configuration.md index 8f9a0756..54271e69 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -113,6 +113,21 @@ handler: GithubHandler > For readability, you should use CamelCase for handler names, as they are Ruby classes. You should then name the file in the `handler_plugin_dir` as `github_handler.rb`. +### `method` + +The HTTP method that the endpoint will respond to. This allows you to configure endpoints for different HTTP verbs based on your webhook provider's requirements. + +**Default:** `post` +**Valid values:** `get`, `post`, `put`, `patch`, `delete`, `head`, `options` + +**Example:** + +```yaml +method: post # Most webhooks use POST +# or +method: put # Some REST APIs might use PUT for updates +``` + ### `auth` Authentication configuration for the endpoint. This section defines how incoming requests will be authenticated before being processed by the handler. diff --git a/lib/hooks/app/api.rb b/lib/hooks/app/api.rb index 0d43f1df..acece6e3 100644 --- a/lib/hooks/app/api.rb +++ b/lib/hooks/app/api.rb @@ -47,8 +47,9 @@ def self.create(config:, endpoints:, log:) endpoints.each do |endpoint_config| full_path = "#{config[:root_path]}#{endpoint_config[:path]}" handler_class_name = endpoint_config[:handler] + http_method = (endpoint_config[:method] || "post").downcase.to_sym - post(full_path) do + send(http_method, full_path) do request_id = uuid start_time = Time.now diff --git a/lib/hooks/core/config_validator.rb b/lib/hooks/core/config_validator.rb index 0fb98007..c610dd6d 100644 --- a/lib/hooks/core/config_validator.rb +++ b/lib/hooks/core/config_validator.rb @@ -34,6 +34,7 @@ class ValidationError < StandardError; end ENDPOINT_CONFIG_SCHEMA = Dry::Schema.Params do required(:path).filled(:string) required(:handler).filled(:string) + optional(:method).filled(:string, included_in?: %w[get post put patch delete head options]) optional(:auth).hash do required(:type).filled(:string) diff --git a/spec/acceptance/config/endpoints/boomtown.yaml b/spec/acceptance/config/endpoints/boomtown.yaml index af0bc05b..497ed57c 100644 --- a/spec/acceptance/config/endpoints/boomtown.yaml +++ b/spec/acceptance/config/endpoints/boomtown.yaml @@ -1,2 +1,3 @@ path: /boomtown handler: Boomtown +method: post diff --git a/spec/acceptance/config/endpoints/okta_setup.yaml b/spec/acceptance/config/endpoints/okta_setup.yaml new file mode 100644 index 00000000..29461b92 --- /dev/null +++ b/spec/acceptance/config/endpoints/okta_setup.yaml @@ -0,0 +1,3 @@ +path: /okta_webhook_setup +handler: OktaSetupHandler +method: get diff --git a/spec/acceptance/plugins/handlers/okta_setup_handler.rb b/spec/acceptance/plugins/handlers/okta_setup_handler.rb new file mode 100644 index 00000000..b04de5e4 --- /dev/null +++ b/spec/acceptance/plugins/handlers/okta_setup_handler.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class OktaSetupHandler < Hooks::Plugins::Handlers::Base + def call(payload:, headers:, config:) + # Handle Okta's one-time verification challenge + # Okta sends a GET request with x-okta-verification-challenge header + # We need to return the challenge value in a JSON response + + verification_challenge = extract_verification_challenge(headers) + + if verification_challenge + log.info("Processing Okta verification challenge") + { + verification: verification_challenge + } + else + log.error("Missing x-okta-verification-challenge header in request") + { + error: "Missing verification challenge header", + expected_header: "x-okta-verification-challenge" + } + end + end + + private + + # Extract the verification challenge from headers (case-insensitive) + # + # @param headers [Hash] HTTP headers from the request + # @return [String, nil] The verification challenge value or nil if not found + def extract_verification_challenge(headers) + return nil unless headers.is_a?(Hash) + + # Search for the header case-insensitively + headers.each do |key, value| + if key.to_s.downcase == "x-okta-verification-challenge" + return value + end + end + + nil + end +end diff --git a/spec/unit/lib/hooks/core/config_validator_spec.rb b/spec/unit/lib/hooks/core/config_validator_spec.rb index dd042fcd..bd53518e 100644 --- a/spec/unit/lib/hooks/core/config_validator_spec.rb +++ b/spec/unit/lib/hooks/core/config_validator_spec.rb @@ -244,6 +244,33 @@ expect(result).to eq(config) end + + it "returns validated configuration with method specified" do + config = { + path: "/webhook/put", + handler: "PutHandler", + method: "put" + } + + result = described_class.validate_endpoint_config(config) + + expect(result).to eq(config) + end + + it "accepts all valid HTTP methods" do + valid_methods = %w[get post put patch delete head options] + + valid_methods.each do |method| + config = { + path: "/webhook/test", + handler: "TestHandler", + method: method + } + + result = described_class.validate_endpoint_config(config) + expect(result[:method]).to eq(method) + end + end end context "with invalid configuration" do @@ -405,6 +432,30 @@ described_class.validate_endpoint_config(config) }.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/) end + + it "raises ValidationError for invalid HTTP method" do + config = { + path: "/webhook/test", + handler: "TestHandler", + method: "invalid" + } + + expect { + described_class.validate_endpoint_config(config) + }.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/) + end + + it "raises ValidationError for non-string method" do + config = { + path: "/webhook/test", + handler: "TestHandler", + method: 123 + } + + expect { + described_class.validate_endpoint_config(config) + }.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/) + end end end From 1844d2cdf2fb14fa07a21954d676009a900cddb2 Mon Sep 17 00:00:00 2001 From: GrantBirki Date: Wed, 11 Jun 2025 23:21:35 -0700 Subject: [PATCH 2/4] add acceptance tests --- spec/acceptance/acceptance_tests.rb | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/acceptance/acceptance_tests.rb b/spec/acceptance/acceptance_tests.rb index a80246f4..e8015651 100644 --- a/spec/acceptance/acceptance_tests.rb +++ b/spec/acceptance/acceptance_tests.rb @@ -191,5 +191,35 @@ expect(response.body).to include("Boomtown error occurred") end end + + describe "okta setup" do + it "sends a POST request to the /webhooks/okta_webhook_setup endpoint and it fails because it is not a GET" do + payload = {}.to_json + headers = {} + response = http.post("/webhooks/okta_webhook_setup", payload, headers) + + expect(response).to be_a(Net::HTTPMethodNotAllowed) + expect(response.body).to include("405 Not Allowed") + end + + it "sends a GET request to the /webhooks/okta_webhook_setup endpoint and it returns the verification challenge" do + headers = { "x-okta-verification-challenge" => "test-challenge" } + response = http.get("/webhooks/okta_webhook_setup", headers) + + expect(response).to be_a(Net::HTTPSuccess) + body = JSON.parse(response.body) + expect(body["verification"]).to eq("test-challenge") + end + + it "sends a GET request to the /webhooks/okta_webhook_setup endpoint but it is missing the verification challenge header" do + response = http.get("/webhooks/okta_webhook_setup") + + expect(response).to be_a(Net::HTTPSuccess) + expect(response.code).to eq("200") + body = JSON.parse(response.body) + expect(body["error"]).to eq("Missing verification challenge header") + expect(body["expected_header"]).to eq("x-okta-verification-challenge") + end + end end end From 018544a89fd0f8af7df787abd150a7409b6c69b0 Mon Sep 17 00:00:00 2001 From: GrantBirki Date: Wed, 11 Jun 2025 23:23:47 -0700 Subject: [PATCH 3/4] okta notes --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 54271e69..eb21b966 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -128,6 +128,8 @@ method: post # Most webhooks use POST method: put # Some REST APIs might use PUT for updates ``` +In some cases, webhook providers (such as Okta) may require a one time verification request via a GET request. In such cases, you can set the method to `get` for that specific endpoint and then write a handler that processes the verification request. + ### `auth` Authentication configuration for the endpoint. This section defines how incoming requests will be authenticated before being processed by the handler. From cec1b52b92771821e8e100acd7cef268acc3e997 Mon Sep 17 00:00:00 2001 From: GrantBirki Date: Wed, 11 Jun 2025 23:25:15 -0700 Subject: [PATCH 4/4] lint --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index 23efd6b1..19f93ae6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -19,6 +19,7 @@ GitHub/InsecureHashAlgorithm: GitHub/AvoidObjectSendWithDynamicMethod: Exclude: - "spec/unit/lib/hooks/core/logger_factory_spec.rb" + - "lib/hooks/app/api.rb" Style/HashSyntax: Enabled: false