---
title: Authenticate checkouts
description: >-
  Prefill checkout with buyer account information using Checkout Kit and the
  Customer Account API.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/mobile/checkout-kit/authenticate-checkouts?extension=swift
  md: >-
    https://shopify.dev/docs/storefronts/mobile/checkout-kit/authenticate-checkouts.md?extension=swift
---

# Authenticate checkouts

Authenticate checkouts with Checkout Kit and the Customer Account API. When a buyer signs in, Shopify prefills checkout with their email, shipping address, and payment method.

Use the **Platform** dropdown under **Project** to switch between Swift, Kotlin, and React Native. The steps and code update for each platform.

## What you'll learn

In this tutorial, you'll:

* Obtain a Customer Account API access token using OAuth and Proof Key for Code Exchange (PKCE).
* Use the access token to create an authenticated cart.
* Present checkout to a signed-in buyer using Checkout Kit.

![Sequence diagram showing the OAuth flow for authenticated checkouts: app requests authorization, buyer signs in, app exchanges code for token, and presents checkout.](https://shopify.dev/assets/assets/images/custom-storefronts/csk-auth.md-1-dWlrYKLk.svg)

## Requirements

[Enable new customer accounts](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/getting-started#step-1-enable-customer-accounts)

New customer accounts enabled on your store.

[Set up Checkout Kit](https://shopify.dev/docs/storefronts/mobile/checkout-kit)

Checkout Kit installed in your app.

[Configure Storefront API access](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started#step-1-enable-storefront-api-access)

Storefront API access for your app.

## Project

[View on GitHub](https://github.com/Shopify/example-mobile--storefront--swift)

## Configure Customer Account API access

Configure [Customer Account API access](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/getting-started#step-2-configure-customer-account-api-access) as a public mobile client. Save the callback URL and client ID because you'll need them in the next step.

### Set up the mobile client

In the Dev Dashboard, configure a Customer Account API client for your app. Set the client type to **Public** and save these values:

* **Client ID**: Required for the authorization URL and token exchange.
* **Callback URL**: The redirect URI for the OAuth flow. By default, the format is `shop.{your_shop_id}.app://callback`.

## Authenticate the buyer

Implement an [OAuth 2.0 authorization flow](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/getting-started#step-3-perform-authorization-requests-and-make-queries) with PKCE to sign the buyer in through the Customer Account API.

### Generate a code challenge and verifier

Generate a PKCE code verifier and challenge. These prove that the client exchanging the authorization code is the same client that started the request, as defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636). Save the code verifier because you'll need it when you exchange the authorization code for a token.

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

### Discover authentication endpoints

Fetch the authorization and token endpoints from your shop's OpenID configuration.

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

### Build the authorization URL

Combine the authorization endpoint, client ID, redirect URI, code challenge, and a random state parameter into an authorization URL. Open the URL in a web view so the buyer can sign in.

***

[Authorization endpoint](https://shopify.dev/docs/api/customer#step-authorization)

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## Handle the authorization callback

After the buyer signs in, the web view redirects to your `redirect_uri` with a `code` query parameter. Extract the authorization code and close the web view.

### Extract the authorization code

Listen for navigation events in the web view. When the URL matches your redirect URI, extract the `code` and `state` parameters. Verify that the `state` matches the value you sent in the authorization URL to prevent CSRF attacks.

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## Exchange the code for an access token

Submit the authorization code and the code verifier to the token endpoint to [obtain a Customer Account API access token](https://shopify.dev/docs/api/customer#step-obtain-access-token).

### Send the token request

Send a `POST` request with the authorization code and code verifier to the token endpoint. The response includes `access_token`, `refresh_token`, and `expires_in`.

***

Access tokens expire after the `expires_in` duration. Refresh an expired token by making a [`POST` request](https://shopify.dev/docs/api/customer#step-using-refresh-token) to the token endpoint with `grant_type` set to `refresh_token`.

Store tokens securely using your platform's built-in security features (or equivalent secure storage):

Use [Keychain Services](https://developer.apple.com/documentation/security/keychain-services) to store tokens on iOS.

**Caution:**

Never store tokens in plain text. Compromised tokens could allow unauthorized access to buyer accounts.

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## Create an authenticated cart

Include the access token in the [buyer identity input](https://shopify.dev/docs/api/storefront/latest/input-objects/CartBuyerIdentityInput#field-customeraccesstoken) of your `cartCreate` mutation. Shopify uses the token to prefill checkout with the buyer's account information.

### Pass buyer identity in the cart mutation

Pass the `access_token` from the token response as the `customerAccessToken` in the cart's `buyerIdentity`. The response includes a `checkoutUrl` that you pass to Checkout Kit.

***

**Caution:**

Present the `checkoutUrl` soon after receiving it. Authenticated checkout URLs expire, and an expired URL starts an unauthenticated checkout session.

[cart​Create mutation](https://shopify.dev/docs/api/storefront/latest/mutations/cartCreate) [Cart​Buyer​Identity​Input](https://shopify.dev/docs/api/storefront/latest/input-objects/CartBuyerIdentityInput)

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## Present the checkout URL in Checkout Kit

Pass the `checkoutUrl` to Checkout Kit's `present()` method. The buyer sees checkout prefilled with their account information.

### Open Checkout Kit with the prefilled URL

Call `present()` with the `checkoutUrl` from the cart response. Checkout Kit opens with the buyer's shipping address and payment methods prefilled.

***

**Note:**

After checkout, sign the buyer out by sending a request to the [logout endpoint](https://shopify.dev/docs/api/customer#logging-out) and clearing any locally stored tokens and authenticated carts.

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## /AuthClient.swift

```swift
import Foundation
import CommonCrypto
import Security
import ShopifyCheckoutSheetKit


class AuthClient {


    let shopDomain: String
    let customerAccountsApiClientId: String
    let customerAccountsApiRedirectUri: String
    let storefrontAccessToken: String


    private var codeVerifier: String?
    private var savedState: String?


    init(shopDomain: String, clientId: String, redirectUri: String, storefrontAccessToken: String) {
        self.shopDomain = shopDomain
        self.customerAccountsApiClientId = clientId
        self.customerAccountsApiRedirectUri = redirectUri
        self.storefrontAccessToken = storefrontAccessToken
    }


    // MARK: - PKCE


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


    private func base64Encode(_ data: Data) -> String {
        data.base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }


    // MARK: - Endpoint Discovery


    func discoverAuthEndpoints() async throws -> (String, String) {
        let discoveryUrl = URL(string: "https://\(shopDomain)/.well-known/openid-configuration")!
        let (data, _) = try await URLSession.shared.data(from: discoveryUrl)
        let config = try JSONSerialization.jsonObject(with: data) as! [String: Any]


        let authEndpoint = config["authorization_endpoint"] as! String
        let tokenEndpoint = config["token_endpoint"] as! String


        return (authEndpoint, tokenEndpoint)
    }


    // MARK: - Authorization URL


    func buildAuthorizationUrl() async throws -> String? {
        let codeVerifier = generateCodeVerifier()
        self.codeVerifier = codeVerifier


        let codeChallenge = generateCodeChallenge(for: codeVerifier)
        let (authEndpoint, _) = try await discoverAuthEndpoints()
        let state = generateRandomString(length: 36)
        self.savedState = state


        var components = URLComponents(string: authEndpoint)
        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: state),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256")
        ]


        return components?.url?.absoluteString
    }


    private func generateRandomString(length: Int) -> String {
        let characters = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
        var bytes = [UInt8](repeating: 0, count: length)
        _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        return String(bytes.map { characters[Int($0) % characters.count] })
    }


    // MARK: - Callback Handling


    func handleCallback(url: URL) -> String? {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
              let configuredComponents = URLComponents(string: customerAccountsApiRedirectUri),
              components.scheme == configuredComponents.scheme,
              components.host == configuredComponents.host else {
            return nil
        }


        guard let queryItems = components.queryItems else { return nil }


        guard let state = queryItems.first(where: { $0.name == "state" })?.value,
              state == savedState else {
            return nil
        }


        return queryItems.first(where: { $0.name == "code" })?.value
    }


    // MARK: - Token Exchange


    func requestAccessToken(code: String) async throws -> OAuthTokenResult {
        guard let codeVerifier = self.codeVerifier else {
            fatalError("Code verifier not found. Call buildAuthorizationUrl() first.")
        }


        let (_, tokenEndpoint) = try await discoverAuthEndpoints()
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "grant_type", value: "authorization_code"),
            URLQueryItem(name: "client_id", value: customerAccountsApiClientId),
            URLQueryItem(name: "redirect_uri", value: customerAccountsApiRedirectUri),
            URLQueryItem(name: "code", value: code),
            URLQueryItem(name: "code_verifier", value: codeVerifier)
        ]


        var request = URLRequest(url: URL(string: tokenEndpoint)!)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = components.percentEncodedQuery?.data(using: .utf8)


        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(OAuthTokenResult.self, from: data)
    }


    // MARK: - Authenticated Cart


    func createAuthenticatedCart(variantId: String, accessToken: String) async throws -> String {
        let query = """
        mutation cartCreate($input: CartInput!) {
            cartCreate(input: $input) {
                cart {
                    id
                    checkoutUrl
                }
                userErrors { field message }
            }
        }
        """


        let variables: [String: Any] = [
            "input": [
                "lines": [["merchandiseId": variantId, "quantity": 1]],
                "buyerIdentity": ["customerAccessToken": accessToken]
            ]
        ]


        let payload: [String: Any] = ["query": query, "variables": variables]
        let body = try JSONSerialization.data(withJSONObject: payload)


        var request = URLRequest(url: URL(string: "https://\(shopDomain)/api/2026-01/graphql.json")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(storefrontAccessToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
        request.httpBody = body


        let (data, _) = try await URLSession.shared.data(for: request)
        let result = try JSONDecoder().decode(CartCreateResponse.self, from: data)


        if let error = result.data.cartCreate.userErrors.first {
            throw CartError.userError(error.message)
        }


        guard let cart = result.data.cartCreate.cart else {
            throw CartError.cartNotCreated
        }


        return cart.checkoutUrl
    }


    // MARK: - Present Checkout


    func presentCheckout(checkoutUrl: URL, from viewController: UIViewController) {
        ShopifyCheckoutSheetKit.present(checkout: checkoutUrl, from: viewController, delegate: viewController as? CheckoutDelegate)
    }
}


struct OAuthTokenResult: Decodable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int


    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
    }
}
```

## Tutorial complete!

You've built an authenticated checkout flow that signs buyers in through the Customer Account API and prefills checkout with their account information.

### Next steps

[Handle offsite payments\
\
](https://shopify.dev/docs/storefronts/mobile/checkout-kit/offsite-payments)

[Bring buyers back to your app after offsite payment redirects.](https://shopify.dev/docs/storefronts/mobile/checkout-kit/offsite-payments)

[Privacy compliance\
\
](https://shopify.dev/docs/storefronts/mobile/checkout-kit/privacy-compliance)

[Pass visitor consent for GDPR, CCPA, and App Tracking Transparency.](https://shopify.dev/docs/storefronts/mobile/checkout-kit/privacy-compliance)

[Accelerated checkouts\
\
](https://shopify.dev/docs/storefronts/mobile/checkout-kit/accelerated-checkouts-overview)

[Add Shop Pay and Apple Pay buttons for one-tap purchases.](https://shopify.dev/docs/storefronts/mobile/checkout-kit/accelerated-checkouts-overview)
