How to Remove Customer Account API Authentication in Hydrogen
Building Custom Authentication Forms: Migrating to Email/Password Authentication
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:
The Customer Account API
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:
Pros | Cons |
Effortless setup | Forced to use email code auth |
Preconfigured auth form | Difficult to test queries |
Ensured Security out of box | Unable 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.
Pros | Cons |
Better Documentation and Support | Longer Setup Process |
Completely Headless | Less Beginner-friendly |
Ability to use custom auth form | Abstracts 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:
Login
Authorization
Logout
Fetching Customer Details
And you will need to create:
User Creation functionality
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 sessioncontext.session.set('key', value);
stores a new value in the sessioncontext.session.unset('key');
deletes a session valuecontext.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.