Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing online token creation #1413

Merged
merged 5 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Unreleased
----------
* Fix regression in apps using online tokens. [#1413](https://github.com/Shopify/shopify_app/pull/1413)

19.0.1 (April 11, 2022)
----------
* Bump Shopify API (https://github.com/Shopify/shopify_api) to version 10.0.2. This update includes patch fixes since the initial v10 release.
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/shopify_app/callback_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ def callback
value: auth_result[:cookie].value,
}

session[:shopify_user_id] = auth_result[:session].associated_user.id if auth_result[:session].online?

if start_user_token_flow?(auth_result[:session])
return respond_with_user_token_flow
end

perform_post_authenticate_jobs(auth_result[:session])

respond_successfully
Expand All @@ -43,6 +49,25 @@ def respond_with_error
redirect_to(login_url_with_optional_shop)
end

def respond_with_user_token_flow
redirect_to(login_url_with_optional_shop)
end

def start_user_token_flow?(shopify_session)
return false unless ShopifyApp::SessionRepository.user_storage.present?
return false if shopify_session.online?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does shopify_session.online? check whether the response if for an online token?

Copy link
Contributor

@hannachen hannachen Apr 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe is_online is defined on shopify-api. I would keep online? as I'm getting an undefined error when tophatting.

Copy link
Contributor

@hannachen hannachen Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I misread it! @paulomarg, do you have additional context for this part? From my understanding, if the app is requesting an online token, then the merchant will be taken through the auth process twice starting with an offline token. If the last token received is an online token, then we don't initiate a redirect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe that is the original behaviour here - if we want online tokens, we go through OAuth twice in a row to get both of them.

update_user_access_scopes?
end

def update_user_access_scopes?
return true if session[:shopify_user_id].nil?
user_access_scopes_strategy.update_access_scopes?(shopify_user_id: session[:shopify_user_id])
end

def user_access_scopes_strategy
ShopifyApp.configuration.user_access_scopes_strategy
end

def perform_post_authenticate_jobs(session)
install_webhooks(session)
install_scripttags(session)
Expand Down
6 changes: 6 additions & 0 deletions lib/shopify_app/controller_concerns/login_protection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,13 @@ def session_shop_conflicts_with_params
current_shopify_session && params[:shop].is_a?(String) && current_shopify_session.shop != params[:shop]
end

def shop_session
ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(sanitize_shop_param(params))
end

def user_session_expected?
return false if shop_session.nil?
return false if ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_session.shop)
!ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/shopify_app/session/session_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def user_storage
# ShopifyAPI::Auth::SessionStorage override
def store_session(session)
if session.online?
user_storage.store(session, session.associated_user.id.to_s)
user_storage.store(session, session.associated_user)
else
shop_storage.store(session)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module UserSessionStorageWithScopes

class_methods do
def store(auth_session, user)
user = find_or_initialize_by(shopify_user_id: user[:id])
user = find_or_initialize_by(shopify_user_id: user.id)
user.shopify_token = auth_session.access_token
user.shopify_domain = auth_session.shop
user.access_scopes = auth_session.scope.to_s
Expand Down
23 changes: 21 additions & 2 deletions test/controllers/sessions_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ class SessionsControllerTest < ActionController::TestCase
assert_redirected_to "/auth-route"
end

test "#new starts OAuth requesting online token if user session is expected" do
test "#new starts OAuth requesting offline token if user session is expected but there is no shop session" do
ShopifyApp::SessionRepository.user_storage = ShopifyApp::InMemoryUserSessionStore

ShopifyAPI::Auth::Oauth.expects(:begin_auth)
.with(shop: "my-shop.myshopify.com", redirect_path: "/auth/shopify/callback", is_online: true)
.with(shop: "my-shop.myshopify.com", redirect_path: "/auth/shopify/callback", is_online: false)
.returns({
cookie: ShopifyAPI::Auth::Oauth::SessionCookie.new(value: "", expires: Time.now),
auth_route: "/auth-route",
Expand All @@ -110,6 +110,25 @@ class SessionsControllerTest < ActionController::TestCase
get :new, params: { shop: "my-shop", top_level: true }
end

test "#new starts OAuth requesting online token if user session is expected and there is a shop session" do
shop = "my-shop.myshopify.com"

ShopifyApp::SessionRepository.user_storage = ShopifyApp::InMemoryUserSessionStore
ShopifyApp::SessionRepository.shop_storage = ShopifyApp::InMemoryShopSessionStore
ShopifyApp::SessionRepository.shop_storage.stubs(:retrieve_by_shopify_domain)
.with(shop)
.returns(mock_session(shop: shop))

ShopifyAPI::Auth::Oauth.expects(:begin_auth)
.with(shop: shop, redirect_path: "/auth/shopify/callback", is_online: true)
.returns({
cookie: ShopifyAPI::Auth::Oauth::SessionCookie.new(value: "", expires: Time.now),
auth_route: "/auth-route",
})

get :new, params: { shop: shop, top_level: true }
end

test "#new starts OAuth requesting online token if user session is unexpected" do
ShopifyAPI::Auth::Oauth.expects(:begin_auth)
.with(shop: "my-shop.myshopify.com", redirect_path: "/auth/shopify/callback", is_online: false)
Expand Down
7 changes: 7 additions & 0 deletions test/dummy/config/initializers/shopify_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ class ShopifyAppConfigurer
def self.call
ShopifyApp.configure do |config|
config.api_key = "key"
config.old_secret = nil
config.secret = "secret"
config.scope = "read_orders, read_products"
config.shop_access_scopes = nil
config.user_access_scopes = nil
config.embedded_app = true
config.myshopify_domain = "myshopify.com"
config.api_version = :unstable

config.shop_session_repository = ShopifyApp::InMemorySessionStore
config.after_authenticate_job = false
config.reauth_on_access_scope_changes = true
end
end
end
Expand Down
36 changes: 31 additions & 5 deletions test/shopify_app/controller_concerns/login_protection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ class LoginProtectionControllerTest < ActionController::TestCase
end

test "#current_shopify_session loads online session if user session expected" do
shop = "my-shop.myshopify.com"
ShopifyApp::SessionRepository.shop_storage.stubs(:retrieve_by_shopify_domain)
.with(shop)
.returns(mock_session(shop: shop))

cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie"
request.headers["HTTP_AUTHORIZATION"] = "Bearer token"

Expand All @@ -72,7 +77,7 @@ class LoginProtectionControllerTest < ActionController::TestCase
.returns(nil)

with_application_test_routes do
get :index
get :index, params: { shop: shop }
end
end

Expand Down Expand Up @@ -161,6 +166,10 @@ class LoginProtectionControllerTest < ActionController::TestCase
end

test "#current_shopify_session redirects to login if the loaded session doesn't have enough scope" do
shop = "my-shop.myshopify.com"
ShopifyApp::SessionRepository.shop_storage.stubs(:retrieve_by_shopify_domain)
.with(shop)
.returns(mock_session(shop: shop))
ShopifyAPI::Context.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new(["scope1", "scope2"]))

cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie"
Expand All @@ -173,18 +182,22 @@ class LoginProtectionControllerTest < ActionController::TestCase
is_online: true
)
.returns(
ShopifyAPI::Auth::Session.new(shop: "some-shop", scope: ["scope1"])
ShopifyAPI::Auth::Session.new(shop: shop, scope: ["scope1"])
)

with_application_test_routes do
get :index
get :index, params: { shop: shop }

assert_redirected_to "/login"
assert_redirected_to "/login?shop=my-shop.myshopify.com"
assert_nil cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME]
end
end

test "#current_shopify_session does not redirect when sufficient scope" do
shop = "my-shop.myshopify.com"
ShopifyApp::SessionRepository.shop_storage.stubs(:retrieve_by_shopify_domain)
.with(shop)
.returns(mock_session(shop: shop))
ShopifyAPI::Context.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new(["scope1"]))

cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie"
Expand All @@ -201,7 +214,7 @@ class LoginProtectionControllerTest < ActionController::TestCase
)

with_application_test_routes do
get :index
get :index, params: { shop: shop }
assert_response :ok
assert_equal "cookie", cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME]
end
Expand Down Expand Up @@ -440,4 +453,17 @@ def with_custom_login_url(url)
ensure
ShopifyApp.configure { |config| config.login_url = original_url }
end

def mock_associated_user
ShopifyAPI::Auth::AssociatedUser.new(
id: 100,
first_name: "John",
last_name: "Doe",
email: "johndoe@email.com",
email_verified: true,
account_owner: false,
locale: "en",
collaborator: true
)
end
end
2 changes: 1 addition & 1 deletion test/shopify_app/session/session_repository_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class SessionRepositoryTest < ActiveSupport::TestCase
user = mock_associated_user
session.stubs(:associated_user).returns(user)

InMemoryUserSessionStore.expects(:store).with(session, user.id.to_s)
InMemoryUserSessionStore.expects(:store).with(session, user)

SessionRepository.store_session(session)
end
Expand Down
1 change: 1 addition & 0 deletions test/shopify_app/session/shop_session_storage_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ShopSessionStorageTest < ActiveSupport::TestCase
mock_auth_hash = mock
mock_auth_hash.stubs(:shop).returns(mock_shop_instance.shopify_domain)
mock_auth_hash.stubs(:access_token).returns("a-new-token!")
mock_auth_hash.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new("read_products,write_orders"))
saved_id = ShopMockSessionStore.store(mock_auth_hash)

assert_equal "a-new-token!", mock_shop_instance.shopify_token
Expand Down
1 change: 1 addition & 0 deletions test/shopify_app/session/user_session_storage_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class UserSessionStorageTest < ActiveSupport::TestCase
mock_auth_hash = mock
mock_auth_hash.stubs(:shop).returns(mock_user_instance.shopify_domain)
mock_auth_hash.stubs(:access_token).returns("a-new-user_token!")
mock_auth_hash.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new("read_products,write_orders"))

associated_user = {
id: 100,
Expand Down
32 changes: 22 additions & 10 deletions test/shopify_app/session/user_session_storage_with_scopes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,13 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase

UserMockSessionStoreWithScopes.stubs(:find_or_initialize_by).returns(mock_user_instance)

mock_auth_hash = mock
mock_auth_hash.stubs(:shop).returns(mock_user_instance.shopify_domain)
mock_auth_hash.stubs(:access_token).returns("a-new-user_token!")
mock_auth_hash.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new(TEST_MERCHANT_SCOPES))

associated_user = {
id: 100,
}

saved_id = UserMockSessionStoreWithScopes.store(mock_auth_hash, user: associated_user)
saved_id = UserMockSessionStoreWithScopes.store(
mock_session(
shop: mock_user_instance.shopify_domain,
scope: TEST_MERCHANT_SCOPES
),
mock_associated_user
)

assert_equal "a-new-user_token!", mock_user_instance.shopify_token
assert_equal mock_user_instance.id, saved_id
Expand Down Expand Up @@ -99,5 +96,20 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase
UserMockSessionStoreWithScopes.retrieve(1)
end
end

private

def mock_associated_user
ShopifyAPI::Auth::AssociatedUser.new(
id: 100,
first_name: "John",
last_name: "Doe",
email: "johndoe@email.com",
email_verified: true,
account_owner: false,
locale: "en",
collaborator: true
)
end
end
end
10 changes: 10 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,17 @@ def before_setup
super
WebMock.disable_net_connect!
WebMock.stub_request(:get, "https://app.shopify.com/services/apis.json").to_return(body: API_META_TEST_RESPONSE)
ShopifyApp::InMemorySessionStore.clear
ShopifyAppConfigurer.call
Rails.application.reload_routes!
end

def mock_session(shop: "my-shop.myshopify.com", scope: ShopifyApp.configuration.scope)
mock_session = mock
mock_session.stubs(:shop).returns(shop)
mock_session.stubs(:access_token).returns("a-new-user_token!")
mock_session.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new(scope))

mock_session
end
end