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

Skip CSRF check if a valid JWT is passed in #994

Merged
merged 2 commits into from
Jun 4, 2020
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
1 change: 1 addition & 0 deletions app/controllers/concerns/shopify_app/authenticated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Authenticated
included do
include ShopifyApp::Localization
include ShopifyApp::LoginProtection
include ShopifyApp::CsrfProtection
include ShopifyApp::EmbeddedApp
before_action :login_again_if_different_user_or_shop
around_action :activate_shopify_session
Expand Down
1 change: 1 addition & 0 deletions lib/shopify_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def self.use_webpacker?
require 'shopify_app/utils'

# controller concerns
require 'shopify_app/controller_concerns/csrf_protection'
require 'shopify_app/controller_concerns/localization'
require 'shopify_app/controller_concerns/itp'
require 'shopify_app/controller_concerns/login_protection'
Expand Down
15 changes: 15 additions & 0 deletions lib/shopify_app/controller_concerns/csrf_protection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true
module ShopifyApp
module CsrfProtection
extend ActiveSupport::Concern
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you're using ActiveSupport::Concern, you can just include LoginProtection here. You don't need to do the ancestors check.

See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure that's a good idea. CsrfProtection should ideally have nothing to do with LoginProtection. They serve different purposes. If you want, I can have CsrfProtection access the jwt_shopify_domain directly from the request environment.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the middleware exposes that interface right now, so it doesn't bother me if you access the env directly.

My feedback is that under the current approach you have a dependency on LoginProtection, and it's better to use the tools that ActiveSupport::Concern gives you to enforce that dependency than to build your own thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to do something more duck-typing-esque? Like, put some defensive code around the reference to jwt_shopify_domain to raise a meaningful error if it's not present?

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 think so, personally. IMO the interface is the keys added to the env. We can either access that interface directly or we can pull in a method we know accesses the interface correctly.

The jwt_shopify_domain method is not the interface and testing that a jwt_shopify_domain method of unknown origin is present is the wrong approach IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The thing is we don't know if the app is using JWT or cookie based auth (Especially due to the fallbacks where it can use either). So we default to the standard Rails CSRF error.

included do
protect_from_forgery with: :exception, unless: :valid_session_token?
end

private

def valid_session_token?
request.env['jwt.shopify_domain']
end
end
end
1 change: 1 addition & 0 deletions test/controllers/concerns/authenticated_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def index
test "includes all the needed concerns" do
AuthenticatedTestController.include?(ShopifyApp::Localization)
AuthenticatedTestController.include?(ShopifyApp::LoginProtection)
AuthenticatedTestController.include?(ShopifyApp::CsrfProtection)
AuthenticatedTestController.include?(ShopifyApp::EmbeddedApp)
end
end
52 changes: 52 additions & 0 deletions test/shopify_app/controller_concerns/csrf_protection_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class CsrfProtectionController < ActionController::Base
include ShopifyApp::LoginProtection
include ShopifyApp::CsrfProtection

def authenticity_token
render(json: { authenticity_token: form_authenticity_token })
end

def csrf_test
head(:ok)
end
end

class CsrfProtectionTest < ActionDispatch::IntegrationTest
setup do
@authenticity_protection = ActionController::Base.allow_forgery_protection
ActionController::Base.allow_forgery_protection = true
Rails.application.routes.draw do
get '/authenticity_token', to: 'csrf_protection#authenticity_token'
post '/csrf_protection_test', to: 'csrf_protection#csrf_test'
end
end

teardown do
ActionController::Base.allow_forgery_protection = @authenticity_protection
Rails.application.reload_routes!
end

test 'it raises an invalid authenticity token error if a valid session token or csrf token is not provided' do
assert_raises ActionController::InvalidAuthenticityToken do
post '/csrf_protection_test'
end
end

test 'it does not raise if a valid CSRF token was provided' do
get '/authenticity_token'

csrf_token = JSON.parse(response.body)['authenticity_token']

post '/csrf_protection_test', headers: { 'X-CSRF-Token': csrf_token }

assert_response :ok
end

test 'it does not raise if a valid session token was provided' do
post '/csrf_protection_test', env: { 'jwt.shopify_domain': "exampleshop.myshopify.com" }

assert_response :ok
end
end