Skip to content

Commit 245f070

Browse files
committed
Add HMAC with timestamp endpoint configuration and acceptance tests
1 parent 32840bc commit 245f070

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

spec/acceptance/acceptance_tests.rb

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,162 @@
116116
end
117117
end
118118

119+
describe "hmac_with_timestamp" do
120+
it "successfully processes a valid POST request with HMAC signature and timestamp" do
121+
payload = { text: "Hello, World!" }
122+
timestamp = Time.now.utc.iso8601
123+
body = payload.to_json
124+
signing_payload = "#{timestamp}:#{body}"
125+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
126+
headers = {
127+
"Content-Type" => "application/json",
128+
"X-HMAC-Signature" => "sha256=#{digest}",
129+
"X-HMAC-Timestamp" => timestamp
130+
}
131+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
132+
expect(response).to be_a(Net::HTTPSuccess)
133+
body = JSON.parse(response.body)
134+
expect(body["status"]).to eq("success")
135+
end
136+
137+
it "successfully processes a valid POST request with HMAC signature and timestamp and an empty payload" do
138+
payload = {}
139+
timestamp = Time.now.utc.iso8601
140+
body = payload.to_json
141+
signing_payload = "#{timestamp}:#{body}"
142+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
143+
headers = {
144+
"Content-Type" => "application/json",
145+
"X-HMAC-Signature" => "sha256=#{digest}",
146+
"X-HMAC-Timestamp" => timestamp
147+
}
148+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
149+
expect(response).to be_a(Net::HTTPSuccess)
150+
body = JSON.parse(response.body)
151+
expect(body["status"]).to eq("success")
152+
end
153+
154+
it "successfully processes a valid POST request with HMAC signature and the POST has no body" do
155+
timestamp = Time.now.utc.iso8601
156+
signing_payload = "#{timestamp}:" # Empty body
157+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
158+
headers = {
159+
"Content-Type" => "application/json",
160+
"X-HMAC-Signature" => "sha256=#{digest}",
161+
"X-HMAC-Timestamp" => timestamp
162+
}
163+
response = http.post("/webhooks/hmac_with_timestamp", nil, headers)
164+
expect(response).to be_a(Net::HTTPSuccess)
165+
body = JSON.parse(response.body)
166+
expect(body["status"]).to eq("success")
167+
end
168+
169+
it "fails due to using the wrong HMAC secret" do
170+
payload = { text: "Hello, World!" }
171+
timestamp = Time.now.utc.iso8601
172+
body = payload.to_json
173+
signing_payload = "#{timestamp}:#{body}"
174+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), "bad-hmac-secret", signing_payload)
175+
headers = {
176+
"Content-Type" => "application/json",
177+
"X-HMAC-Signature" => "sha256=#{digest}",
178+
"X-HMAC-Timestamp" => timestamp
179+
}
180+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
181+
expect(response).to be_a(Net::HTTPUnauthorized)
182+
expect(response.body).to include("authentication failed")
183+
end
184+
185+
it "fails due to missing timestamp header" do
186+
payload = { text: "Hello, World!" }
187+
body = payload.to_json
188+
signing_payload = "#{Time.now.utc.iso8601}:#{body}"
189+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
190+
headers = {
191+
"Content-Type" => "application/json",
192+
"X-HMAC-Signature" => "sha256=#{digest}"
193+
# Missing X-HMAC-Timestamp header
194+
}
195+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
196+
expect(response).to be_a(Net::HTTPUnauthorized)
197+
expect(response.body).to include("authentication failed")
198+
end
199+
200+
it "fails due to invalid timestamp format" do
201+
payload = { text: "Hello, World!" }
202+
invalid_timestamp = "not-a-timestamp"
203+
body = payload.to_json
204+
signing_payload = "#{invalid_timestamp}:#{body}"
205+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
206+
headers = {
207+
"Content-Type" => "application/json",
208+
"X-HMAC-Signature" => "sha256=#{digest}",
209+
"X-HMAC-Timestamp" => invalid_timestamp
210+
}
211+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
212+
expect(response).to be_a(Net::HTTPUnauthorized)
213+
expect(response.body).to include("authentication failed")
214+
end
215+
216+
it "rejects request with timestamp manipulation attack" do
217+
payload = { text: "Hello, World!" }
218+
original_timestamp = Time.now.utc.iso8601
219+
manipulated_timestamp = (Time.now.utc + 100).iso8601 # Future timestamp
220+
221+
# Create signature with original timestamp
222+
signing_payload = "#{original_timestamp}:#{payload.to_json}"
223+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
224+
225+
# But send manipulated timestamp in header
226+
headers = {
227+
"Content-Type" => "application/json",
228+
"X-HMAC-Signature" => "sha256=#{digest}",
229+
"X-HMAC-Timestamp" => manipulated_timestamp
230+
}
231+
232+
response = http.post("/webhooks/hmac_with_timestamp", payload.to_json, headers)
233+
expect(response).to be_a(Net::HTTPUnauthorized)
234+
expect(response.body).to include("authentication failed")
235+
end
236+
237+
it "fails because the timestamp is too old" do
238+
payload = { text: "Hello, World!" }
239+
# Use timestamp that's 10 minutes old (beyond the tolerance)
240+
expired_timestamp = (Time.now.utc - 600).iso8601
241+
242+
signing_payload = "#{expired_timestamp}:#{payload.to_json}"
243+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), FAKE_ALT_HMAC_SECRET, signing_payload)
244+
245+
headers = {
246+
"Content-Type" => "application/json",
247+
"X-HMAC-Signature" => "sha256=#{digest}",
248+
"X-HMAC-Timestamp" => expired_timestamp
249+
}
250+
251+
response = http.post("/webhooks/hmac_with_timestamp", payload.to_json, headers)
252+
expect(response).to be_a(Net::HTTPUnauthorized)
253+
expect(response.body).to include("authentication failed")
254+
end
255+
256+
it "fails because the wrong HMAC algorithm is used" do
257+
payload = { text: "Hello, World!" }
258+
timestamp = Time.now.utc.iso8601
259+
body = payload.to_json
260+
signing_payload = "#{timestamp}:#{body}"
261+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha512"), FAKE_ALT_HMAC_SECRET, signing_payload)
262+
263+
headers = {
264+
"Content-Type" => "application/json",
265+
"X-HMAC-Signature" => "sha512=#{digest}",
266+
"X-HMAC-Timestamp" => timestamp
267+
}
268+
269+
response = http.post("/webhooks/hmac_with_timestamp", body, headers)
270+
expect(response).to be_a(Net::HTTPUnauthorized)
271+
expect(response.body).to include("authentication failed")
272+
end
273+
end
274+
119275
describe "slack" do
120276
it "successfully processes a valid POST request with HMAC signature and timestamp" do
121277
payload = { text: "Hello, Slack!" }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
path: /hmac_with_timestamp
2+
handler: Hello
3+
4+
auth:
5+
type: hmac
6+
secret_env_key: ALT_WEBHOOK_SECRET
7+
header: X-HMAC-Signature
8+
timestamp_header: X-HMAC-Timestamp
9+
timestamp_tolerance: 60 # 1 minute
10+
algorithm: sha256
11+
format: "algorithm=signature" # produces "sha256=abc123..."
12+
payload_template: "{timestamp}:{body}"

0 commit comments

Comments
 (0)