All things Shopify and commerce
Hello,
i am having trouble following the docs of the Customer Account API Customer Account API reference (shopify.dev).
My objective (correct me, if i misunderstood anything, and the objective seems odd or even impossible):
- Setting up the Headless sales channel and using the Customer Account API as a confidential client (i have set up a new shop for testing)
- Authenticating customers to the shopify backend with my Next.js app, by using the code from the docs above (i am not using Hydrogen, since i have to use Next.js for other purposes anyway)
So far, i haven't got any further than completing the authorization process. When trying to obtain the access_token, i recieve the response code 401, "invalid_client", with the following error description:
'The client identifier provided is invalid, the client failed to authenticate, the client did not include its credentials, provided multiple client credentials, or used unsupported credentials type.'
Obviously, i triple checked both client-id and client-secret, as well as rotating the credentials in the backend and ensuring the validity of both strings.
Here is the code of my home/login page,
import Head from 'next/head'
import Link from 'next/link'
import * as util from '../components/Utilities
export default function Home() {
function loginCustomer(event) {
event.preventDefault()
const clientId = process.env.NEXT_PUBLIC_CLIENT_ID
const authorizationRequestUrl = new URL(`https://shopify.com/${process.env.NEXT_PUBLIC_SHOP_ID}/auth/oauth/authorize`)
authorizationRequestUrl.searchParams.append('scope', 'openid email https://api.customers.com/auth/customer.graphql')
authorizationRequestUrl.searchParams.append('client_id', clientId)
authorizationRequestUrl.searchParams.append('response_type', 'code')
authorizationRequestUrl.searchParams.append('redirect_uri', 'http://localhost:3000/dashboard')
authorizationRequestUrl.searchParams.append('state', util.generateState())
window.location.href = authorizationRequestUrl.toString()
}
return (<>
<Head>
<meta name="description" content="Beschreibung" />
<meta name="keywords" content="Keywords" />
<title>Page title</title>
<link rel="icon" href="/favicon.png" sizes="any" />
</Head>
<main id="page-home">
<div className="ml-container">
<form onSubmit={loginCustomer}>
<button type="submit">Login</button>
</form>
</div>
</main>
</>)
}
the dashboard,
import Head from 'next/head'
import Link from 'next/link'
import { useEffect } from 'react'
// schopify
import { getCredentials } from './api/accounts/auth'
export async function getServerSideProps({ query }) {
// obtain access token
const credentials = await getCredentials()
const body = new URLSearchParams()
body.append('grant_type', 'authorization_code')
body.append('client_id', process.env.NEXT_PUBLIC_CLIENT_ID)
body.append('redirect_uri', `http://localhost:3000/dashboard`)
body.append('code', query.code)
const headers = {
'content-type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
}
console.log('Authorization: ', `Basic ${credentials}`);
const response = await fetch(`https://shopify.com/${process.env.NEXT_PUBLIC_SHOP_ID}/auth/oauth/token`, {
method: 'POST', headers: headers, body: body,
})
const test = await response.json()
// this line logs the error code 400, invalid_client
console.log(test);
return {
props: {}
}
}
export default function Dashboard(props) {
function logoutCustomer() {
console.log('logout');
}
return (<>
<Head>
<meta name="description" content="Beschreibung" />
<meta name="keywords" content="Keywords" />
<title>Dashboard</title>
<link rel="icon" href="/favicon.png" sizes="any" />
</Head>
<main id="page-dashboard">
<div className="ml-container">
<h1>Dashboard</h1>
<button onClick={logoutCustomer}>Logout</button>
</div>
</main>
</>)
}
and the "authorization header" functions
import crypto from 'crypto'
export async function getCredentials() {
const clientId = process.env.NEXT_PUBLIC_CLIENT_ID;
const clientSecret = process.env.NEXT_PUBLIC_CLIENT_SECRET;
// const credentials = await crypto.subtle.digest({ name: "SHA-256" }, new TextEncoder().encode(`${clientId}:${clientSecret}`))
// const hash = convertBufferToString(credentials);
// return base64UrlEncode(hash);
return await crypto.subtle.digest({ name: "SHA-256" }, new TextEncoder().encode(`${clientId}:${clientSecret}`))
}
function base64UrlEncode(string) {
const base64 = btoa(string);
// This is to ensure that the encoding does not have +, /, or = characters in it.
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function convertBufferToString(ArrayBuffer) {
const uintArray = new Uint8Array(ArrayBuffer);
const numberArray = Array.from(uintArray);
return String.fromCharCode(...numberArray);
}
Note, that i tried converting the arrayBuffer back to a string as well, since it didn't seem right to me in the documentation.
I know that the Customer Account API is brand new, but since i don't see any alternative in the docs, i have to keep trying for now. Any help would be well apreciated.
Thanks in advance and
best regards
I'm facing a similar issue, doesn't seem to have any other alternative in the docs either
Hello, any updates here? same issue
I believe I got this working by using client_id/client_secret in the body, with no Authorization header.
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:token-exchange')
body.append('client_id', clientId)
body.append('client_secret', clientSecret)
body.append('audience', customerApiClientId)
body.append('subject_token', accessToken)
body.append('subject_token_type', 'urn:ietf:params:oauth:token-type:access_token')
body.append('scopes', 'https://api.customers.com/auth/customer.graphql')
You probably don't want to be exposing your client secret in an http request like this
i don't think that is a concern, as this is a service to service call. being part of the body, it is protected by SSL
but to your point, you are correct in the case of performing these from a front end
I'm trying to implement the passwordless login but I'm getting an "access denied" error. @futbolpal I would greatly appreciate if you could share a working example. I'd be willing to compensate economically, because I really need this done. Thank you!
I also was only able to do this the way you did. I checked my network requests and my client secret is not exposed, only my client id, which I think is fine. The documentation seems to be wrong https://shopify.dev/docs/api/customer. This is really annoying and they need to fix it. What are the credentials and nonce for? I still don't know. It says I can retrieve the nonce but what for?
I'm facing the same issue after deploying in Vercel. I am getting the acces_token in local.
In prod env, I am getting the response
{"error":"invalid_request","error_description":"'client_id', 'grant_type' required."}
may be you forgot
I just want to let you know that I was doing it this way as well but then I found this https://shopify.dev/docs/custom-storefronts/building-with-the-storefront-api/customer-accounts. It's a lot better to do the graphql way from the documentation above. You don't need to redirect the user to a different page and you can easily implement Google, Apple, etc login as well.
Discover how to increase the efficiency of commerce operations with Shopify Academy's l...
By Jacqui Mar 26, 2025Shopify and our financial partners regularly review and update verification requiremen...
By Jacqui Mar 14, 2025Unlock the potential of marketing on your business growth with Shopify Academy's late...
By Shopify Mar 12, 2025