Skip to content

Commit 27ca4c8

Browse files
committed
Support to Partner App authentication
1 parent 45ee427 commit 27ca4c8

File tree

7 files changed

+153
-14
lines changed

7 files changed

+153
-14
lines changed

README.md

Lines changed: 77 additions & 2 deletions
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)
@@ -114,7 +115,9 @@ import ShopifyCheckoutSheetKit
114115
class MyViewController: UIViewController {
115116
func presentCheckout() {
116117
let checkoutURL: URL = // from cart object
117-
ShopifyCheckoutSheetKit.present(checkout: checkoutURL, from: self, delegate: self)
118+
let appAuth = CheckoutOptions.AppAuthentication(token: "<JWT_TOKEN>")
119+
let checkoutOptions = CheckoutOptions(appAuthentication: appAuth)
120+
ShopifyCheckoutSheetKit.present(checkout: checkoutURL, from: self, delegate: self, options: checkoutOptions)
118121
}
119122
}
120123
```
@@ -545,7 +548,79 @@ public func checkoutDidClickLink(url: URL) {
545548
}
546549
```
547550

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

Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift

Lines changed: 5 additions & 5 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
@@ -60,7 +60,7 @@ extension CheckoutViewController {
6060
}
6161
}
6262

63-
public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutConfigurable {
63+
public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutOptionsConfigurable {
6464
public typealias UIViewControllerType = CheckoutViewController
6565

6666
var checkoutURL: URL
@@ -144,14 +144,14 @@ public class CheckoutDelegateWrapper: CheckoutDelegate {
144144
}
145145
}
146146

147-
public protocol CheckoutConfigurable {
147+
public protocol CheckoutOptionsConfigurable {
148148
func backgroundColor(_ color: UIColor) -> Self
149149
func colorScheme(_ colorScheme: ShopifyCheckoutSheetKit.Configuration.ColorScheme) -> Self
150150
func tintColor(_ color: UIColor) -> Self
151151
func title(_ title: String) -> Self
152152
}
153153

154-
extension CheckoutConfigurable {
154+
extension CheckoutOptionsConfigurable {
155155
@discardableResult public func backgroundColor(_ color: UIColor) -> Self {
156156
ShopifyCheckoutSheetKit.configuration.backgroundColor = color
157157
return self

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: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,40 @@ 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+
// In the future, other configurations can be added here:
64+
// public let branding: BrandingConfig?
65+
// public let analytics: AnalyticsConfig?
66+
// etc.
67+
}
68+
4569
/// Preloads the checkout for faster presentation.
46-
public func preload(checkout url: URL) {
70+
public func preload(checkout url: URL, options: CheckoutOptions? = nil) {
4771
guard configuration.preloading.enabled else {
4872
return
4973
}
5074

5175
CheckoutWebView.preloadingActivatedByClient = true
52-
CheckoutWebView.for(checkout: url).load(checkout: url, isPreload: true)
76+
let webView = CheckoutWebView.for(checkout: url)
77+
webView.checkoutOptions = options
78+
webView.load(checkout: url, isPreload: true)
5379
}
5480

5581
/// Invalidate the checkout cache from preload calls
@@ -59,8 +85,8 @@ public func invalidate() {
5985

6086
/// Presents the checkout from a given `UIViewController`.
6187
@discardableResult
62-
public func present(checkout url: URL, from: UIViewController, delegate: CheckoutDelegate? = nil) -> CheckoutViewController {
63-
let viewController = CheckoutViewController(checkout: url, delegate: delegate)
88+
public func present(checkout url: URL, from: UIViewController, delegate: CheckoutDelegate? = nil, options: CheckoutOptions? = nil) -> CheckoutViewController {
89+
let viewController = CheckoutViewController(checkout: url, delegate: delegate, options: options)
6490
from.present(viewController, animated: true)
6591
return viewController
6692
}

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 {

Tests/ShopifyCheckoutSheetKitTests/SwiftUITests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class CheckoutSheetTests: XCTestCase {
121121
}
122122
}
123123

124-
class CheckoutConfigurableTests: XCTestCase {
124+
class CheckoutOptionsConfigurableTests: XCTestCase {
125125
var checkoutURL: URL!
126126
var checkoutSheet: CheckoutSheet!
127127

0 commit comments

Comments
 (0)