Skip to content

Commit 562d723

Browse files
authored
Merge pull request #27 from github/support-one-time-verification-via-get-endpoints
Support one time verification via get endpoints
2 parents e7ecec0 + cec1b52 commit 562d723

File tree

9 files changed

+149
-1
lines changed

9 files changed

+149
-1
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ GitHub/InsecureHashAlgorithm:
1919
GitHub/AvoidObjectSendWithDynamicMethod:
2020
Exclude:
2121
- "spec/unit/lib/hooks/core/logger_factory_spec.rb"
22+
- "lib/hooks/app/api.rb"
2223

2324
Style/HashSyntax:
2425
Enabled: false

docs/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,23 @@ handler: GithubHandler
113113
114114
> 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`.
115115

116+
### `method`
117+
118+
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.
119+
120+
**Default:** `post`
121+
**Valid values:** `get`, `post`, `put`, `patch`, `delete`, `head`, `options`
122+
123+
**Example:**
124+
125+
```yaml
126+
method: post # Most webhooks use POST
127+
# or
128+
method: put # Some REST APIs might use PUT for updates
129+
```
130+
131+
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.
132+
116133
### `auth`
117134

118135
Authentication configuration for the endpoint. This section defines how incoming requests will be authenticated before being processed by the handler.

lib/hooks/app/api.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ def self.create(config:, endpoints:, log:)
4747
endpoints.each do |endpoint_config|
4848
full_path = "#{config[:root_path]}#{endpoint_config[:path]}"
4949
handler_class_name = endpoint_config[:handler]
50+
http_method = (endpoint_config[:method] || "post").downcase.to_sym
5051

51-
post(full_path) do
52+
send(http_method, full_path) do
5253
request_id = uuid
5354
start_time = Time.now
5455

lib/hooks/core/config_validator.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ValidationError < StandardError; end
3434
ENDPOINT_CONFIG_SCHEMA = Dry::Schema.Params do
3535
required(:path).filled(:string)
3636
required(:handler).filled(:string)
37+
optional(:method).filled(:string, included_in?: %w[get post put patch delete head options])
3738

3839
optional(:auth).hash do
3940
required(:type).filled(:string)

spec/acceptance/acceptance_tests.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,5 +191,35 @@
191191
expect(response.body).to include("Boomtown error occurred")
192192
end
193193
end
194+
195+
describe "okta setup" do
196+
it "sends a POST request to the /webhooks/okta_webhook_setup endpoint and it fails because it is not a GET" do
197+
payload = {}.to_json
198+
headers = {}
199+
response = http.post("/webhooks/okta_webhook_setup", payload, headers)
200+
201+
expect(response).to be_a(Net::HTTPMethodNotAllowed)
202+
expect(response.body).to include("405 Not Allowed")
203+
end
204+
205+
it "sends a GET request to the /webhooks/okta_webhook_setup endpoint and it returns the verification challenge" do
206+
headers = { "x-okta-verification-challenge" => "test-challenge" }
207+
response = http.get("/webhooks/okta_webhook_setup", headers)
208+
209+
expect(response).to be_a(Net::HTTPSuccess)
210+
body = JSON.parse(response.body)
211+
expect(body["verification"]).to eq("test-challenge")
212+
end
213+
214+
it "sends a GET request to the /webhooks/okta_webhook_setup endpoint but it is missing the verification challenge header" do
215+
response = http.get("/webhooks/okta_webhook_setup")
216+
217+
expect(response).to be_a(Net::HTTPSuccess)
218+
expect(response.code).to eq("200")
219+
body = JSON.parse(response.body)
220+
expect(body["error"]).to eq("Missing verification challenge header")
221+
expect(body["expected_header"]).to eq("x-okta-verification-challenge")
222+
end
223+
end
194224
end
195225
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
path: /boomtown
22
handler: Boomtown
3+
method: post
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
path: /okta_webhook_setup
2+
handler: OktaSetupHandler
3+
method: get
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
class OktaSetupHandler < Hooks::Plugins::Handlers::Base
4+
def call(payload:, headers:, config:)
5+
# Handle Okta's one-time verification challenge
6+
# Okta sends a GET request with x-okta-verification-challenge header
7+
# We need to return the challenge value in a JSON response
8+
9+
verification_challenge = extract_verification_challenge(headers)
10+
11+
if verification_challenge
12+
log.info("Processing Okta verification challenge")
13+
{
14+
verification: verification_challenge
15+
}
16+
else
17+
log.error("Missing x-okta-verification-challenge header in request")
18+
{
19+
error: "Missing verification challenge header",
20+
expected_header: "x-okta-verification-challenge"
21+
}
22+
end
23+
end
24+
25+
private
26+
27+
# Extract the verification challenge from headers (case-insensitive)
28+
#
29+
# @param headers [Hash] HTTP headers from the request
30+
# @return [String, nil] The verification challenge value or nil if not found
31+
def extract_verification_challenge(headers)
32+
return nil unless headers.is_a?(Hash)
33+
34+
# Search for the header case-insensitively
35+
headers.each do |key, value|
36+
if key.to_s.downcase == "x-okta-verification-challenge"
37+
return value
38+
end
39+
end
40+
41+
nil
42+
end
43+
end

spec/unit/lib/hooks/core/config_validator_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,33 @@
244244

245245
expect(result).to eq(config)
246246
end
247+
248+
it "returns validated configuration with method specified" do
249+
config = {
250+
path: "/webhook/put",
251+
handler: "PutHandler",
252+
method: "put"
253+
}
254+
255+
result = described_class.validate_endpoint_config(config)
256+
257+
expect(result).to eq(config)
258+
end
259+
260+
it "accepts all valid HTTP methods" do
261+
valid_methods = %w[get post put patch delete head options]
262+
263+
valid_methods.each do |method|
264+
config = {
265+
path: "/webhook/test",
266+
handler: "TestHandler",
267+
method: method
268+
}
269+
270+
result = described_class.validate_endpoint_config(config)
271+
expect(result[:method]).to eq(method)
272+
end
273+
end
247274
end
248275

249276
context "with invalid configuration" do
@@ -405,6 +432,30 @@
405432
described_class.validate_endpoint_config(config)
406433
}.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/)
407434
end
435+
436+
it "raises ValidationError for invalid HTTP method" do
437+
config = {
438+
path: "/webhook/test",
439+
handler: "TestHandler",
440+
method: "invalid"
441+
}
442+
443+
expect {
444+
described_class.validate_endpoint_config(config)
445+
}.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/)
446+
end
447+
448+
it "raises ValidationError for non-string method" do
449+
config = {
450+
path: "/webhook/test",
451+
handler: "TestHandler",
452+
method: 123
453+
}
454+
455+
expect {
456+
described_class.validate_endpoint_config(config)
457+
}.to raise_error(described_class::ValidationError, /Invalid endpoint configuration/)
458+
end
408459
end
409460
end
410461

0 commit comments

Comments
 (0)