diff --git a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb index b5aeb9949..c7a21f672 100644 --- a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb +++ b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb @@ -41,7 +41,7 @@ def call_rpc(endpoint, method: :get, payload: nil, symbolize_keys: false) faraday.ssl.verify = !ForestAdminRpcAgent::Facades::Container.cache(:debug) end - timestamp = Time.now.utc.iso8601 + timestamp = Time.now.utc.iso8601(3) signature = generate_signature(timestamp) headers = { diff --git a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/sse_client.rb b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/sse_client.rb index 30e959740..e22cea41f 100644 --- a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/sse_client.rb +++ b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/sse_client.rb @@ -17,7 +17,7 @@ def initialize(uri, auth_secret, &on_rpc_stop) def start return if @closed - timestamp = Time.now.utc.iso8601 + timestamp = Time.now.utc.iso8601(3) signature = generate_signature(timestamp) headers = { diff --git a/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/utils/sse_client_spec.rb b/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/utils/sse_client_spec.rb index aaaf10fb4..c4d856108 100644 --- a/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/utils/sse_client_spec.rb +++ b/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/utils/sse_client_spec.rb @@ -19,7 +19,7 @@ module Utils allow(fake_client).to receive(:on_event) allow(fake_client).to receive(:on_error) - timestamp = '2025-01-01T12:00:00Z' + timestamp = '2025-01-01T12:00:00.000Z' signature = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp) # fix the timestamp to a specific value allow(Time).to receive(:now).and_return(Time.parse(timestamp)) diff --git a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/middleware/authentication_spec.rb b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/middleware/authentication_spec.rb index 69d5f39f3..33068792a 100644 --- a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/middleware/authentication_spec.rb +++ b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/middleware/authentication_spec.rb @@ -192,6 +192,45 @@ module Middleware end end + context 'when multiple requests with millisecond timestamps (rapid fire)' do + it 'allows multiple requests in the same second with different milliseconds' do + # Simulate 3 rapid requests within the same second but with different milliseconds + 3.times do |i| + timestamp_ms = Time.now.utc.iso8601(3) + signature_ms = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp_ms) + + test_env = { + 'HTTP_X_SIGNATURE' => signature_ms, + 'HTTP_X_TIMESTAMP' => timestamp_ms + } + + status, = middleware.call(test_env) + expect(status).to eq(200), "Request #{i + 1} should succeed with millisecond timestamp" + + # Simulate a tiny delay to ensure different milliseconds + sleep(0.002) + end + end + + it 'blocks replay with same millisecond timestamp' do + # First request with millisecond precision + timestamp_ms = Time.now.utc.iso8601(3) + signature_ms = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp_ms) + + env['HTTP_X_SIGNATURE'] = signature_ms + env['HTTP_X_TIMESTAMP'] = timestamp_ms + + # First request - should pass + status, = middleware.call(env) + expect(status).to eq(200) + + # Second request with exact same timestamp and signature - should be blocked + status, _headers, body = middleware.call(env) + expect(status).to eq(401) + expect(JSON.parse(body.first)).to eq({ 'error' => 'Unauthorized' }) + end + end + context 'with edge cases' do it 'handles malformed ISO8601 timestamp' do env['HTTP_X_SIGNATURE'] = signature