Skip to main content

Command Palette

Search for a command to run...

How to implement 'Sign in with Google' for Headless Shopify Stores (React, Next.js, Hydrogen etc.)

Updated
6 min read
How to implement 'Sign in with Google' for Headless Shopify Stores (React, Next.js, Hydrogen etc.)
D

I am a developer from North Carolina. I have always been fascinated by the internet, and scince high school I have been trying to understand the magic behind it all. ECU Computer Science Graduate

Shopify, by default, does not have pre-configured methods to authenticate customers via their Google accounts. In a Shopify theme environment, there are numerous apps you could use to accomplish social sign-in. But what about headless stores?!

In this tutorial, I will give you a step-by-step guide on implementing a ‘Sign in with Google’ option to your customer authentication.

This solution will work with ANY React framework (Hydrogen, Next.js, Remix etc.)

Requirements

General Steps

  1. Use Google Cloud Console to generate your Client ID

  2. Enable multipass tokens in your Shopify Admin

  3. Setup npm package react-oauth/google

  4. Make the client component (renders button and Google popup)

  5. Make the server component (authenticates Google token and creates multipass & Shopify access token)

1: Get Client ID from Google Cloud Console

Enter Google Cloud Console. Make an account and a new project if you have not already.

Then, navigate to APIs & ServicesCredentialsCreate Credentials

Fill out the form to create your credentials, adding the correct callback URIs and javascript origins.

The callback URI is the url that is visited after user successfully signs in via Google. This url is also sent an id_token.

Make sure to save your client ID, as it will be crucial for the next steps in this tutorial.

2: Enable Multipass Tokens in your Shopify Admin

Within your Shopify Admin, navigate to:

SettingsClassic Customer AccountsEnable Multipass

Again, make sure to save your Multipass Secret, as it will be used later in the tutorial

3: Setup ‘react-oauth/google’ NPM Package

npm install react-oauth/google

If you are using a CSP (Hydrogen contains one by default), you will need to enable the google account urls in your CSP (Hydrogen’s is in entry.server.jsx):

const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    connectSrc: [
        'https://accounts.google.com',  // add this line
        // any other connectSrc items
    ],
    defaultSrc: [
        'https://accounts.google.com',  // add this line
        // any other connectSrc items
    ],
});

You will also need to wrap your content in the GoogleAuthProvider tag, inputting your Google Cloud client id as a parameter. You also need to include the <script> tag for the google gsi client.

export default function App() {

  // whatever logic you have here

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <meta name="google-site-verification" content="tZeIR-uo2DFULTGGrPpycfZgoligNd9KpFY61czQMFU" />
        <Meta/>
        <Links pathname={location.pathname} />
      </head>
      <body className='font-open'>

          {/* Wrap content in GoogleAuthProvider */}
        <GoogleOAuthProvider clientId="<YOUR_CLIENT_ID>" >
          <Elements stripe={stripePromise}>
            <Layout {...data}>
              <Outlet />
            </Layout>
          </Elements>
        </GoogleOAuthProvider>

        <ScrollRestoration nonce={nonce} />
        <Scripts nonce={nonce} />

        {/* Use google accounts script */}
        <script src="https://accounts.google.com/gsi/client" async defer></script>

        <LiveReload nonce={nonce} />
      </body>
    </html>
  );
}

4: Create the Client Component

This component will add the ‘Sign in with Google’ button, and allow the popup for users to select their google account.

import React from "react";
import { GoogleLogin } from "@react-oauth/google";


export default function GoogleAuth() {

        // on successful google signon, send id token to callback URI
    const handleSuccess = (credentialResponse) => {
        const idToken = credentialResponse.credential;
        if (!idToken) {
            console.error("Google ID Token is missing.");
            return;
        }
        window.location.href = `/auth/callback/google?id_token=${idToken}`;
    };

        // Log error if sign-on fails 
    const handleError = () => {
        console.error("Google Sign-In failed.");
    };


    return (
        <div className="flex justify-center items-center mt-6">

                {/* Handles google sign-in popup */}
            <GoogleLogin
                onSuccess={handleSuccess}
                onError={handleError}
                theme="outline"
                size="large"
                text="signin_with"
            />

        </div>
    );
}

5: Create The Server Component

Make sure the route to this component is the same as the callback URI entered when generating the Google Cloud client id.

This server component:

  • Gets id_token from google authentication

  • Gets email using id_token

  • Creates multipass token for user (using multipassify npm package)

  • If new account, create a new customer account with this email via Admin API

  • Use multipass to generate customerAccessToken from Storefront API

  • Redirects user to account page once complete

import Multipassify from 'multipassify';
import { Buffer } from 'buffer-polyfill';
import { redirect } from '@shopify/remix-oxygen';
import crypto from 'crypto';

globalThis.Buffer = Buffer;


// Creates correctly formated date for multipass field 
function generateCreatedAt() {
    const now = new Date();
    const offsetMinutes = now.getTimezoneOffset();
    const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
    const offsetRemainder = Math.abs(offsetMinutes) % 60;
    const offsetSign = offsetMinutes > 0 ? '-' : '+';
    const formattedOffset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetRemainder).padStart(2, '0')}`;
    return now.toISOString().replace('Z', formattedOffset);
}


function generateSecurePassword() {
    // randomly generates secure passsword
}



export const loader = async ({ request, context }) => {

    // Environment variables
    const STORE_DOMAIN = await context.env.PUBLIC_STORE_DOMAIN;
    const ADMIN_API_TOKEN = context.env.SHOPIFY_ADMIN_ACCESS_TOKEN;

    // Get incoming id_token from URL param (added by Google)
    const url = new URL(request.url);
    const idToken = url.searchParams.get("id_token");
    if (!idToken) {
        throw new Error("ID Token is missing.");
    }

    // Validate the Google ID Token
    const googleResponse = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${idToken}`);
    const googleData = await googleResponse.json();

    if (!googleResponse.ok || !googleData.email) {
        throw new Error("Failed to validate Google ID Token.");
    }

    // Get email from id_token
    const { email } = googleData;

    // Get multipass secret from env
    const multipassSecret = context.env.MULTIPASS_SECRET;
    if (!multipassSecret) {
        throw new Error("Multipass secret is missing from the environment variables.");
    }

    // Create multipass token
    const createdAt = generateCreatedAt();
    const multipassify = new Multipassify(multipassSecret);       // create multipass
    const multipassPayload = { email, created_at: createdAt };
    const multipassToken = multipassify.encode(multipassPayload); // encode multipass

    // Check if customer exists
    const customerSearchResponse = await fetch(`https://${STORE_DOMAIN}/admin/api/2023-10/customers/search.json?query=email:${email}`, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'X-Shopify-Access-Token': ADMIN_API_TOKEN,
        },
    });
    const customerSearchResult = await customerSearchResponse.json();
    if (!customerSearchResponse.ok) {
        throw new Error(`Error finding account: ${JSON.stringify(result.errors || result)}`);
    }
    const customerExists = customerSearchResult?.customers.length > 0;


    // If customer doesn't exist
    if (!customerExists) {

        // Auto-generate secure password (however you want to do this)
        // Not showing my password generation due to security reasons 
        const password = generateSecurePassword();  

        // Create a new customer using the Admin API, if customer does not exist
        const createCustomerResponse = await fetch(`https://${STORE_DOMAIN}/admin/api/2023-10/customers.json`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Shopify-Access-Token': ADMIN_API_TOKEN,
            },
            body: JSON.stringify({
                customer: {
                    email,
                    password,
                    password_confirmation: password,
                },
            }),
        });
        const createCustomerData = await createCustomerResponse.json();
        if (!createCustomerResponse.ok || createCustomerData.errors) {
            throw new Error(`Failed to create customer: ${JSON.stringify(createCustomerData.errors || createCustomerData)}`);
        }
    }

    // Authenticate user and generate customerAccessToken using Multipass
    const tokenMutation = await context.storefront.mutate(
        `
        mutation customerAccessTokenCreateWithMultipass($multipassToken: String!) {
            customerAccessTokenCreateWithMultipass(multipassToken: $multipassToken) {
                customerAccessToken {
                    accessToken
                    expiresAt
                }
                customerUserErrors {
                    code
                    field
                    message
                }
            }
        }
        `,
        { variables: { multipassToken } }
    );

    const { customerAccessToken, customerUserErrors } = tokenMutation?.customerAccessTokenCreateWithMultipass || {};
    if (customerUserErrors && customerUserErrors.length > 0) {
        throw new Error(
            `Failed to create Customer Access Token: ${JSON.stringify(customerUserErrors)}`
        );
    }
    if (!customerAccessToken) {
        throw new Error("Failed to generate Customer Access Token.");
    }

    // Adding access token to session
      // Could be different for you, depending on how you are storing & validating tokens
    await context.session.set("customerAccessToken", customerAccessToken.accessToken);

    // redirect to account page
    return redirect("/account", {
        headers: {
            "Set-Cookie": await context.session.commit(),
        },
    });
};

Note I am using the buffer npm package under the alias buffer-polyfill . When I try to use buffer without an alias, I get an error since Hydrogen does not use the Node environment. AKA, ignore this if you are not deploying to Oxygen environment

To do this, add this line to your package.json dependencies object and run npm install

buffer-polyfill: "npm:buffer@^6.0.3"

Also with crypto , I have to edit my remix.config file to use crypto on the client and server. Here is the fields I added to my module.exports:

serverNodeBuiltinsPolyfill: {
  modules: {
    crypto: true, // Provide a JSPM polyfill
  },
},
browserNodeBuiltinsPolyfill: { 
  modules: { 
    crypto: true,
  }
},

Conclusion

Test and tinker with your solution so that it works for your authentication process for your store. Hopefully this can help you, and if not, please feel free to leave a comment or reach out.

Thanks for reading!

More from this blog

David Williford

21 posts

Bug fixes, tutorials, and general information about software topics I am interested in.