Authenticate checkouts with the Customer Account API
You can create an authenticated checkout experience for buyers within your mobile app by using the Checkout Sheet Kit and the Customer Account API. The checkout will be prefilled with the buyer's customer account information, making it easier and faster to complete checkout. To get started, follow the step-by-step example provided in this guide.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
-
Obtain a Customer Account API access token
-
Use the access token to create an authenticated cart
-
Present the checkout to a buyer using Checkout Sheet Kit
The following sequence diagram illustrates the complete flow covered in the following steps.
Anchor to RequirementsRequirements
- Your store is using new customer accounts.
- You have an app using Checkout Sheet Kit.
- Your app has Storefront API access.
Anchor to Step 1: Get access to the Customer Account APIStep 1: Get access to the Customer Account API
Configure Customer Account API access for your app.
Make sure that you've configured Customer Account API access as a public mobile client. Take a note of the callback URL and client ID as you'll need this information in the following step.
Anchor to Step 2: Open the login page in a WebViewStep 2: Open the login page in a Web View
To authenticate with the Customer Account API, your app needs to implement an OAuth 2.0 authorization flow.
Mobile clients can begin the authorization flow by opening the authorization endpoint in a WebView. When building the URL, the client_id
parameter should match the mobile client you created in the previous step. The redirect_uri
should also match the callback URL specified for that client. By default, the format is shop.<your shop id>.app://callback
.
The code_challenge
and code_challenge_method
parameters are also required for mobile clients.
After the URL has been built, open the URL in a WebView for the customer to complete login.
Anchor to Generate a code challenge and code verifierGenerate a code challenge and code verifier
A code challenge and verifier will be used to verify that the client requesting tokens is the same client that initiated the authorization request. For more information, refer to Proof Key for Code Exchange. The code snippets below show how you can generate a code challenge and code verifier.
Save a reference to the code verifier as you'll use it in Step 4.
Generate code challenge and code verifier
func generateCodeVerifier() -> String {
var buffer = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
return base64Encode(Data(buffer))
}
func generateCodeChallenge(for codeVerifier: String) -> String {
guard let data = codeVerifier.data(using: .utf8) else { fatalError() }
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { bytes in
_ = CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &digest)
}
return base64Encode(Data(digest))
}
fun generateCodeVerifier(): String {
val buffer = ByteArray(32)
SecureRandom().nextBytes(buffer)
return buffer.base64UrlEncode()
}
fun generateCodeChallenge(codeVerifier: String): String {
val data = codeVerifier.toByteArray(Charsets.UTF_8)
val digest = data.sha256()
return digest.base64UrlEncode()
}
import * as Crypto from 'expo-crypto';
function encodeBase64(bytes: string){
return Buffer.from(bytes).toString('base64')
}
export async function generateCodeVerifier() {
const bytes = await Crypto.getRandomBytesAsync(32);
return encodeBase64(bytes);
}
export async function generateCodeChallenge(codeVerifier: string) {
const digestString = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA512,
codeVerifier,
);
return encodeBase64(digestString);
}
After you've generated the code challenge and code verifier, you can build a complete authorization URL.
Build an authorization URL
let codeChallenge = generateCodeChallenge(for: codeVerifier)
var components = URLComponents(string: "https://shopify.com/authentication/<shop-id>/oauth/authorize")
components?.queryItems = [
URLQueryItem(name: "scope", value: "openid email customer-account-api:full"),
URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
URLQueryItem(name: "state", value: generateRandomString(length: 36)),
URLQueryItem(name: "code_challenge", value: codeChallenge),
URLQueryItem(name: "code_challenge_method", value: "S256")
]
return components?.url?.absoluteString
}
fun buildAuthorizationUrl(codeVerifier: String): String {
val codeChallenge = generateCodeChallenge(codeVerifier)
return Uri.parse("https://shopify.com/authentication/<shop-id>/oauth/authorize").buildUpon()
.appendQueryParameter("scope", "openid email customer-account-api:full")
.appendQueryParameter("client_id", customerAccountsApiClientId)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", customerAccountsApiRedirectUri)
.appendQueryParameter("state", generateRandomString(length = 36))
.appendQueryParameter("code_challenge", codeChallenge)
.appendQueryParameter("code_challenge_method", "S256")
.build()
.toString()
}
function buildAuthorizationUrl(codeVerifier) {
const codeChallenge = generateCodeChallenge(codeVerifier);
const url = new URL('https://shopify.com/authentication/<shop-id>/oauth/authorize');
url.searchParams.append('scope', 'openid email customer-account-api:full');
url.searchParams.append('client_id', customerAccountsApiClientId);
url.searchParams.append('response_type', 'code');
url.searchParams.append('redirect_uri', customerAccountsApiRedirectUri);
url.searchParams.append('state', generateRandomString(36));
url.searchParams.append('code_challenge', codeChallenge);
url.searchParams.append('code_challenge_method', 'S256');
return url.toString();
}
After the customer successfully logs in, the WebView is redirected to the specified redirect_uri
with a code
query parameter. Clients should listen for page navigation events within the login WebView and extract the authorization code from the URL so that it can be used in Step 4. After the code
has been extracted, the login WebView can be closed.
Handle the authorization code callback
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = action.request.url else {
decisionHandler(.allow)
return
}
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
components.scheme == configuredComponents.scheme,
components.host == configuredComponents.host else {
decisionHandler(.allow)
return
}
guard let queryItems = components.queryItems else {
decisionHandler(.allow)
return
}
guard let state = queryItems.first(where: { $0.name == "state" })?.value,
state == savedState else {
decisionHandler(.allow)
return
}
guard let code = queryItems.first(where: { $0.name == "code" })?.value else {
decisionHandler(.allow)
return
}
// Use the code and cancel the navigation since we've handled the callback
exchangeCodeForTokens(code: code)
decisionHandler(.cancel)
}
if ("${request?.url?.scheme}://${request?.url?.host}" != customerAccountsApiRedirectUri) {
return super.shouldOverrideUrlLoading(view, request)
}
val code = request.url.getQueryParameter("code") ?: return super.shouldOverrideUrlLoading(view, request)
// use the code and cancel the navigation since we've handled the callback
exchangeCodeForTokens(code)
return true
}
function handleNavigationStateChange(navState) {
const { url } = navState;
if (url?.startsWith(configuredRedirectUri)) {
const code = new URL(url).searchParams.get('code');
if (code) {
// use the code and cancel the navigation since we've handled the callback
exchangeCodeForTokens(code);
return false;
}
}
return true;
}
After you've extracted the code
parameter from the callback URL, you can submit it, along with the code_verifier
saved earlier, to the token endpoint to obtain a Customer Account API access token.
Exchange authorization code for a Customer Account API access token
private func requestAccessToken(code: String, codeVerifier: String) async throws -> OAuthTokenResult {
let parameters: [String: String] = [
"grant_type": "authorization_code",
"client_id": clientId,
"redirect_uri": redirectUri,
"code": code,
"code_verifier": codeVerifier
]
let requestBody = try! JSONSerialization.data(withJSONObject: parameters, options: [])
var request = URLRequest(url: URL(string: "https://shopify.com/authentication/<shop-id>/oauth/token")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = requestBody
return try await executeOAuthTokenRequest(request: request)
}
suspend fun requestAccessToken(code: String, codeVerifier: String): OAuthTokenResult {
val requestBody = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", clientId)
.add("redirect_uri", redirectUri)
.add("code", code)
.add("code_verifier", codeVerifier)
.build()
val request = Request.Builder()
.url("https://shopify.com/authentication/<shop-id>/oauth/token")
.post(requestBody)
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build()
return executeOAuthTokenRequest(request)
}
export async function requestAccessToken(code, codeVerifier) {
const requestBody = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
redirect_uri: redirectUri,
code: code,
code_verifier: codeVerifier,
});
const response = await fetch('https://shopify.com/authentication/<shop-id>/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: requestBody.toString(),
});
return handleOAuthTokenResponse(response);
}
Anchor to Step 5: Attach the access token to your cartStep 5: Attach the access token to your cart
When you create or update a cart using the Storefront API, the token obtained in Step 4 should be included in the buyer identity input of your cart mutation requests.
The token endpoint response has multiple fields (including access_token
, refresh_token
, and expires_in
), the token to submit in cart mutation requests is found in the access_token
field.
POST https://{shop}.myshopify.com/api/{api_version}/graphql.json
GraphQL mutation
Variables
The checkoutUrl
returned from cart mutation requests should be used within a short time frame.
Anchor to Step 6: Show the checkout using Checkout Sheet KitStep 6: Show the checkout using Checkout Sheet Kit
Storefront API cart mutations will return a checkoutUrl
that you can submit to Checkout Sheet Kit's present()
or preload()
methods to display the authenticated checkout to the buyer.
Generate code challenge and code verifier
import ShopifyCheckoutSheetKit
ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: rootViewController, delegate: self)
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit
ShopifyCheckoutSheetKit.present(checkoutUrl, activity, eventProcessor)
import {ShopifyCheckoutSheetProvider} from '@shopify/checkout-sheet-kit';
const shopifyCheckout = useShopifyCheckoutSheet();
shopifyCheckout.present(checkoutUrl);
More details can be found within the repository READMEs.
Anchor to Limitations and considerationsLimitations and considerations
This section describes some of the limitations and considerations associated with authenticating checkouts with the Checkout Sheet Kit and the Customer Account API.
Anchor to Token expiryToken expiry
Customer Account API access tokens have an expiry in seconds, which is available from the expires_in
field of the token endpoint response. After an access token expires, it's invalid and needs to be refreshed. You can refresh an access token using the refresh_token
, which is also returned in the token endpoint response, by making a request with refresh_token
as the grant_type
.
Anchor to Storing tokensStoring tokens
Any tokens that have been retrieved should be stored securely using security features offered by the operating system.
Examples:
- KeyChain in iOS
- Keystore or Encrypted Shared Preferences in Android
Anchor to Logging outLogging out
To allow users to logout of your app, you can make a request to the logout endpoint. This is also the time to clear up any related state in your app, such as locally stored tokens and existing authenticated carts.