diff --git a/lib/shopify_api/auth/client_credentials.rb b/lib/shopify_api/auth/client_credentials.rb new file mode 100644 index 000000000..5775c9686 --- /dev/null +++ b/lib/shopify_api/auth/client_credentials.rb @@ -0,0 +1,52 @@ +# typed: strict +# frozen_string_literal: true + +module ShopifyAPI + module Auth + module ClientCredentials + extend T::Sig + + CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials" + + class << self + extend T::Sig + + sig do + params( + shop: String, + ).returns(ShopifyAPI::Auth::Session) + end + def client_credentials(shop:) + unless ShopifyAPI::Context.setup? + raise ShopifyAPI::Errors::ContextNotSetupError, + "ShopifyAPI::Context not setup, please call ShopifyAPI::Context.setup" + end + + shop_session = ShopifyAPI::Auth::Session.new(shop: shop) + body = { + client_id: ShopifyAPI::Context.api_key, + client_secret: ShopifyAPI::Context.api_secret_key, + grant_type: CLIENT_CREDENTIALS_GRANT_TYPE, + } + + client = Clients::HttpClient.new(session: shop_session, base_path: "/admin/oauth") + response = + client.request( + Clients::HttpRequest.new( + http_method: :post, + path: "access_token", + body: body, + body_type: "application/json", + ), + ) + response_hash = T.cast(response.body, T::Hash[String, T.untyped]).to_h + + Session.from( + shop: shop, + access_token_response: Oauth::AccessTokenResponse.from_hash(response_hash), + ) + end + end + end + end +end diff --git a/lib/shopify_api/auth/session.rb b/lib/shopify_api/auth/session.rb index 1fba413b0..68cef2774 100644 --- a/lib/shopify_api/auth/session.rb +++ b/lib/shopify_api/auth/session.rb @@ -95,13 +95,16 @@ def from(shop:, access_token_response:) if is_online associated_user = T.must(access_token_response.associated_user) - expires = Time.now + access_token_response.expires_in.to_i associated_user_scope = access_token_response.associated_user_scope id = "#{shop}_#{associated_user.id}" else id = "offline_#{shop}" end + if access_token_response.expires_in + expires = Time.now + access_token_response.expires_in.to_i + end + new( id: id, shop: shop, diff --git a/test/auth/client_credentials_test.rb b/test/auth/client_credentials_test.rb new file mode 100644 index 000000000..d66adeb44 --- /dev/null +++ b/test/auth/client_credentials_test.rb @@ -0,0 +1,53 @@ +# typed: false +# frozen_string_literal: true + +require_relative "../test_helper" + +module ShopifyAPITest + module Auth + class ClientCredentialsTest < Test::Unit::TestCase + def setup + super() + + @stubbed_time_now = Time.now + @shop = "test-shop.myshopify.com" + @client_credentials_request = { + client_id: ShopifyAPI::Context.api_key, + client_secret: ShopifyAPI::Context.api_secret_key, + grant_type: "client_credentials", + } + @offline_token_response = { + access_token: SecureRandom.alphanumeric(10), + scope: "scope1,scope2", + expires_in: 1000, + } + end + + def test_client_credentials_context_not_setup + modify_context(api_key: "", api_secret_key: "", host: "") + + assert_raises(ShopifyAPI::Errors::ContextNotSetupError) do + ShopifyAPI::Auth::ClientCredentials.client_credentials(shop: @shop) + end + end + + def test_client_credentials_offline_token + stub_request(:post, "https://#{@shop}/admin/oauth/access_token") + .with(body: @client_credentials_request) + .to_return(body: @offline_token_response.to_json, headers: { content_type: "application/json" }) + expected_session = ShopifyAPI::Auth::Session.new( + id: "offline_#{@shop}", + shop: @shop, + access_token: @offline_token_response[:access_token], + scope: @offline_token_response[:scope], + is_online: false, + expires: @stubbed_time_now + @offline_token_response[:expires_in].to_i, + ) + + session = ShopifyAPI::Auth::ClientCredentials.client_credentials(shop: @shop) + + assert_equal(expected_session, session) + end + end + end +end diff --git a/test/auth/session_test.rb b/test/auth/session_test.rb index 4f55d3923..065b43433 100644 --- a/test/auth/session_test.rb +++ b/test/auth/session_test.rb @@ -79,7 +79,7 @@ def test_temp_with_block_var assert_equal(session, ShopifyAPI::Context.active_session) end - def test_from_with_offline_access_token_response + def test_from_with_offline_access_token_response_with_no_expires_in shop = "test-shop" response = ShopifyAPI::Auth::Oauth::AccessTokenResponse.new( access_token: "token", @@ -103,6 +103,30 @@ def test_from_with_offline_access_token_response assert_equal(expected_session, session) end + def test_from_with_offline_access_token_response_with_expires_in + shop = "test-shop" + response = ShopifyAPI::Auth::Oauth::AccessTokenResponse.new( + access_token: "token", + scope: "scope1, scope2", + expires_in: 1000, + ) + + expected_session = ShopifyAPI::Auth::Session.new( + id: "offline_#{shop}", + shop: shop, + access_token: response.access_token, + scope: response.scope, + is_online: false, + associated_user_scope: nil, + associated_user: nil, + expires: Time.now + response.expires_in, + shopify_session_id: response.session, + ) + + session = ShopifyAPI::Auth::Session.from(shop: shop, access_token_response: response) + assert_equal(expected_session, session) + end + def test_from_with_online_access_token_response shop = "test-shop" associated_user = ShopifyAPI::Auth::AssociatedUser.new(