Skip to main content

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.


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.

    Steps for authenticating checkouts in a mobile app


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.

Note

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 WebView

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);
}

Anchor to Building the authorization URLBuilding the authorization URL

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();
}

Anchor to Step 3: Handle the authorization code callbackStep 3: Handle the authorization code callback

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;
}

Anchor to Step 4: Exchange authorization code for an access tokenStep 4: Exchange authorization code for an access token

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.

Note

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

mutation cartCreate($input: CartCreateInput!) {
cartCreate(input: $input) {
checkoutUrl
userErrors {
field
message
}
}
}

Variables

{
"input": {
"lines": [
{
"merchandiseId": "gid://shopify/ProductVariant/12345",
"quantity": 1
}
],
"buyerIdentity": {
"customerAccessToken": "<accessToken>"
}
}
}
Caution

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.

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.

Any tokens that have been retrieved should be stored securely using security features offered by the operating system.

Examples:

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.


Was this page helpful?