diff --git a/README.md b/README.md index 9b927b846..bc0a2610a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ require 'fulfillment-outbound-api-model' Rails.cache.write("SPAPI-TOKEN-#{access_token_key}", token[:access_token], expires_in: token[:expires_in] - 60) end config.get_access_token = -> (access_token_key) { Rails.cache.read("SPAPI-TOKEN-#{access_token_key}") } + + # optional lambdas for caching grantless LWA access token instead of requesting it each time, e.g.: + config.save_grantless_access_token = -> (access_token_key, token) do + Rails.cache.write("SPAPI-TOKEN-#{access_token_key}", token[:access_token], expires_in: token[:expires_in] - 60) + end + config.get_grantless_access_token = -> (access_token_key) { Rails.cache.read("SPAPI-TOKEN-#{access_token_key}") } end begin diff --git a/lib/sp_api_client.rb b/lib/sp_api_client.rb index 5d3a935ce..c06c92413 100644 --- a/lib/sp_api_client.rb +++ b/lib/sp_api_client.rb @@ -13,40 +13,64 @@ def initialize(config = SpConfiguration.default) alias_method :super_call_api, :call_api def call_api(http_method, path, opts = {}) unsigned_request = build_request(http_method, path, opts) - aws_headers = auth_headers(http_method, unsigned_request.url, unsigned_request.encoded_body) + aws_headers = auth_headers(http_method, path, unsigned_request.url, unsigned_request.encoded_body) signed_opts = opts.merge(:header_params => aws_headers.merge(opts[:header_params] || {})) super(http_method, path, signed_opts) end private + def retrieve_token(grantless, get_token_method, save_token_method, token_key) + return request_lwa_access_token(grantless)[:access_token] unless config.public_send(get_token_method) + + stored_token = config.public_send(get_token_method).call(config.public_send(token_key)) + stored_token || store_and_return_new_token(grantless, save_token_method, token_key) + end + + def store_and_return_new_token(grantless, save_token_method, token_key) + new_token = request_lwa_access_token(grantless) + config.public_send(save_token_method).call(config.public_send(token_key), new_token) if config.public_send(save_token_method) + new_token[:access_token] + end + + def retrieve_lwa_grantless_access_token + retrieve_token( + true, + :get_grantless_access_token, + :save_grantless_access_token, + :grantless_access_token_key) + end + def retrieve_lwa_access_token - return request_lwa_access_token[:access_token] unless config.get_access_token - stored_token = config.get_access_token.call(config.access_token_key) - if stored_token.nil? - new_token = request_lwa_access_token - config.save_access_token.call(config.access_token_key, new_token) if config.save_access_token - return new_token[:access_token] - else - return stored_token - end + retrieve_token( + false, + :get_access_token, + :save_access_token, + :access_token_key) end - def request_lwa_access_token + def request_lwa_access_token(grantless) newself = self.dup newself.config = config.dup newself.config.host = 'api.amazon.com' + form_params = { + client_id: config.client_id, + client_secret: config.client_secret, + grant_type: grantless ? 'client_credentials' : 'refresh_token' + } + + if grantless + form_params[:scope] = 'sellingpartnerapi::notifications' + else + form_params[:refresh_token] = config.refresh_token + end + data, status_code, headers = newself.super_call_api(:POST, '/auth/o2/token', :header_params => { 'Content-Type' => 'application/x-www-form-urlencoded' }, - :form_params => { - grant_type: 'refresh_token', - refresh_token: config.refresh_token, - client_id: config.client_id, - client_secret: config.client_secret - }, + :form_params => form_params, :return_type => 'Object') unless data && data[:access_token] @@ -73,10 +97,30 @@ def signed_request_headers(http_method, url, body) signer.sign_request(http_method: http_method.to_s, url: url, body: body).headers end - def auth_headers(http_method, url, body) - signed_request_headers(http_method, url, body).merge({ - 'x-amz-access-token' => retrieve_lwa_access_token - }) + def auth_headers(http_method, path, url, body) + access_token = if grantless_request?(http_method, path) + retrieve_lwa_grantless_access_token + else + retrieve_lwa_access_token + end + + signed_request_headers(http_method, url, body).merge({ 'x-amz-access-token' => access_token }) + end + + def grantless_request?(http_method, path) + case http_method + when :POST + path == '/notifications/v1/destinations' + when :GET + %w[/notifications/v1/destinations /authorization/v1/authorizationCode].include?(path) || + !!path.match(%r{^/notifications/v1/subscriptions/[^/]+/[^/]+$}) || + !!path.match(%r{^/notifications/v1/destinations/[^/]+$}) + when :DELETE + !!path.match(%r{^/notifications/v1/destinations/[^/]+$}) || + !!path.match(%r{^/notifications/v2/subscriptions/[^/]+/[^/]+$}) + else + false + end end end end diff --git a/lib/sp_configuration.rb b/lib/sp_configuration.rb index 80a73a8d5..69132f105 100644 --- a/lib/sp_configuration.rb +++ b/lib/sp_configuration.rb @@ -4,7 +4,8 @@ module AmzSpApi class SpConfiguration < Configuration attr_accessor :refresh_token, :client_id, :client_secret, :sandbox, :region, :aws_access_key_id, :aws_secret_access_key, :credentials_provider, # either access key or credentials_provider for AWS Signer, e.g. Aws::STS::Client - :save_access_token, :get_access_token # optional lambdas for storing and retrieving token + :save_access_token, :get_access_token, # optional lambdas for storing and retrieving token + :save_grantless_access_token, :get_grantless_access_token # optional lambdas for storing and retrieving grantless token # from https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md#selling-partner-api-endpoints AWS_REGION_MAP = { @@ -33,6 +34,10 @@ def access_token_key Digest::MD5.hexdigest("#{client_id} #{refresh_token}") end + def grantless_access_token_key + Digest::MD5.hexdigest("#{client_id} #{region} grantless") + end + def self.default @@default ||= SpConfiguration.new end