From e496bd9cb1d975683f99fd83c5a61b2d3ea5467c Mon Sep 17 00:00:00 2001 From: Muhsin Date: Tue, 17 Feb 2026 19:29:28 +0530 Subject: [PATCH] chore: fix integration --- .../shopify/callbacks_controller.rb | 40 ++++++++++++++++--- lib/redis/secure_storage.rb | 16 ++++---- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/app/controllers/shopify/callbacks_controller.rb b/app/controllers/shopify/callbacks_controller.rb index a4484d270..a29745550 100644 --- a/app/controllers/shopify/callbacks_controller.rb +++ b/app/controllers/shopify/callbacks_controller.rb @@ -9,6 +9,7 @@ class Shopify::CallbacksController < ApplicationController end rescue StandardError => e Rails.logger.error("Shopify callback error: #{e.message}") + Rails.logger.error("Shopify callback error backtrace: #{e.backtrace.first(10).join("\n")}") redirect_to error_redirect_url end @@ -37,6 +38,12 @@ class Shopify::CallbacksController < ApplicationController # Shopify will reject any attempt to exchange a code at a different shop's endpoint. @response = oauth_client.auth_code.get_token(params[:code], redirect_uri: redirect_callback_uri) + Rails.logger.info("OAuth Debug - Response present: #{@response.present?}") + Rails.logger.info("OAuth Debug - Response class: #{@response.class}") + Rails.logger.info("OAuth Debug - Parsed body: #{parsed_body.inspect}") + + raise StandardError, 'Failed to parse OAuth response' if parsed_body.blank? + token_key = SecureRandom.hex(16) pending_data = { access_token: parsed_body['access_token'], @@ -62,7 +69,15 @@ class Shopify::CallbacksController < ApplicationController end def parsed_body - @parsed_body ||= @response.response.parsed + @parsed_body ||= begin + parsed = @response.response.parsed + # SnakyHash may not behave like a regular hash for all operations + # Convert to a regular hash to ensure compatibility + { + 'access_token' => parsed.access_token || parsed['access_token'], + 'scope' => parsed.scope || parsed['scope'] + } + end end def oauth_client @@ -115,12 +130,25 @@ class Shopify::CallbacksController < ApplicationController def valid_hmac? return false if params[:hmac].blank? - # Shopify signs callback parameters with HMAC to prevent tampering + # Shopify HMAC validation + # Reference: https://shopify.dev/docs/apps/build/authentication-authorization/get-access-tokens hmac = params[:hmac] - # Convert to unsafe hash to avoid strong parameters restriction, then build query string - # Shopify expects parameters to be sorted alphabetically when computing HMAC - query_params = params.except(:hmac, :controller, :action).to_unsafe_h.sort.to_h.to_query - computed_hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), client_secret, query_params) + + # Build query string from params, excluding hmac and Rails-added params + query_params = params.except(:hmac, :controller, :action).to_unsafe_h + query_string = query_params.sort.map { |k, v| "#{k}=#{v}" }.join('&') + + # Compute HMAC-SHA256 + computed_hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), client_secret, query_string) + + # Debug logging + Rails.logger.info("HMAC Debug - Query params: #{query_params.inspect}") + Rails.logger.info("HMAC Debug - Query string for validation: #{query_string}") + Rails.logger.info("HMAC Debug - Shopify HMAC: #{hmac}") + Rails.logger.info("HMAC Debug - Computed HMAC: #{computed_hmac}") + Rails.logger.info("HMAC Debug - Client secret present: #{client_secret.present?}") + Rails.logger.info("HMAC Debug - Client secret length: #{client_secret&.length}") + Rails.logger.info("HMAC Debug - Client secret starts with: #{client_secret&.first(10)}") ActiveSupport::SecurityUtils.secure_compare(computed_hmac, hmac) end diff --git a/lib/redis/secure_storage.rb b/lib/redis/secure_storage.rb index ebd9c3445..0bd4d4cb8 100644 --- a/lib/redis/secure_storage.rb +++ b/lib/redis/secure_storage.rb @@ -15,14 +15,14 @@ module Redis::SecureStorage # @param expiry [Integer, ActiveSupport::Duration] TTL in seconds def set(key, data, expiry) encrypted = encrypt(data) - Alfred.setex(key, encrypted, expiry) + Redis::Alfred.setex(key, encrypted, expiry) end # Retrieve and decrypt data from Redis # @param key [String] Redis key # @return [Hash, nil] Decrypted data or nil if not found/invalid def get(key) - encrypted = Alfred.get(key) + encrypted = Redis::Alfred.get(key) return nil if encrypted.blank? decrypt(encrypted) @@ -33,16 +33,18 @@ module Redis::SecureStorage # Delete data from Redis # @param key [String] Redis key def delete(key) - Alfred.delete(key) + Redis::Alfred.delete(key) end private def encryptor - @encryptor ||= ActiveSupport::MessageEncryptor.new( - Rails.application.credentials.secret_key_base[0..31], - cipher: 'aes-256-gcm' - ) + @encryptor ||= begin + # Derive a proper 32-byte key from secret_key_base + key_generator = ActiveSupport::KeyGenerator.new(Rails.application.secret_key_base) + key = key_generator.generate_key('redis_secure_storage', 32) + ActiveSupport::MessageEncryptor.new(key, cipher: 'aes-256-gcm') + end end def encrypt(data)