How to Remove Customer Account API Authentication in Hydrogen

Building Custom Authentication Forms: Migrating to Email/Password Authentication

How to Remove Customer Account API Authentication in Hydrogen

In this tutorial, we are going to migrate from the default customer account API authentication in Hydrogen. Which is the non-customizable, email-code login (like below).

If you are reading this, you likely made a new Hydrogen project after August 2023. Hydrogen projects created after this date are pre-configured with the customer account API as the default user authentication method.

This article is going to teach you how to abandon this form of authentication, and implement the storefront API for authentication instead.

Notice Before Reading

Depending on your knowledge level of JWT (access token) authentication, and the Shopify APIs, I would highly recommend reading and understand the FULL article, not just skimming.

This is not really too difficult, but will require a good understanding of Shopify’s authentication. If you attempt to implement authentication and do it wrongly or without full oversight, you risk exposing user data to malicious attacks.

Therefore, it is very important to understand the user authentication process very well before tampering with it.

Authentication Methods in Shopify

There are 2 primary methods for authentication in headless Shopify stores:

  1. The Customer Account API

  2. The Storefront API

Customer Account API

This is the authentication method we are abandoning

Newer Hydrogen builds come with the Customer Account API preconfigured as the default authentication method. You can tell if your Hydrogen project is using this, by looking for these methods in your account/login and account/logout routes within the loader functions.

/**
* The Customer Account API methods
*/

context.customerAccount.login();     // redirects user to shopify form to login
context.customerAccount.logout();    // logs out user
context.customerAccount.authorize(); // authorizes current user
context.customerAccount.handleAuthStatus();  // checks auth status of current user

If you are reading this blog, you likely made a Hydrogen build after August 2023, and this was the pre-configured authentication method in your hydrogen build (pictured under article title).

Even though we are here to abandon this as our authentication method, there are some benefits to using the customer account API on Hydrogen:

ProsCons
Effortless setupForced to use email code auth
Preconfigured auth formDifficult to test queries
Ensured Security out of boxUnable to customize authentication form
Not really “headless”

Storefront API Authentication

This is the authentication method we want to switch to

The Storefront API is the original method of authentication for headless storefronts. The Storefront API allows customers to sign in via email & password, and allows developers full control over the authentication process.

This also allows us to make a completely custom login form and keep all of the user authentication within your headless site.

ProsCons
Better Documentation and SupportLonger Setup Process
Completely HeadlessLess Beginner-friendly
Ability to use custom auth formAbstracts security responsibility to developers
Easy to test endpoints via GraphiQL

Example Storefront API Authentication Queries

# To create user
mutation customerCreate($input: CustomerCreateInput!) {
  customerCreate(input: $input) {
    customer {
      id
      email
      firstName
    }
    customerUserErrors {
      field
      message
    }
  }
}


# Login and return access token via email/password
mutation customerAccessTokenCreate($email: String!, $password: String!) {
    customerAccessTokenCreate(input: { email: $email, password: $password }) {
      customerAccessToken {
        accessToken
        expiresAt
      }
      customerUserErrors {
        code
        field
        message
      }
   }
}


# Use accessToken for authenticated requests
query customer($customerAccessToken: String!) {
  customer(customerAccessToken: $customerAccessToken) {
    firstName
    lastName
    email
    orders(first: 10) {
      edges {
        node {
          orderNumber
          totalPrice {
            amount
          }
        }
      }
    }
  }
}


# refresh accessToken
mutation customerAccessTokenRenew($customerAccessToken: String!) {
  customerAccessTokenRenew(customerAccessToken: $customerAccessToken) {
    customerAccessToken {
      accessToken
      expiresAt
    }
    userErrors {
      field
      message
    }
  }
}


# Log out
mutation customerAccessTokenDelete($customerAccessToken: String!) {
  customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
    deletedAccessToken
    userErrors {
      field
      message
    }
  }
}

These methods will be used later in this article, as we switch our Hydrogen application from using the Customer Account API to using the Storefront API for customer authentication (Which will allow us to make our own custom login form on our site).

Build A Custom Login Form on Frontend

For testing purposes, it is smart to build the form on the frontend first. This way, we can test the endpoints we are changing within our application. Keep in mind, the storefront API is email/password authentication.

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


export async function loader({ context }) {
  const customerAccessToken = await context.session.get('customerAccessToken');
  if (!customerAccessToken) {
    return json({ isLoggedIn: false });
  }

  const { customer } = await context.storefront.query(
  `query CustomerDetails($customerAccessToken: String!) {
    customer(customerAccessToken: $customerAccessToken) {
      id
    }
  }`, 
  {
    variables: { customerAccessToken },
  });

  if (customer) {
    return redirect('/account/orders');
  } else {
    return null;
  }
}



export default function LoginForm() {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const fetcher = useFetcher();

    const handleSubmit = (event) => {
        event.preventDefault();
        if(email && password) {
            const formData = new FormData();
            formData.append('email', email);
            formData.append('password', password);
            fetcher.submit(formData, { method: 'post', action: '/account/login' });
        }
    };

    useEffect(() => {
        if (fetcher.data) {
            console.log('Response from action:', fetcher.data);
        }
    }, [fetcher.data]);

    return (
        <div className='bg-white'>
            <form 
                onSubmit={(event) => handleSubmit(event)}
                className='max-w-sm mx-auto flex flex-col gap-3 py-10 text-sizzleBlue'
            >
                <h3 className='text-4xl text-center font-thin mb-2'>Login</h3>
                <input
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    placeholder='Email'
                    className='rounded-md border-neutral-300'
                    required
                />
                <input
                    type="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    placeholder='Password'
                    className='rounded-md border-neutral-300'
                    required
                />
                <button 
                    type="submit"
                    className='self-center mt-2 px-10 py-2 bg-sizzleOrange text-white font-thin text-lg rounded-full'
                >
                    Log In
                </button>
            </form>
        </div>
    );
}

Later you can add creating new accounts, styling etc

Switching From Customer Account API to Storefront API

There are 4 primary components that need to be updated, when switching from the Customer Account API to the Storefront API:

  1. Login

  2. Authorization

  3. Logout

  4. Fetching Customer Details

And you will need to create:

  1. User Creation functionality

  2. Custom Login / Signup form

Any other instances of customerAccount methods will need to be replaced with their storefront API equivalents

Login Component

Your account/login route will go from this:

import {json} from '@shopify/remix-oxygen';

export async function loader({ request, context }) {
  return context.customerAccount.login();
}

to this:

import {json} from '@shopify/remix-oxygen';
import {redirect} from '@shopify/remix-oxygen';


// Will now receive POST requests from custome login form
export async function action({ request, context }) {

  const formData = await request.formData();
  const email = formData.get('email');
  const password = formData.get('password');

  // Call the Storefront API's customerAccessTokenCreate mutation
  const { customerAccessTokenCreate } = await context.storefront.mutate(`
    mutation customerAccessTokenCreate($email: String!, $password: String!) {
      customerAccessTokenCreate(input: { email: $email, password: $password }) {
        customerAccessToken {
          accessToken
          expiresAt
        }
        customerUserErrors {
          code
          field
          message
        }
      }
    }
  `, { variables: { email, password } });

  // Check if there were any errors in the mutation response
  if (customerAccessTokenCreate.customerUserErrors.length > 0) {
    return json({ error: customerAccessTokenCreate.customerUserErrors }, { status: 400 });
  }

  // Get the access token
  const accessToken = customerAccessTokenCreate.customerAccessToken.accessToken;

  console.log("Access Token: ", accessToken);

  // Store the access token in the session
  await context.session.set('customerAccessToken', accessToken);

  // Commit the session and send Set-Cookie header back to the client
  return redirect('/account/orders', {
    headers: {
      'Set-Cookie': await context.session.commit(),
    },
  });
}

Authorization Component

In my storefront API setup, I no longer utilize the /account/authorization route given by the customer Account API:

export async function loader({context}) {
  return context.customerAccount.authorize();
}

Instead, I store access tokens in the session. Sessions should already be set up in your Hydrogen application (in lib/session.js ).

Session methods:

  • context.session.get('key'); gets the value of a value stored in the session

  • context.session.set('key', value); stores a new value in the session

  • context.session.unset('key'); deletes a session value

  • context.session.commit(); saves all of the changes that were made to the session during the component (typically is towards bottom of server component

How to check if a user is logged in, and redirect accordingly

This code snippet is used to check if a user is logged in, and redirect accordingly. Put this in any page you only want signed-in users to view. Orders page, account page etc.

This effectively replaces the authorization component. You could abstract this to a utility function and just reference it access multiple components.

const customerAccessToken = await context.session.get('customerAccessToken');

const { customer } = await context.storefront.query(
  `query CustomerDetails($customerAccessToken: String!) {
    customer(customerAccessToken: $customerAccessToken) {
      id
    }
  }`, 
  {
    variables: { customerAccessToken },
  });

  if (customer) {
    return redirect('/account/orders');
  } else {
    return null;
  }

Logout Component

For my logout code, in route /account/logout, it goes from:

import {redirect} from '@shopify/remix-oxygen';

export async function loader() {
  return redirect('/');
}

export async function action({context}) {
  return context.customerAccount.logout();
}

to:

// In essence, all this does is delete the access token and redirect

import { redirect } from '@shopify/remix-oxygen';

// Mutation to invalidate the access token on Shopify
const CUSTOMER_ACCESS_TOKEN_DELETE_MUTATION = `
  mutation customerAccessTokenDelete($customerAccessToken: String!) {
    customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
      deletedAccessToken
      userErrors {
        field
        message
      }
    }
  }
`;

export async function loader() {
  return redirect('/');
}

export async function action({ context }) {
  // Get the customerAccessToken from the session
  const customerAccessToken = await context.session.get('customerAccessToken');

  if (customerAccessToken) {
    // Invalidate the token in Shopify
    try {
      const { data, errors } = await context.storefront.mutate({
        mutation: CUSTOMER_ACCESS_TOKEN_DELETE_MUTATION,
        variables: { customerAccessToken },
      });

      if (errors?.length || !data.customerAccessTokenDelete.deletedAccessToken) {
        console.error('Error invalidating customer access token:', errors || data.customerAccessTokenDelete.userErrors);
      }
    } catch (error) {
      console.error('Error while invalidating access token:', error);
    }
  }

  // Clear the session by unsetting the customerAccessToken
  context.session.unset('customerAccessToken');

  // Redirect the user to the homepage and commit the session changes
  return redirect('/', {
    headers: {
      'Set-Cookie': await context.session.commit(),
    },
  });
}

Fetching Customer Details

Change your /account route from this:

export async function loader({context}) {
  const {data, errors} = await context.customerAccount.query(
    CUSTOMER_DETAILS_QUERY,
  );

  if (errors?.length || !data?.customer) {
    throw new Error('Customer not found');
  }

  return json(
    {customer: data.customer},
    {
      headers: {
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Set-Cookie': await context.session.commit(),
      },
    },
  );
}

to this:

export async function loader({ context }) {
  const customerAccessToken = await context.session.get('customerAccessToken');
  console.log("Customer Access Token from session: ", customerAccessToken);

  if (!customerAccessToken) {
    throw new Error('Customer not authenticated');
  }

  // Using the Storefront API instead of customerAccount API
  const { customer } = await context.storefront.query(CUSTOMER_DETAILS_QUERY, {
    variables: { customerAccessToken },
  });

  if (!customer) {
    throw new Error('Customer not found');
  }

  return json(
    { customer },
    {
      headers: {
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Set-Cookie': await context.session.commit(),
      },
    }
  );
}



const CUSTOMER_DETAILS_QUERY = `
  query CustomerDetails($customerAccessToken: String!) {
    customer(customerAccessToken: $customerAccessToken) {
      id
      firstName
      lastName
      email
      acceptsMarketing
      createdAt
      phone
      defaultAddress {
        address1
        address2
        city
        province
        country
        zip
      }
    }
  }
`;

Notice I also changed the graphQL query, since we are now using the Storefront API.

Conclusion

I hope you enjoyed this tutorial. And most importantly, I hope you learned more about the storefront API and the customer account API. Understanding these 2 APIs in Hydrogen will allow you to make this migration seamlessly and specific to your application.

Don’t forget, your application may be different than mine. The main idea here is switching your customer account API methods to storefront API queries that can accomplish the same task.

Make sure that you have gotten rid of any context.customerAccount references. When context.customerAccount is referenced, your application will automatically send a GET request to account/login if not logged in via customer account API.