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

Invalid CSRF Token in incognito mode #1219

Closed
dianedouglas-thrive opened this issue Mar 15, 2021 · 14 comments
Closed

Invalid CSRF Token in incognito mode #1219

dianedouglas-thrive opened this issue Mar 15, 2021 · 14 comments

Comments

@dianedouglas-thrive
Copy link

Description

When submitting a form in incognito mode in an embedded, multi-page app using jwt authentication I get an Invalid CSRF Token error in incognito mode.

Steps to Reproduce

  1. Submit a form with a post request to create a db record in incognito mode, using a separate controller from the home controller used for basic CRUD actions.
  2. Controller inherits from ApplicationController and includes ShopifyApp::EmbeddedApp.
  3. Controller was generated with standard rails scaffold command: rails g scaffold Thing shop:references path:string
  4. In a regular browser window the form to create the Thing is permitted but in an incognito window I get a "Can't verify CSRF token authenticity."

Expected behavior:

The app should just confirm the jwt/session token and then skip CSRF token validation since (as far as I understand it) incognito blocks 3rd party cookies.

Actual behavior:

I understand this behavior should be coming from using include ShopifyApp::Authenticated. But if I do that then my app redirects a bunch of times and I land back on the admin/apps page with an error:

The app couldn’t be loaded. This app can’t load due to an issue with browser cookies. Try enabling cookies in your browser, switching to another browser, or contacting the developer to get support.

Additional information regarding the redirect error caused by ShopifyApp:Authenticated:

  • This redirect behavior happens regardless of incognito mode or not, on any controller action.
  • In trying to implement the behavior manually to skip the csrf validation as long as the jwt is there, I noticed that in both incognito and regular mode in my controller request.env['jwt.shopify_domain'] is always nil.
  • I am using the standard jwt implementation with turbolinks because when started the app I used the default shopify_app generator and when I generated the app I was on 17.0.2.

Possible solution

I made some progress with this and found that using this on my controller bypasses the incognito CSRF error.

protect_from_forgery with: :null_session, prepend: true

But I am unsure if this is the correct way to go about it.

Reproduces how often:

Every time.

Browsers

Chrome, incognito.

Gem versions

Shopify_app version 17.0.5 and rails version 6.0.3.4

Additional Information

The app uses session tokens, it is an embedded app, created with default generator on version 17.0.2.

@rezaansyed
Copy link
Contributor

@dianedouglas-thrive, how often are you fetching the JWT when making the POST request? JWTs expire after 1 minute and you should be retrieving a session token prior to each request. You can use the getSessionToken utility to do this.

The request.env['jwt.shopify_domain'] leads me to believe that an invalid/expired session token (JWT) is being passed in.

@NabeelAhsen
Copy link
Contributor

Do you attach a fresh session token to your POST submission? You can verify whether the session token that is passed to your backend is expired or not using jwt.io.

I think that your controller should inherit from AuthenticatedController, since it includes the CsrfProtection concern for you. Determining why the session tokens can't be decoded is a separate problem:

  1. As mentioned above, try to capture some information about the session token using your browser's network activity tab in dev tools
  2. On your app server, watch out for the following logs: https://github.com/Shopify/shopify_app/blob/master/docs/Troubleshooting.md#inspect-server-logs
    These indicate that something went wrong when trying to decode your session token.

Update us on your findings!

@dianedouglas-thrive
Copy link
Author

dianedouglas-thrive commented Mar 15, 2021

Hi guys, thanks for your time.

On your app server, watch out for the following logs:

Happily on my app server logs I am not seeing any Failed to validate JWT errors. When I inherit from AuthenticatedController or include it, I just see a bunch of 302 redirects until it hits /auth and then drops me back on the admin/apps page.

try to capture some information about the session token using your browser's network activity tab in dev tools

Apologies if I am missing something basic here, I'm pretty new to dealing with jwt - looked at my network activity and I do see a whole bunch of requests hitting app-bridge and index.js but I'm not really sure what I'm looking for. I don't see where I am supposed to copy and paste the token into jwt.io.

you should be retrieving a session token prior to each request.
Do you attach a fresh session token to your POST submission?

I have left the generated code pretty untouched here as far as anything app bridge or jwt related. So I have not specifically added anything for attaching a fresh session token to my Post request, or retrieving a session token prior to each request. Here is what I do have:

In my shopify_app.js:

document.addEventListener('DOMContentLoaded', () => {
  var data = document.getElementById('shopify-app-init').dataset;
  var AppBridge = window['app-bridge'];
  var createApp = AppBridge.default;
  window.app = createApp({
    apiKey: data.apiKey,
    shopOrigin: data.shopOrigin,
  });

  var actions = AppBridge.actions;
  var TitleBar = actions.TitleBar;
  TitleBar.create(app, {
    title: data.page,
  });
});

And in embedded_app.html.erb:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <% application_name = ShopifyApp.configuration.application_name %>
    <title><%= application_name %></title>
    <%= stylesheet_link_tag 'application' %>
    <% if ShopifyApp.use_webpacker? %>
      <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <% else %>
      <%= javascript_include_tag 'application', "data-turbolinks-track" => true %>
    <% end %>
    <%= csrf_meta_tags %>
  </head>

  <body>
    <div class="app-wrapper">
      <div class="app-content">
        <main role="main">
          <%= yield %>
        </main>
      </div>
    </div>

    <%= render 'layouts/flash_messages' %>

    <script src="https://unpkg.com/@shopify/app-bridge"></script>

    <%= content_tag(:div, nil, id: 'shopify-app-init', data: {
      api_key: ShopifyApp.configuration.api_key,
      shop_origin: @shop_origin || (@current_shopify_session.domain if @current_shopify_session),
      debug: Rails.env.development?
    } ) %>

    <% if content_for?(:javascript) %>
      <div id="ContentForJavascript" data-turbolinks-temporary>
        <%= yield :javascript %>
      </div>
    <% end %>
  </body>
</html>

I also saw that this was needed in my initial application home page to make installation work, but I tried adding this to the /new view on my other controller as well as the embedded_app view and it had not effect on the behavior.

     <script src="https://unpkg.com/@shopify/app-bridge-utils"></script>
    <script>
      document.addEventListener("DOMContentLoaded", async function() {
        var SessionToken = window["app-bridge"].actions.SessionToken
        var app = window.app;

        app.dispatch(
          SessionToken.request(),
        );

        // Save a session token for future requests
        window.sessionToken = await new Promise((resolve) => {
          app.subscribe(SessionToken.ActionType.RESPOND, (data) => {
            resolve(data.sessionToken || "");
          });
        });
      });
    </script>

Am I missing the code where I should be retrieving or refreshing a session token? Is that why anything in a controller inheriting from AuthenticatedController is redirecting?

@dianedouglas-thrive
Copy link
Author

I guess one further piece of information is that I've been manually passing around the shop domain as a parameter under shop on all my requests in order to authenticate with the api. This seems to work, but is headache inducing and I am starting to suspect it's the wrong way to go about this?


class ApplicationController < ActionController::Base

  private
    def set_shop
      @shop_origin = shopify_domain
      Rails.logger.info("Shop domain found: #{@shop_origin}")
      if @shop.class != Shop
        @shop = Shop.where(shopify_domain: @shop_origin).first
      end
    end

    # taken from shopify_app gem
    def shopify_domain
      return if params[:shop].blank?
      @shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
    end
end

This allows me to initialize a session with the API using the shop:

session = ShopifyAPI::Session.new(
        domain: shop.shopify_domain,
        token: shop.shopify_token, 
        api_version: API_VERSION
      )
      ShopifyAPI::Base.activate_session(session)

@NabeelAhsen
Copy link
Contributor

NabeelAhsen commented Mar 15, 2021

Is your app a single-page app or is it a server-side rendered Rails app (using erbs for example)?

If it's the latter, this tutorial might help you with refreshing your session tokens: https://shopify.dev/tutorials/authenticate-server-side-rendered-embedded-apps-using-rails-and-turbolinks


Passing a shop parameter around is needed to help secure your controllers to a small degree. You'll notice that your HomeController has the ShopifyApp::RequireKnownShop parameter. This does not help authenticate with the Shopify API nor that your requests are coming from trusted sources.

This requirement is just needed so that your HomeController has some semblance of requests coming from the Shopify Admin when it's embedded. You will have to manually pass this around if your app is a multi-page server-side rendered app.

@dianedouglas-thrive
Copy link
Author

It is a server-side rendered Rails app. I've been looking at that tutorial and it's a little complicated because I was not intending to use a splash page. Is the splash page needed? Also I thought all this stuff was included in the default generator. I guess there must be some stuff I still need to add.

@dianedouglas-thrive
Copy link
Author

You will have to manually pass this around if your app is a multi-page server-side rendered app.

Oh ok, well that's fine then. If that's what's intended no problem there.

@NabeelAhsen
Copy link
Contributor

NabeelAhsen commented Mar 15, 2021

It is a server-side rendered Rails app. I've been looking at that tutorial and it's a little complicated because I was not intending to use a splash page. Is the splash page needed? Also I thought all this stuff was included in the default generator. I guess there must be some stuff I still need to add.

Unfortunately the default generator does not configure Turbolinks to refresh and cache session tokens for you - this is a decision left to the app developer.

The path with the least headaches right now is to build a single-page application that uses App Bridge fetch methods (these always make sure your outbound requests have fresh session tokens), but it's understandable that not all developers would want to do this. This is partially why there exists a Turbolinks solution

Edit: here's a sample app you can refer to that has Turbolinks configured and multiple pages that pass around the shop parameter: https://github.com/Shopify/turbolinks-jwt-sample-app

@dianedouglas-thrive
Copy link
Author

It is too late for me to go back and change to a single page app I think, I'll have to go with the turbolinks solution. I'll try to follow the tutorial/app you have linked more closely. Two things

  1. Is the splash page before the home controller needed?
  2. I'm curious have you tested the widgets form submission for this app in incognito mode? Generally I thought my stuff was working but then hit all sorts of errors in incognito mode.

@rezaansyed
Copy link
Contributor

rezaansyed commented Mar 15, 2021

  1. Is the splash page before the home controller needed?

Yes, the splash page is to allow you to load a skeleton page with the initial JS to render App Bridge and get your session token. If you're navigating to a new view of a different controller, the JS served will need to load App Bridge again and the splash page helps with this.

@dianedouglas-thrive
Copy link
Author

Thanks @rezaansyed, sorry for the delay in my response. Is the intention to have the splash page load between every controller? Or just once at the beginning to get the session token, and then any subsequent controller views also need to repeat this process to get a new session token?

@asecondwill
Copy link

@rezaansyed / @NabeelAhsen

There are two concerning tickets in that demo app.

Do all requests have to be XHR?
Shopify/turbolinks-jwt-sample-app#11

Does it work reliably?
Shopify/turbolinks-jwt-sample-app#26

Can it be right that there is no way to have a non-embedded regular rails app as a shopify_app?

@github-actions
Copy link

github-actions bot commented Oct 8, 2022

This issue is stale because it has been open for 90 days with no activity. It will be closed if no further action occurs in 14 days.

@github-actions github-actions bot added the stale label Oct 8, 2022
@github-actions
Copy link

We are closing this issue because it has been inactive for a few months.
This probably means that it is not reproducible or it has been fixed in a newer version.
If it’s an enhancement and hasn’t been taken on since it was submitted, then it seems other issues have taken priority.

If you still encounter this issue with the latest stable version, please reopen using the issue template. You can also contribute directly by submitting a pull request– see the CONTRIBUTING.md file for guidelines

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants