Solved

Storefront API authentication and the Web checkout

martinfotp
Shopify Partner
24 4 15

Hello

We're using the Storefront API for our shop. Customers login to the storefront using the `customerAccessTokenCreate` mutation. We then associate the checkout with the customer using the `checkoutCustomerAssociateV2` mutation. However when we direct the user to the web checkout, they're not authenticated.

If they click the login link, they're taken to the custom domain login page where they can authenticate using the themes engine store. This is not ideal as they have to authenticate twice.

From spelunking through the forums and various Github issues, this appears to be the case because of valid security concerns. Is this still the case?

I am also unsure what's the purpose of `checkoutCustomerAssociateV2` and `customerAccessTokenCreate` if when you come to the most important part of most shops: the checkout, you're asked to login again.

What's the recommended implementation for the Storefront API? Should we be using the Themes Engine for all authenticated views (account management, etc) and only use it to build the Storefront API to build the anonymous parts of our storefront?

 

 

Accepted Solution (1)
martinfotp
Shopify Partner
24 4 15

This is an accepted solution.

No worries Hector

We used this minimal theme as a starting point:

https://github.com/jaredkc/shopify-starter-theme

It uses webpack so the build process is very familiar to us. We have a CircleCI deployment pipeline setup. 

The relevant parts of our `theme.liquid` redirect logic is as follows (I've stripped everything else):

<!doctype html>
<html>
<head>
  <title>{{ page_title }}</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="description" content="{{ page_description | escape }}">
  <link rel="canonical" href="{{ canonical_url }}">
  <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">

  {%- comment -%}
  Detect what 'mode' the page is in. We cannot auto redirect under certain modes.
  {%- endcomment -%}
  {%- if request.design_mode -%}
    {%- assign page_mode = "design" -%}
  {%- elsif content_for_header contains "previewBarInjector.init();" -%}
    {%- assign page_mode = "preview" -%}
  {%- else -%}
    {%- assign page_mode = "live" -%}
  {%- endif -%}

  {%- comment -%}
  ## Headless router

  Re-route to the headless store by assigning the redirect path to `headless_redirect_path`.

  If a path is not assigned to then we will render the liquid template.
  {%- endcomment -%}
  {%- case request.page_type -%}
  {% when "404" %}
  {% when "article" %}
  {% when "blog" %}
  {% when "cart" %}
  {% assign headless_redirect_path = "/cart" %}
  {% when "collection" %}
  {% assign headless_redirect_path = "" %}
  {% when "list-collections" %}
  {% assign headless_redirect_path = "" %}
  {% when "customers/account" %}
  {% when "customers/activate_account" %}
  {% when "customers/addresses" %}
  {% when "customers/login" %}
  {% when "customers/order" %}
  {% when "customers/register" %}
  {% when "customers/reset_password" %}
  {% when "gift_card" %}
  {% when "index" %}
  {% assign headless_redirect_path = "" %}
  {% when "page" %}
  {% when "password" %}
  {% when "product" %}
  {% assign headless_redirect_path = "/products/" | append: handle %}
  {% when "search" %}
  {% assign headless_redirect_path = "" %}
  {% else %}
  {%- endcase -%}

  {%- assign headless_origin = settings.headless_origin -%}
  {%- assign headless_redirect_base_url = settings.headless_redirect_base_url -%}

  {%- comment -%} Construct the full headless path {%- endcomment -%}
  {%- if headless_redirect_path and settings.headless_redirect_enabled == true -%}
    {%- assign headless_redirect_url = headless_origin | append: headless_redirect_base_url | append: headless_redirect_path -%}
  {% endif %}

  {%- comment -%}We attempt JS redirect, if that fails, then a meta redirect with a fallback to a link.{%- endcomment -%}
  {%- if headless_redirect_url -%}
    {%- if page_mode == "live" -%}
      <meta content="noindex" name="robots">
      <script>
        (function (){
          var href = {{ headless_redirect_url | json }};
          if (window.location.search) {
            href += window.location.search;
          }
          if (window.location.hash) {
            href += window.location.hash;
          }
          window.location.href = href;
        })();
      </script>
      <style>
        #headless-manual-redirect {
            opacity: 0;
            animation: delayFadeIn 500ms;
            animation-delay: 2s;
            animation-fill-mode: forwards;
        }

        @keyframes delayFadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
      </style>
      <meta content="1;url={{ headless_redirect_url }}" http-equiv="refresh">
    {%- endif -%}
  {%- else -%}
    {% comment %} Additional head for Theme Engine pages here... {% endcomment %}
    {{ content_for_header }}
  {%- endif -%}
</head>
<body class="site">
  {%- if headless_redirect_url -%}
    <div
    id="headless-manual-redirect"
    style="align-items: center; display: flex; flex-direction: column; height: 100vh; justify-content: center; width: 100%;">
    <h1>{{ 'general.headless_redirect.title' | t }}</h1>
    <p>
      <a href="{{ headless_redirect_url }}">{{ 'general.headless_redirect.link' | t }}</a>
    </p>
    {%- if page_mode != "live" %}<em>auto-redirect from {{ headless_redirect_path }} to {{ headless_redirect_url }} skipped in {{ page_mode }} mode</em>{%- endif %}
  </div>
  {%- else -%}
    {%- section 'site-header' -%}

    <main role="main" class="site-content">
      {{ content_for_layout }}
    </main>

    {%- section 'site-footer' -%}
  {%- endif -%}

</body>
</html>

 

 Good luck!

View solution in original post

Replies 8 (8)

HectorLeon
Tourist
4 0 0

Hello @martinfotp ,
I am in your same situation, did you find a workaround about this ?

Thanks in advance

martinfotp
Shopify Partner
24 4 15

I believe that the solution is Multipass.

We came to the conclusion that we mainly cared about a headless storefront and less so about account management and authentication. Our solution without Multipass was to use the theme for authentication and account management.

Elevar built a headless site for StriVectin like this and there’s a public write up here:

https://www.getelevar.com/shopify/headless-shopify-learning-lessons/

We have the Shopify store on a sub domain of our headless site so that we can share cookies for analytics and we also set an “authenticated” cookie from the themes engine to tweak menus on our storefront and pass to analytics. We have a minimal theme with a redirect to our headless site for shop URLs. We use a JS redirect in the head (required to pass query string params as Liquid doesn’t have access to them), head meta redirect fallback after the JS (no query string params) and finally a 500ms delayed with css user message with redirect link in case those both fail. It’s actually rather seamless experience.

A lot of the Shopify and App Store ecosystem make assumptions about shop URLs so the added advantage of the redirects theme is that product page URLs that assume you use the theme still work. We’re also using ReCharge and it was a pain to work with their account subscription management APIs with a headless site so this method allows us to just use their out of the box account management.

 

HectorLeon
Tourist
4 0 0

Hello,
thank you so much for you answer @martinfotp 

I believe the same about Multipass, sadly Shopify Plus plan is not a viable option for starters stores.

I'm gonna read the article, and dig about redirect theme, thank you so much.

By now
GraphQL setup docs for getting products and built the PDP pages, home and collection pages is quite good.

Then the official docs start to be a little painful about Customer auth ( ogin etc.. ) and checkout process.




martinfotp
Shopify Partner
24 4 15

This is an accepted solution.

No worries Hector

We used this minimal theme as a starting point:

https://github.com/jaredkc/shopify-starter-theme

It uses webpack so the build process is very familiar to us. We have a CircleCI deployment pipeline setup. 

The relevant parts of our `theme.liquid` redirect logic is as follows (I've stripped everything else):

<!doctype html>
<html>
<head>
  <title>{{ page_title }}</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="description" content="{{ page_description | escape }}">
  <link rel="canonical" href="{{ canonical_url }}">
  <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">

  {%- comment -%}
  Detect what 'mode' the page is in. We cannot auto redirect under certain modes.
  {%- endcomment -%}
  {%- if request.design_mode -%}
    {%- assign page_mode = "design" -%}
  {%- elsif content_for_header contains "previewBarInjector.init();" -%}
    {%- assign page_mode = "preview" -%}
  {%- else -%}
    {%- assign page_mode = "live" -%}
  {%- endif -%}

  {%- comment -%}
  ## Headless router

  Re-route to the headless store by assigning the redirect path to `headless_redirect_path`.

  If a path is not assigned to then we will render the liquid template.
  {%- endcomment -%}
  {%- case request.page_type -%}
  {% when "404" %}
  {% when "article" %}
  {% when "blog" %}
  {% when "cart" %}
  {% assign headless_redirect_path = "/cart" %}
  {% when "collection" %}
  {% assign headless_redirect_path = "" %}
  {% when "list-collections" %}
  {% assign headless_redirect_path = "" %}
  {% when "customers/account" %}
  {% when "customers/activate_account" %}
  {% when "customers/addresses" %}
  {% when "customers/login" %}
  {% when "customers/order" %}
  {% when "customers/register" %}
  {% when "customers/reset_password" %}
  {% when "gift_card" %}
  {% when "index" %}
  {% assign headless_redirect_path = "" %}
  {% when "page" %}
  {% when "password" %}
  {% when "product" %}
  {% assign headless_redirect_path = "/products/" | append: handle %}
  {% when "search" %}
  {% assign headless_redirect_path = "" %}
  {% else %}
  {%- endcase -%}

  {%- assign headless_origin = settings.headless_origin -%}
  {%- assign headless_redirect_base_url = settings.headless_redirect_base_url -%}

  {%- comment -%} Construct the full headless path {%- endcomment -%}
  {%- if headless_redirect_path and settings.headless_redirect_enabled == true -%}
    {%- assign headless_redirect_url = headless_origin | append: headless_redirect_base_url | append: headless_redirect_path -%}
  {% endif %}

  {%- comment -%}We attempt JS redirect, if that fails, then a meta redirect with a fallback to a link.{%- endcomment -%}
  {%- if headless_redirect_url -%}
    {%- if page_mode == "live" -%}
      <meta content="noindex" name="robots">
      <script>
        (function (){
          var href = {{ headless_redirect_url | json }};
          if (window.location.search) {
            href += window.location.search;
          }
          if (window.location.hash) {
            href += window.location.hash;
          }
          window.location.href = href;
        })();
      </script>
      <style>
        #headless-manual-redirect {
            opacity: 0;
            animation: delayFadeIn 500ms;
            animation-delay: 2s;
            animation-fill-mode: forwards;
        }

        @keyframes delayFadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
      </style>
      <meta content="1;url={{ headless_redirect_url }}" http-equiv="refresh">
    {%- endif -%}
  {%- else -%}
    {% comment %} Additional head for Theme Engine pages here... {% endcomment %}
    {{ content_for_header }}
  {%- endif -%}
</head>
<body class="site">
  {%- if headless_redirect_url -%}
    <div
    id="headless-manual-redirect"
    style="align-items: center; display: flex; flex-direction: column; height: 100vh; justify-content: center; width: 100%;">
    <h1>{{ 'general.headless_redirect.title' | t }}</h1>
    <p>
      <a href="{{ headless_redirect_url }}">{{ 'general.headless_redirect.link' | t }}</a>
    </p>
    {%- if page_mode != "live" %}<em>auto-redirect from {{ headless_redirect_path }} to {{ headless_redirect_url }} skipped in {{ page_mode }} mode</em>{%- endif %}
  </div>
  {%- else -%}
    {%- section 'site-header' -%}

    <main role="main" class="site-content">
      {{ content_for_layout }}
    </main>

    {%- section 'site-footer' -%}
  {%- endif -%}

</body>
</html>

 

 Good luck!

rickolas
Shopify Partner
2 0 0

Using Multipass implies that you have a separate users database and authentication system, which is not necessarily the case if you just have a statically generated storefront built with the storefront API. Using the checkout workflow, you can use `checkoutShippingAddressUpdateV2` to pass customer details to the checkout so a customer with a valid access token can hit step 2 of the checkout immediately. They are still not "logged in" though. Additionally, with the release of the cart workflow, which is required for subscriptions, there is no mechanism to do that (see open GitHub issue).

PabloGa
Tourist
3 0 1

Hi,
We've developed a frontend web app using Storefront API.
We can do user registration, login, etc but we cannot create an authenticated checkout.
When user is logged in in our web app, appears not logged in the checkout, but when the order is complete it's associated to the user.

 

We've tried both "cartCreate" and "checkoutCreate" APIs.

 

With "cartCreate" API we cannot assign "deliveryAddressPreferences" to the "CartBuyerIdentity".
We tried also with "cartBuyerIdentityUpdate" but it didn't work.

 

With "checkoutCreate" instead we can send the shipping address to the checkout and when the user arrive to the chekout the address is filled.
With "checkoutCustomerAssociateV2" we associate the user to the checkout object but the user is stil not logged in the checkout page.

 

Are there any solutions?

Is it possible to create a logout where the user is logged in, if we login with Storefront API?

rickolas
Shopify Partner
2 0 0

If you use the checkout API you can use `checkoutShippingAddressUpdateV2` so at least the user does not have to enter their address again. They are still not authenticated in this case, however. 

 

With cart API there is no way to do it other than passing the address fields in a URL query string to the checkout.

 

Unfortunately, considering how much Shopify is pushing headless commerce and the storefront API, there are some serious gaps. It should be at parity with liquid and any features that are added to liquid should also be available on the storefront API.

Liquidator3358
Explorer
44 1 15

Is this 100% confirmed?  If so, I'm not worried about them being authenticated.  Like you, I have them automatically going to step 2 of the checkout process.  As long as the user's order is properly attached to the contact email, I think I can live with that.  It's not ideal having the login link listed but I believe that can be removed.