Storefront Access Token Creation Issue #2073

Issue summary

The storefrontAccessTokenCreate mutation is consistently returning ACCESS_DENIED, whereas it previously worked without issues. This behavior change occurred without any documented updates from Shopify.

Note: The issue persists even when testing with a new sales channel app.

Current App Scopes

read_content,read_markets,read_products,read_reports,unauthenticated_read_checkouts,unauthenticated_read_customer_tags,unauthenticated_read_customers,unauthenticated_read_product_listings,unauthenticated_read_product_tags,unauthenticated_read_selling_plans,unauthenticated_write_checkouts,write_customers,write_discounts,write_orders,write_themes

Expected Behavior

  • The mutation should successfully create and return a new storefront access token
  • If there are permission issues, the error response should provide actionable details about:
    • Why the access was denied
    • What permissions are missing
    • How to resolve the issue

Actual behavior

The mutation consistently returns ACCESS_DENIED without additional context.

Steps to reproduce the problem

  1. Install Shopify GraphQL app with all available scopes
  2. Execute the following mutation:
mutation StorefrontAccessTokenCreate($input: StorefrontAccessTokenInput!) {
  storefrontAccessTokenCreate(input: $input) {
    userErrors {
      field
      message
    }
    storefrontAccessToken {
      accessToken
    }
  }
}
  1. Observe the ACCESS_DENIED error response
4 Likes

Sorry not here to answer you question but was hoping if you could shine some light for me. So I have custom hydrogen application I want to implement my own custom auth like signing up the user through my own onboarding flow etc ofc I know graph QL admin has a bunch of mutations but I’m kind lf lost as to what orders things need to be done and the session. I saw there is a mutation called createCustomer and also createCustomerAccessToken I’m lost as in the order / flow of things essentially I have wall the inputs for the mutations but just unsure of the sequence of mutations I need to perform I would truly appreciate if you could take few minutes to answer me and I’ll keep looking into if i find something i will share too

I have the same problem, is there any solution?

I’m facing this issue as well, the documentation for Unauthenticated Storefront API access isn’t clear on which access scope you need.

The documentation for this mutation doesn’t share a clue either.

Hey! I just noticed this thread. I recently worked through some of these issues on a thread here: What permission is required to use storefrontAccessTokenCreate mutation in Shopify Admin API?

Can you follow some of those steps to see if it helps? As Dylan mentioned, the documentation isn’t very clear so I’ll see if we can get that updated as well.

1 Like

Yeah, since it seems like quite a few people are looking into this, i might as well just share the code that i use:

import {useState, useEffect} from 'react';
import {json, redirect} from '@shopify/remix-oxygen';
import {Form, useActionData} from '@remix-run/react';

// Define the page as public so it's accessible without authentication
export const handle = {
  isPublic: true,
};

export async function action({request, context}) {
  const {storefront, session, cart} = context;
  const formData = await request.formData();
  
  const firstName = formData.get('firstName');
  const lastName = formData.get('lastName');
  const email = formData.get('email');
  const password = formData.get('password');
  const passwordConfirm = formData.get('passwordConfirm');
  const acceptsMarketing = formData.has('acceptsMarketing');
  const phone = formData.get('phone') || undefined;
  
  // Add a hidden field for account type
  const accountType = 'consumer';
  
  // Form validation
  if (!email || !password || typeof email !== 'string' || typeof password !== 'string') {
    return json({error: 'Please provide both an email and a password.'}, {status: 400});
  }
  
  if (password !== passwordConfirm) {
    return json({error: 'Passwords do not match.'}, {status: 400});
  }
  
  try {
    // We'll store the account type in metafields or tags
    // For this example, let's use tags to identify account type
    const tags = ['consumer'];
    
    // Execute the customerCreate mutation
    const data = await storefront.mutate(
      CUSTOMER_CREATE_MUTATION,
      {
        variables: {
          input: {
            firstName,
            lastName,
            email,
            password,
            phone,
            acceptsMarketing,
            tags
          },
        },
      }
    );

    // Check for errors from the mutation
    if (data?.customerCreate?.customerUserErrors?.length) {
      const error = data.customerCreate.customerUserErrors[0];
      return json({error: error.message}, {status: 400});
    }

    if (!data?.customerCreate?.customer) {
      return json({error: 'Something went wrong. Please try again.'}, {status: 500});
    }

    // After successful registration, automatically log in the user
    const loginData = await storefront.mutate(
      CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION,
      {
        variables: {
          input: {
            email,
            password,
          },
        },
      }
    );

    if (loginData?.customerAccessTokenCreate?.customerUserErrors?.length) {
      const error = loginData.customerAccessTokenCreate.customerUserErrors[0];
      return json({error: error.message}, {status: 400});
    }

    const {customerAccessToken} = loginData.customerAccessTokenCreate;
    if (!customerAccessToken?.accessToken) {
      return json({error: 'Something went wrong. Please try again.'}, {status: 500});
    }

    // Store token in session
    session.set('customerAccessToken', customerAccessToken.accessToken);
    session.set('accountType', accountType);
    if (customerAccessToken.expiresAt) {
      session.set('customerAccessTokenExpiresAt', customerAccessToken.expiresAt);
    }

    // Initialize headers
    let headers = new Headers();
    
    try {
      // Create a new cart with buyer identity
      const result = await storefront.mutate(CART_CREATE_MUTATION, {
        variables: {
          input: {
            buyerIdentity: {
              customerAccessToken: customerAccessToken.accessToken,
            },
          },
        },
      });
      
      if (result?.cartCreate?.cart?.id) {
        // Set new cart ID in cookies
        headers = cart.setCartId(result.cartCreate.cart.id);
      }
    } catch (cartError) {
      console.error('Cart operation error:', cartError);
      // Continue with registration even if cart operations fail
    }

    // Add session cookie
    headers.append('Set-Cookie', await session.commit());

    // Redirect to account page
    return redirect('/', {headers});
  } catch (error) {
    console.error('Registration error:', error);

    return json(
      {error: 'An unexpected error occurred. Please try again.'},
      {status: 500}
    );
  }
}

export default function ConsumerRegister() {
  const actionData = useActionData();
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [passwordsMatch, setPasswordsMatch] = useState(true);

  // Reset isSubmitting state when actionData changes (means form submission completed)
  useEffect(() => {
    if (actionData) {
      setIsSubmitting(false);
    }
  }, [actionData]);

  const handlePasswordConfirmChange = (e) => {
    const password = document.getElementById('password')?.value;
    setPasswordsMatch(e.target.value === password);
  };

  const handleBackClick = () => {
    window.location.href = '/register';
  };

  return (
    <div className="flex min-h-full flex-col justify-center px-4 py-12 sm:px-6 lg:px-8 mt-[64px]">
      <div className="mx-auto w-full max-w-md">
        <div className="flex items-center mb-6">
          <button
            onClick={handleBackClick}
            className="mr-4 text-gray-500 hover:text-gray-700"
            type="button"
          >
            <svg 
              xmlns="http://www.w3.org/2000/svg" 
              fill="none" 
              viewBox="0 0 24 24" 
              strokeWidth={1.5} 
              stroke="currentColor" 
              className="w-5 h-5"
            >
              <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
            </svg>
          </button>
          <h1 className="text-center text-3xl font-bold tracking-tight text-gray-900 flex-grow">
            Create Consumer Account
          </h1>
        </div>

        {actionData?.error && (
          <div className="mt-4 rounded-md bg-red-50 p-4">
            <div className="flex">
              <div className="flex-shrink-0">
                <svg
                  className="h-5 w-5 text-red-400"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                  aria-hidden="true"
                >
                  <path
                    fillRule="evenodd"
                    d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
                    clipRule="evenodd"
                  />
                </svg>
              </div>
              <div className="ml-3">
                <h3 className="text-sm font-medium text-red-800">{actionData.error}</h3>
              </div>
            </div>
          </div>
        )}

        <div className="mt-8">
          <div className="bg-white px-6 py-8 shadow sm:rounded-lg sm:px-10">
            <Form
              method="post"
              noValidate
              className="space-y-6"
              onSubmit={() => setIsSubmitting(true)}
            >
              {/* Hidden field for registerType */}
              <input type="hidden" name="registerType" value="consumer" />
              
              <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
                <div>
                  <label htmlFor="firstName" className="block text-sm font-medium text-gray-900">
                    First name
                  </label>
                  <div className="mt-2">
                    <input
                      id="firstName"
                      name="firstName"
                      type="text"
                      autoComplete="given-name"
                      className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
                    />
                  </div>
                </div>

                <div>
                  <label htmlFor="lastName" className="block text-sm font-medium text-gray-900">
                    Last name
                  </label>
                  <div className="mt-2">
                    <input
                      id="lastName"
                      name="lastName"
                      type="text"
                      autoComplete="family-name"
                      className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
                    />
                  </div>
                </div>
              </div>

              <div>
                <label htmlFor="email" className="block text-sm font-medium text-gray-900">
                  Email address
                </label>
                <div className="mt-2">
                  <input
                    id="email"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
                  />
                </div>
              </div>

              <div>
                <label htmlFor="phone" className="block text-sm font-medium text-gray-900">
                  Phone number (optional)
                </label>
                <div className="mt-2">
                  <input
                    id="phone"
                    name="phone"
                    type="tel"
                    autoComplete="tel"
                    placeholder="+15146669999"
                    className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
                  />
                </div>
              </div>

              <div>
                <label htmlFor="password" className="block text-sm font-medium text-gray-900">
                  Password
                </label>
                <div className="mt-2">
                  <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="new-password"
                    required
                    className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3"
                  />
                </div>
              </div>

              <div>
                <label htmlFor="passwordConfirm" className="block text-sm font-medium text-gray-900">
                  Confirm password
                </label>
                <div className="mt-2">
                  <input
                    id="passwordConfirm"
                    name="passwordConfirm"
                    type="password"
                    autoComplete="new-password"
                    required
                    onChange={handlePasswordConfirmChange}
                    className={`block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ${
                      passwordsMatch ? 'ring-gray-300' : 'ring-red-300'
                    } placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 px-3`}
                  />
                </div>
                {!passwordsMatch && (
                  <p className="mt-1 text-sm text-red-600">Passwords do not match</p>
                )}
              </div>

              <div className="flex items-center">
                <input
                  id="acceptsMarketing"
                  name="acceptsMarketing"
                  type="checkbox"
                  className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600"
                />
                <label htmlFor="acceptsMarketing" className="ml-2 block text-sm text-gray-900">
                  Receive email updates and promotions
                </label>
              </div>

              <div>
                <button
                  type="submit"
                  disabled={isSubmitting || !passwordsMatch}
                  className="flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:bg-blue-300"
                >
                  {isSubmitting ? 'Creating account...' : 'Create account'}
                </button>
              </div>
            </Form>
          </div>
        </div>
      </div>
    </div>
  );
}

// Customer Create Mutation
const CUSTOMER_CREATE_MUTATION = `#graphql
  mutation customerCreate($input: CustomerCreateInput!) {
    customerCreate(input: $input) {
      customer {
        id
        firstName
        lastName
        email
        phone
        acceptsMarketing
        tags
      }
      customerUserErrors {
        code
        field
        message
      }
    }
  }
`;

// Customer Access Token Create Mutation (for automatic login after registration)
const CUSTOMER_ACCESS_TOKEN_CREATE_MUTATION = `#graphql
  mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
    customerAccessTokenCreate(input: $input) {
      customerAccessToken {
        accessToken
        expiresAt
      }
      customerUserErrors {
        code
        field
        message
      }
    }
  }
`;

// Cart Create Mutation
const CART_CREATE_MUTATION = `#graphql
  mutation cartCreate($input: CartInput!) {
    cartCreate(input: $input) {
      cart {
        id
        checkoutUrl
        buyerIdentity {
          customer {
            id
            email
            firstName
            lastName
            displayName
          }
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;

This is our custom hydrogen registration page, hopefully this can give you a better understanding.