Skip to content

Support one time verification via get endpoints #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,23 @@ 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
```

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.
Expand Down
3 changes: 2 additions & 1 deletion lib/hooks/app/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/hooks/core/config_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions spec/acceptance/acceptance_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions spec/acceptance/config/endpoints/boomtown.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
path: /boomtown
handler: Boomtown
method: post
3 changes: 3 additions & 0 deletions spec/acceptance/config/endpoints/okta_setup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
path: /okta_webhook_setup
handler: OktaSetupHandler
method: get
43 changes: 43 additions & 0 deletions spec/acceptance/plugins/handlers/okta_setup_handler.rb
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions spec/unit/lib/hooks/core/config_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down