Shopify_app: How to re-auth shop for new scope?

Created on 19 Apr 2018  路  13Comments  路  Source: Shopify/shopify_app

I'm trying to add some custom code to make the user/shop re-authenticate when the scope changes.

It's related to this point from #350:

Keep track of extra information returned by Shopify, such as scope & associated user. Currently this app has no way to keep track of this, it only keeps the access token. Keeping track of the scope would facilitate the workflow when updating the application's requested scope. We should force a new oauth autorization when the needed access scope change.

Here's a little code snippet:

current_scope = ShopifyAPI::AccessScope.all.map(&:handle)
new_scope = ShopifyApp.configuration.scope.split(', ')
if current_scope.sort != new_scope.sort
  # what to do here?
end

I'm stuck as to how to redirect to re-auth path. I've tried all of these (and several more):

  set_esdk_headers # response.set_header('P3P', 'CP="Not used"'); response.headers.except!('X-Frame-Options')
  fullpage_redirect_to "/auth/shopify?shop=#{@shop.shopify_domain}"
  fullpage_redirect_to "https://#{@shop.shopify_domain}/admin/oauth/authorize?client_id=#{ENV['SHOPIFY_API_KEY']}&redirect_uri=...&response_type=code&scope=write_themes%2Cread_orders&state=#{? SecureRandom.hex ?}"
  params[:shop] = @shop.shopify_domain
  close_session
  session[:shopify] = nil
  session[:shopify_domain] = nil
  redirect_to_login
  fullpage_redirect_to "/auth/shopify?shop=#{@shop.shopify_domain}"

What I'm trying to do is logout user and redirect him to reauthenticate. If it's not needed, we could skip logout. But I'm generally stuck as to where to redirect to. I understand this might not be shopify_app specific question, but omniauth / oauth specific question. I'm also going through the omniauth documentation, but I still can't figure out what should the redirect url be.
I restarted the app after changing the scope so my thought was that redirecting to /auth/shopify would trigger new scope.

Most helpful comment

Hey @davidwparker!

I created a new request, kind-of like on the first app install:

https://{shop}.myshopify.com/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}&grant_options[]={access_mode}

Here's the code snippet:

          set_esdk_headers # from shopify_app gem
          session['omniauth.state'] = current_shop.scope_nonce # you need to set some state/nonce that you can read later to confirm that's that request
          url = "https://#{current_shop.shopify_domain}/admin/oauth/authorize"
          request_params = []
          request_params << "client_id=#{ENV.fetch('SHOPIFY_API_KEY')}"
          request_params << "scope=#{ERB::Util.url_encode(new_scope.join(','))}"
          request_params << "redirect_uri=https://#{ENV.fetch('APP_DOMAIN')}/auth/shopify/callback"
          request_params << "state=#{current_shop.scope_nonce}"
          fullpage_redirect_to "#{url}?#{request_params.join('&')}"

current_shop is my own helper for getting the access to the...current logged in shop obviously :)

All 13 comments

There is only one way to ask for permissions: https://help.shopify.com/api/getting-started/authentication/oauth#step-2-ask-for-permission whether you ask for the the first time or 100th time. You can ask for more or less scopes in subsequent authorizations.

@EiNSTeiN- THANKS!!! I got it working finally. :)

@vfonic - what did you end up doing?

Hey @davidwparker!

I created a new request, kind-of like on the first app install:

https://{shop}.myshopify.com/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}&grant_options[]={access_mode}

Here's the code snippet:

          set_esdk_headers # from shopify_app gem
          session['omniauth.state'] = current_shop.scope_nonce # you need to set some state/nonce that you can read later to confirm that's that request
          url = "https://#{current_shop.shopify_domain}/admin/oauth/authorize"
          request_params = []
          request_params << "client_id=#{ENV.fetch('SHOPIFY_API_KEY')}"
          request_params << "scope=#{ERB::Util.url_encode(new_scope.join(','))}"
          request_params << "redirect_uri=https://#{ENV.fetch('APP_DOMAIN')}/auth/shopify/callback"
          request_params << "state=#{current_shop.scope_nonce}"
          fullpage_redirect_to "#{url}?#{request_params.join('&')}"

current_shop is my own helper for getting the access to the...current logged in shop obviously :)

This solved my issue too, thanks for posting your solution!

@vfonic Thanks so much for this code snippet, I've couple of questions,

  1. Where do you read/check omniauth.state?
  2. Do I need to make any changes to this snippet to account for same_site_cookies? My app won't load because of this oauth_error=same_site_cookies

Hey @milkbottlelough care to share the steps of making this work for you? I'm stuck on this and I'd really appreciate help on this 馃槄 TIA 馃檹

Here's the code snippet that I use for updating scopes:

set_esdk_headers
session['omniauth.state'] = shop.nonce
url = "https://#{shop.shopify_domain}/admin/oauth/authorize"
request_params = []
request_params << "client_id=#{ENV.fetch('SHOPIFY_API_KEY')}"
request_params << "scope=#{ERB::Util.url_encode(new_scope)}"
request_params << "redirect_uri=https://#{ENV.fetch('APP_DOMAIN')}/auth/shopify/callback"
request_params << "state=#{shop.nonce}"
fullpage_redirect_to "#{url}?#{request_params.join('&')}"

Use this with an "authenticated controller", the one that has: include ShopifyApp::Authenticated or inherits from AuthenticatedController

EDIT: I just realized I already posted this. :) I don't know any more details. I haven't had the need to change the scope for several years now. :) But that's the main part: you have to redirect back to Shopify informing Shopify that user needs to approve the new scope.

@vfonic Thanks so so much, I really appreciate your comment 馃檹鉂わ笍

It's working now! I guess the code wasn't in the right place.

Here is my setup based on your snippet:
SettingsController is my root controller.

class SettingsController < AuthenticatedController
  before_action :set_shop
  before_action :check_scope_and_reauth

  ...

  def check_scope_and_reauth
    shop_remote_scope = ShopifyAPI::AccessScope.find(:all).map(&:handle).sort
    app_scope = ShopifyApp.configuration.scope.split(',').sort

    if shop_remote_scope != app_scope
      set_esdk_headers

      session['omniauth.state'] = @shop.scope_nonce
      url = "https://#{current_shop.shopify_domain}/admin/oauth/authorize"
      request_params = []
      request_params << "client_id=#{ENV.fetch('SHOPIFY_APP_API_KEY')}"
      request_params << "scope=#{ERB::Util.url_encode(app_scope)}"
      request_params << "redirect_uri=https://#{ENV.fetch('HOST_URL')}/auth/shopify/callback"
      request_params << "state=#{@shop.scope_nonce}"
      fullpage_redirect_to "#{url}?#{request_params.join('&')}"
    end
  end

  ...

end

What I'm still not sure about is the state/nonce, I'm not doing anything to check on this yet the code works, am I missing something? Or does shopify_app gem magically takes care of it? 馃

I can't get this to work anymore now with the same site cookies update from Chrome earlier this year. Does anyone know what else you need to get this to work?

  • I'm using newest shopify_app gem (15.0.0), according to https://shopify.dev/tutorials/migrate-your-app-to-support-samesite-cookies that should be good.
  • Using the exact same code as in this thread to update scope, and have updated scope in the past.
  • The app works fine, until I try to update the scope (adding a new feature), then I get the error in Shopify apps section saying "This app can鈥檛 load due to an issue with browser cookies..."

Been blowing hours on this 馃槀 would love a tip of wisdom from someone that cracked the code.

Figured out that the shopify app will automatically redirect to the auth page with the new scopes - if the current session expires.

This lead me to the work around below. Inspired by you guys. Not battle-tested yet, it might be relevant tho.

    def check_scope_and_reauth
      return unless current_shop

      shop_remote_scope = ShopifyAPI::AccessScope.find(:all).map(&:handle).sort
      app_scope = ShopifyApp.configuration.scope.split(',').sort

      if shop_remote_scope != app_scope
        shop = Shop.find_by(shopify_domain: current_shop.shopify_domain)
        shop.shopify_token = nil
        shop.save

        fullpage_redirect_to("/login?shop=#{shop.shopify_domain}")
      end
    end

Figured out that the shopify app will automatically redirect to the auth page with the new scopes - if the current session expires.

This lead me to the work around below. Inspired by you guys. Not battle-tested yet, it might be relevant tho.

    def check_scope_and_reauth
      return unless current_shop

      shop_remote_scope = ShopifyAPI::AccessScope.find(:all).map(&:handle).sort
      app_scope = ShopifyApp.configuration.scope.split(',').sort

      if shop_remote_scope != app_scope
        shop = Shop.find_by(shopify_domain: current_shop.shopify_domain)
        shop.shopify_token = nil
        shop.save

        fullpage_redirect_to("/login?shop=#{shop.shopify_domain}")
      end
    end

This is great. I actually don't think you even need to delete the shopify_token though. The login URL seems to still work.

def check_scope
      @shop_remote_scope = ShopifyAPI::AccessScope.find(:all).map(&:handle).sort
      @app_scope = ShopifyApp.configuration.scope.split(',').sort
      @shop_remote_scope != @app_scope ? @out_of_date_scope = true : @db_shop.update(updated_scope: DateTime.now)
      if @out_of_date_scope
         @update_scope_URL = "Xxxx/login?shop=#{@db_shop.shopify_domain}&redirect_back=/setup&update_scope=true" if Rails.env.development?
         @update_scope_URL = "Xxxxx/login?shop=#{@db_shop.shopify_domain}}&redirect_back=/setup&update_scope=true" if Rails.env.production?
      end
end

Then because I'm using JWT tokens with Turbolinks, I render this is in my js.erb file to redirect.

<% if @update_scope_URL %>
    top.location.href="<%= @update_scope_URL %>"
<% end %>

Something that caught me out: In your shopify_app.rb file, you can't have any spaces between your commas and scopes or shop_remote_scope and app_scope won't be the same.

@oliwoodsuk Neat!
Keep in mind that the solution taps into your request quote - so make sure not to check on every incoming request. I learned this the hard way 馃槄

Was this page helpful?
0 / 5 - 0 ratings