diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 8e86694c638..69234ac992b 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -4,6 +4,37 @@ class SocureWebhookController < ApplicationController skip_before_action :verify_authenticity_token def create - render json: { message: 'Got here.' } + if token_valid? + render json: { message: 'Secret token is valid.' } + else + render status: :unauthorized, json: { message: 'Invalid secret token.' } + end + end + + private + + def token_valid? + authorization_header = request.headers['Authorization']&.split&.last + + return false if authorization_header.nil? + + verify_current_key(authorization_header: authorization_header) || + verify_queue(authorization_header: authorization_header) + end + + def verify_current_key(authorization_header:) + ActiveSupport::SecurityUtils.secure_compare( + authorization_header, + IdentityConfig.store.socure_webhook_secret_key, + ) + end + + def verify_queue(authorization_header:) + IdentityConfig.store.socure_webhook_secret_key_queue.any? do |key| + ActiveSupport::SecurityUtils.secure_compare( + authorization_header, + key, + ) + end end end diff --git a/config/application.yml.default b/config/application.yml.default index 47f54bdcde8..e48f0ec07a3 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -335,6 +335,8 @@ sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1 sign_in_user_id_per_ip_attempt_window_in_minutes: 720 sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200 sign_in_user_id_per_ip_max_attempts: 50 +socure_webhook_secret_key: '' +socure_webhook_secret_key_queue: '[]' sp_handoff_bounce_max_seconds: 2 sp_issuer_user_counts_report_configs: '[]' team_ada_email: '' @@ -425,6 +427,8 @@ development: show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' + socure_webhook_secret_key: 'secret-key' + socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true @@ -549,6 +553,8 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 short_term_phone_otp_max_attempts: 100 skip_encryption_allowed_list: '[]' + socure_webhook_secret_key: 'secret-key' + socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' state_tracking_enabled: true team_ada_email: 'ada@example.com' team_all_login_emails: '["b@example.com", "c@example.com"]' diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 9412d21a65f..72b545ee576 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -383,6 +383,8 @@ def self.store config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer) config.add(:sign_in_recaptcha_score_threshold, type: :float) config.add(:skip_encryption_allowed_list, type: :json) + config.add(:socure_webhook_secret_key, type: :string) + config.add(:socure_webhook_secret_key_queue, type: :json) config.add(:sp_handoff_bounce_max_seconds, type: :integer) config.add(:sp_issuer_user_counts_report_configs, type: :json) config.add(:state_tracking_enabled, type: :boolean) diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index cd2c4c1027a..04a06d36d03 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -4,9 +4,41 @@ RSpec.describe SocureWebhookController do describe 'POST /api/webhooks/socure/event' do - it 'returns OK' do + let(:socure_secret_key) { 'this-is-a-secret' } + let(:socure_secret_key_queue) { ['this-is-an-old-secret', 'this-is-an-older-secret'] } + + before do + allow(IdentityConfig.store).to receive(:socure_webhook_secret_key). + and_return(socure_secret_key) + allow(IdentityConfig.store).to receive(:socure_webhook_secret_key_queue). + and_return(socure_secret_key_queue) + end + + it 'returns OK with a correct secret key' do + request.headers['Authorization'] = socure_secret_key post :create + + expect(response).to have_http_status(:ok) + end + + it 'returns OK with an older secret key' do + request.headers['Authorization'] = socure_secret_key_queue.last + post :create + expect(response).to have_http_status(:ok) end + + it 'returns unauthorized with a bad secret key' do + request.headers['Authorization'] = 'ABC123' + post :create + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns unauthorized with no secret key' do + post :create + + expect(response).to have_http_status(:unauthorized) + end end end