Skip to content

Commit f131897

Browse files
committed
Add support to Partner App Authentication
1 parent 45ee427 commit f131897

File tree

6 files changed

+141
-9
lines changed

6 files changed

+141
-9
lines changed

README.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- [Shop Pay](#shop-pay)
3737
- [Customer Account API](#customer-account-api)
3838
- [Offsite Payments](#offsite-payments)
39+
- [App Authentication](#app-authentication)
3940
- [Explore the sample apps](#explore-the-sample-apps)
4041
- [Contributing](#contributing)
4142
- [License](#license)
@@ -545,7 +546,79 @@ public func checkoutDidClickLink(url: URL) {
545546
}
546547
```
547548

548-
---
549+
## App Authentication
550+
551+
App Authentication allows your app to securely identify itself to Shopify Checkout Kit and access additional permissions or features granted by Shopify. If your integration requires App Authentication, you must generate a JWT (JSON Web Token) on your secure server and pass it to the SDK.
552+
553+
### How to generate the JWT
554+
555+
**Prerequisites:**
556+
1. Create a Shopify app in the [Shopify Partners Dashboard](https://partners.shopify.com/organizations) or CLI to obtain your `api_key` and `shared_secret`.
557+
2. Install the app on a merchant's shop to obtain an `access_token` via the OAuth flow.
558+
559+
**Encrypt your access token:**
560+
- Use AES-128-CBC with your `shared_secret` to encrypt the `access_token`.
561+
- Derive encryption and signing keys from the SHA-256 hash of your `shared_secret`.
562+
- Base64 encode the result.
563+
564+
<details>
565+
<summary>Pseudo-code (Ruby) for encrypting your access_token</summary>
566+
567+
```ruby
568+
shared_secret = <your_shared_secret>
569+
access_token = <your_access_token>
570+
571+
key_material = OpenSSL::Digest.new("sha256").digest(shared_secret)
572+
encryption_key = key_material[0,16]
573+
signature_key = key_material[16,16]
574+
575+
cipher = OpenSSL::Cipher.new("aes-128-cbc")
576+
cipher.encrypt
577+
cipher.key = encryption_key
578+
cipher.iv = iv = cipher.random_iv
579+
raw_encrypted_token = iv + cipher.update(access_token) + cipher.final
580+
581+
signature = OpenSSL::HMAC.digest("sha256", signature_key, raw_encrypted_token)
582+
encrypted_access_token = Base64.urlsafe_encode64(raw_encrypted_token + signature)
583+
```
584+
</details>
585+
586+
**Create the JWT payload:**
587+
588+
```json
589+
{
590+
"api_key": "<your_api_key>",
591+
"access_token": "<your_encrypted_access_token>",
592+
"iat": <epoch_seconds>,
593+
"jti": "<unique_id>"
594+
}
595+
```
596+
597+
**Sign the JWT:**
598+
599+
```ruby
600+
require 'jwt'
601+
require 'securerandom'
602+
603+
payload = {
604+
api_key: '<your_api_key>',
605+
access_token: '<your_encrypted_access_token>',
606+
iat: Time.now.utc.to_i,
607+
jti: SecureRandom.uuid
608+
}
609+
610+
token = JWT.encode(payload, '<your_shared_secret>', 'HS256')
611+
```
612+
613+
**Use the JWT in your iOS app:**
614+
615+
```swift
616+
let appAuth = CheckoutOptions.AppAuthentication(token: "<JWT_TOKEN>")
617+
let checkoutOptions = CheckoutOptions(appAuthentication: appAuth)
618+
ShopifyCheckoutSheetKit.present(checkout: checkoutURL, from: self, delegate: self, options: checkoutOptions)
619+
```
620+
621+
> **Note:** The JWT should always be generated on a secure server, never on the device.
549622
550623
## Explore the sample apps
551624

Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import UIKit
2525
import SwiftUI
2626

2727
public class CheckoutViewController: UINavigationController {
28-
public init(checkout url: URL, delegate: CheckoutDelegate? = nil) {
29-
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate)
28+
public init(checkout url: URL, delegate: CheckoutDelegate? = nil, options: CheckoutOptions? = nil) {
29+
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, options: options)
3030
rootViewController.notifyPresented()
3131
super.init(rootViewController: rootViewController)
3232
presentationController?.delegate = rootViewController

Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2222
*/
2323

2424
import UIKit
25-
import WebKit
25+
@preconcurrency import WebKit
2626

2727
protocol CheckoutWebViewDelegate: AnyObject {
2828
func checkoutViewDidStartNavigation()
@@ -126,6 +126,8 @@ class CheckoutWebView: WKWebView {
126126
}
127127
var isPreloadRequest: Bool = false
128128

129+
var checkoutOptions: CheckoutOptions?
130+
129131
// MARK: Initializers
130132
init(frame: CGRect = .zero, configuration: WKWebViewConfiguration = WKWebViewConfiguration(), recovery: Bool = false) {
131133
OSLogger.shared.debug("Initializing webview, recovery: \(recovery)")
@@ -231,6 +233,13 @@ class CheckoutWebView: WKWebView {
231233
request.setValue("prefetch", forHTTPHeaderField: "Sec-Purpose")
232234
}
233235

236+
// Add app authentication header if config is provided
237+
if let appAuth = checkoutOptions?.appAuthentication {
238+
let headerValue = "{payload: \(appAuth.token), version: v2}"
239+
request.setValue(headerValue, forHTTPHeaderField: "Shopify-Checkout-Kit-Consumer")
240+
OSLogger.shared.debug("Added app authentication header for checkout")
241+
}
242+
234243
load(request)
235244
}
236245

Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl
4949

5050
// MARK: Initializers
5151

52-
public init(checkoutURL url: URL, delegate: CheckoutDelegate? = nil) {
52+
public init(checkoutURL url: URL, delegate: CheckoutDelegate? = nil, options: CheckoutOptions? = nil) {
5353
self.checkoutURL = url
5454
self.delegate = delegate
5555

@@ -58,6 +58,9 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl
5858
checkoutView.scrollView.contentInsetAdjustmentBehavior = .never
5959
self.checkoutView = checkoutView
6060

61+
// Store checkout configuration for use when loading the checkout
62+
checkoutView.checkoutOptions = options
63+
6164
super.init(nibName: nil, bundle: nil)
6265

6366
title = ShopifyCheckoutSheetKit.configuration.title

Sources/ShopifyCheckoutSheetKit/ShopifyCheckoutSheetKit.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,35 @@ public func configure(_ block: (inout Configuration) -> Void) {
4242
block(&configuration)
4343
}
4444

45+
/// Configuration for partner authentication.
46+
public struct CheckoutOptions {
47+
public struct AppAuthentication {
48+
/// The JWT authentication token.
49+
public let token: String
50+
public init(token: String) {
51+
self.token = token
52+
}
53+
}
54+
55+
/// App authentication configuration for partner identification.
56+
public let appAuthentication: AppAuthentication?
57+
58+
/// Initialize with app authentication configuration.
59+
public init(appAuthentication: AppAuthentication? = nil) {
60+
self.appAuthentication = appAuthentication
61+
}
62+
}
63+
4564
/// Preloads the checkout for faster presentation.
46-
public func preload(checkout url: URL) {
65+
public func preload(checkout url: URL, options: CheckoutOptions? = nil) {
4766
guard configuration.preloading.enabled else {
4867
return
4968
}
5069

5170
CheckoutWebView.preloadingActivatedByClient = true
52-
CheckoutWebView.for(checkout: url).load(checkout: url, isPreload: true)
71+
let webView = CheckoutWebView.for(checkout: url)
72+
webView.checkoutOptions = options
73+
webView.load(checkout: url, isPreload: true)
5374
}
5475

5576
/// Invalidate the checkout cache from preload calls
@@ -59,8 +80,8 @@ public func invalidate() {
5980

6081
/// Presents the checkout from a given `UIViewController`.
6182
@discardableResult
62-
public func present(checkout url: URL, from: UIViewController, delegate: CheckoutDelegate? = nil) -> CheckoutViewController {
63-
let viewController = CheckoutViewController(checkout: url, delegate: delegate)
83+
public func present(checkout url: URL, from: UIViewController, delegate: CheckoutDelegate? = nil, options: CheckoutOptions? = nil) -> CheckoutViewController {
84+
let viewController = CheckoutViewController(checkout: url, delegate: delegate, options: options)
6485
from.present(viewController, animated: true)
6586
return viewController
6687
}

Tests/ShopifyCheckoutSheetKitTests/CheckoutWebViewTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,32 @@ class CheckoutWebViewTests: XCTestCase {
480480

481481
XCTAssertNil(self.mockDelegate.errorReceived)
482482
}
483+
484+
func testAppAuthenticationHeaderIsAddedWithConfig() {
485+
let webView = LoadedRequestObservableWebView()
486+
let appAuthConfig = CheckoutOptions.AppAuthentication(token: "jwt-token-example")
487+
webView.checkoutOptions = CheckoutOptions(appAuthentication: appAuthConfig)
488+
489+
webView.load(
490+
checkout: URL(string: "https://checkout-sdk.myshopify.io")!,
491+
isPreload: false
492+
)
493+
494+
let authHeader = webView.lastLoadedURLRequest?.value(forHTTPHeaderField: "Shopify-Checkout-Kit-Consumer")
495+
XCTAssertEqual(authHeader, "{payload: jwt-token-example, version: v2}")
496+
}
497+
498+
func testAppAuthenticationHeaderNotAddedWithoutConfig() {
499+
let webView = LoadedRequestObservableWebView()
500+
501+
webView.load(
502+
checkout: URL(string: "https://checkout-sdk.myshopify.io")!,
503+
isPreload: false
504+
)
505+
506+
let authHeader = webView.lastLoadedURLRequest?.value(forHTTPHeaderField: "Shopify-Checkout-Kit-Consumer")
507+
XCTAssertNil(authHeader)
508+
}
483509
}
484510

485511
class LoadedRequestObservableWebView: CheckoutWebView {

0 commit comments

Comments
 (0)