Skip to content

Commit fd1336a

Browse files
committed
Update authentication prototype
1 parent 31cd09f commit fd1336a

File tree

9 files changed

+377
-131
lines changed

9 files changed

+377
-131
lines changed

Diff for: Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct AppConfiguration {
2828
public var useVaultedState: Bool = false
2929

3030
/// Pass in customerAccessToken after user logs in
31-
public var useAuthenticatedState: Bool = false
31+
public var useAuthenticatedState: Bool = true
3232

3333
/// Logger to retain Web Pixel events
3434
internal let webPixelsLogger = FileLogger("analytics.txt")

Diff for: Samples/MobileBuyIntegration/MobileBuyIntegration/CustomerAccountClient.swift

+159-59
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class CustomerAccountClient {
3131
// OAuth
3232
static let customerAccountsAudience = "30243aa5-17c1-465a-8493-944bcc4e88aa"
3333
static let authorizationCodeGrantType = "authorization_code"
34+
static let refreshTokenGrantType = "refresh_token"
3435
static let tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
3536
static let accessTokenSubjectTokenType = "urn:ietf:params:oauth:token-type:access_token"
3637
static let customerAPIScope = "https://api.customers.com/auth/customer.graphql"
@@ -44,13 +45,17 @@ class CustomerAccountClient {
4445
private let redirectUri: String
4546
private let customerAccountsBaseUrl: String
4647
private let jsonDecoder: JSONDecoder = JSONDecoder()
48+
private let accessTokenExpirationManager: AccessTokenExpirationManager = AccessTokenExpirationManager.shared
4749

4850
// Note: store tokens in Keychain in any production app
4951
internal var refreshToken: String?
5052
internal var idToken: String?
5153
internal var accessToken: String?
5254
internal var sfApiAccessToken: String?
5355

56+
@Published
57+
var authenticated: Bool = false
58+
5459
init() {
5560
guard
5661
let infoPlist = Bundle.main.infoDictionary,
@@ -67,6 +72,26 @@ class CustomerAccountClient {
6772
self.customerAccountsBaseUrl = "https://shopify.com/\(shopId)"
6873
}
6974

75+
func isAuthenticated() -> Bool {
76+
return self.authenticated
77+
}
78+
79+
func getSfApiAccessToken() -> String? {
80+
return self.sfApiAccessToken
81+
}
82+
83+
func getAccessToken() -> String? {
84+
return self.accessToken
85+
}
86+
87+
func getIdToken() -> String? {
88+
return self.idToken
89+
}
90+
91+
func getRefreshToken() -> String? {
92+
return self.refreshToken
93+
}
94+
7095
func getRedirectUri() -> String {
7196
return self.redirectUri
7297
}
@@ -115,8 +140,8 @@ class CustomerAccountClient {
115140
return
116141
}
117142

118-
if authData.state != state {
119-
callback(nil, "State param does not match")
143+
if authData.state != state {
144+
callback(nil, "State param does not match: \(state)")
120145
return
121146
}
122147

@@ -125,6 +150,79 @@ class CustomerAccountClient {
125150
}
126151
}
127152

153+
func logout(idToken: String, callback: @escaping (String?, String?) -> Void) {
154+
guard let url = URL(string: "https://shopify.com/authentication/\(self.shopId)/logout") else {
155+
return
156+
}
157+
158+
let params: [String: String] = [
159+
"id_token_hint": idToken,
160+
"post_logout_redirect_uri": self.redirectUri
161+
]
162+
163+
executeRequest(
164+
url: url,
165+
method: "GET",
166+
query: params,
167+
completionHandler: { _, response, _ in
168+
let httpResponse = response as? HTTPURLResponse
169+
guard httpResponse != nil, httpResponse?.statusCode == 200 else {
170+
callback(nil, "Failed to logout")
171+
return
172+
}
173+
174+
self.resetAuthentication()
175+
176+
callback("Logged out successfully", nil)
177+
}
178+
)
179+
}
180+
181+
func resetAuthentication() {
182+
self.refreshToken = nil
183+
self.accessToken = nil
184+
self.sfApiAccessToken = nil
185+
self.authenticated = false
186+
}
187+
188+
func refreshAccessToken(refreshToken: String, callback: @escaping (String?, String?) -> Void) {
189+
guard let url = URL(string: "\(self.customerAccountsBaseUrl)/auth/oauth/token") else {
190+
return
191+
}
192+
193+
let params: [String: String] = [
194+
"client_id": clientId,
195+
"grant_type": CustomerAccountClient.refreshTokenGrantType,
196+
"refresh_token": self.refreshToken!
197+
]
198+
199+
executeRequest(
200+
url: url,
201+
headers: [
202+
"Content-Type": CustomerAccountClient.formUrlEncoded
203+
],
204+
body: encodeToFormURLEncoded(parameters: params),
205+
completionHandler: { data, _, _ in
206+
guard let tokenResponse = self.jsonDecoder.decodeOrNil(CustomerAccountRefreshedTokenResponse.self, from: data!) else {
207+
callback(nil, "Couldn't decode refresh access token response")
208+
return
209+
}
210+
211+
self.refreshToken = tokenResponse.refreshToken
212+
self.accessToken = tokenResponse.accessToken
213+
self.accessTokenExpirationManager.addAccessToken(accessToken: tokenResponse.accessToken, expiresIn: tokenResponse.expiresIn)
214+
self.exchangeForStorefrontCustomerAccessToken(
215+
customerAccessToken: tokenResponse.accessToken,
216+
refreshToken: tokenResponse.refreshToken,
217+
callback: callback
218+
)
219+
self.authenticated = true
220+
221+
callback(tokenResponse.accessToken, nil)
222+
}
223+
)
224+
}
225+
128226
/// Requests accessToken with the authorization code and code verifier
129227
private func requestAccessToken(code: String, codeVerifier: String, callback: @escaping (String?, String?) -> Void) {
130228
guard let url = URL(string: "\(self.customerAccountsBaseUrl)/auth/oauth/token") else {
@@ -148,51 +246,26 @@ class CustomerAccountClient {
148246
completionHandler: { data, _, _ in
149247
guard let tokenResponse = self.jsonDecoder.decodeOrNil(CustomerAccountAccessTokenResponse.self, from: data!) else {
150248
print("Couldn't decode access token response")
249+
print("Response:", String(data: data!, encoding: .utf8)!)
151250
return
152251
}
153252

154253
self.idToken = tokenResponse.idToken
155254
self.refreshToken = tokenResponse.refreshToken
156-
self.performTokenExchange(accessToken: tokenResponse.accessToken, callback: callback)
157-
}
158-
)
159-
}
160-
161-
/// Exchanges access token for a new token that can be used to access Customer Accounts API resources
162-
private func performTokenExchange(accessToken: String, callback: @escaping (String?, String?) -> Void) {
163-
guard let url = URL(string: "\(self.customerAccountsBaseUrl)/auth/oauth/token") else {
164-
callback(nil, "Couldn't build token exchange URL")
165-
return
166-
}
167-
168-
let params = [
169-
"grant_type": CustomerAccountClient.tokenExchangeGrantType,
170-
"client_id": self.clientId,
171-
"audience": CustomerAccountClient.customerAccountsAudience,
172-
"subject_token": accessToken,
173-
"subject_token_type": CustomerAccountClient.accessTokenSubjectTokenType,
174-
"scopes": "https://api.customers.com/auth/customer.graphql"
175-
]
176-
177-
executeRequest(
178-
url: url,
179-
headers: [
180-
"Content-Type": CustomerAccountClient.formUrlEncoded
181-
],
182-
body: encodeToFormURLEncoded(parameters: params),
183-
completionHandler: { data, _, _ in
184-
guard let tokenResponse = self.jsonDecoder.decodeOrNil(CustomerAccountExchangeTokenResponse.self, from: data!) else {
185-
callback(nil, "Couldn't decode token exchange response")
186-
return
187-
}
188-
self.accessToken = tokenResponse.accessToken
189-
self.exchangeForStorefrontCustomerAccessToken(customerAccessToken: tokenResponse.accessToken, callback: callback)
255+
self.accessToken = tokenResponse.accessToken
256+
self.accessTokenExpirationManager.addAccessToken(accessToken: tokenResponse.accessToken, expiresIn: tokenResponse.expiresIn)
257+
self.exchangeForStorefrontCustomerAccessToken(
258+
customerAccessToken: tokenResponse.accessToken,
259+
refreshToken: tokenResponse.refreshToken,
260+
callback: callback
261+
)
262+
self.authenticated = true
190263
}
191264
)
192265
}
193266

194267
/// Exchanges a Customer Accounts API access token for a Storefront API customer access token that can be used in Storefront API (and cart mutations)
195-
private func exchangeForStorefrontCustomerAccessToken(customerAccessToken: String, callback: @escaping (String?, String?) -> Void) {
268+
private func exchangeForStorefrontCustomerAccessToken(customerAccessToken: String, refreshToken: String, callback: @escaping (String?, String?) -> Void) {
196269
guard let url = URL(string: "\(customerAccountsBaseUrl)/account/customer/api/2024-07/graphql") else {
197270
return
198271
}
@@ -276,25 +349,56 @@ class CustomerAccountClient {
276349

277350
private func executeRequest(
278351
url: URL,
279-
headers: [String: String],
280-
body: Data?,
352+
headers: [String: String] = [:],
353+
method: String = "POST",
354+
body: Data? = nil,
355+
query: [String: String]? = [:],
281356
completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
282-
) {
283-
var request = URLRequest(url: url)
284-
request.httpMethod = "POST"
357+
) {
358+
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
359+
if query?.isEmpty != true {
360+
components.queryItems = [URLQueryItem]()
361+
}
362+
query?.forEach { components.queryItems?.append(URLQueryItem(name: $0.key, value: $0.value)) }
363+
364+
var request = URLRequest(url: components.url!)
365+
request.httpMethod = method
285366
for (key, value) in headers {
286367
request.setValue(value, forHTTPHeaderField: key)
287368
}
288369
request.httpBody = body
289370

290371
let session = URLSession.shared
291372
let task = session.dataTask(with: request) { data, response, error in
292-
completionHandler(data, response, error)
373+
print("Data: \(String(decoding: data ?? Data(), as: UTF8.self)), Response: \(String(describing: response)), Error: \(String(describing: error))")
374+
completionHandler(data, response, error)
293375
}
294376
task.resume()
295377
}
296378
}
297379

380+
class AccessTokenExpirationManager {
381+
static let shared = AccessTokenExpirationManager()
382+
383+
private var accessTokenExpirationMap: [String: Date]
384+
385+
init() {
386+
self.accessTokenExpirationMap = [:]
387+
}
388+
389+
func addAccessToken(accessToken: String, expiresIn: Int) {
390+
accessTokenExpirationMap[accessToken] = Date().addingTimeInterval(Double(expiresIn))
391+
}
392+
393+
func isAccessTokenExpired(accessToken: String) -> Bool {
394+
return accessTokenExpirationMap[accessToken] != nil && Date() > accessTokenExpirationMap[accessToken]!
395+
}
396+
397+
func getExpirationDate(accessToken: String) -> Date? {
398+
return self.accessTokenExpirationMap[accessToken]
399+
}
400+
}
401+
298402
struct AuthData {
299403
let authorizationUrl: URL
300404
let codeVerifier: String
@@ -306,33 +410,29 @@ struct CustomerAccountAccessTokenResponse: Codable {
306410
let expiresIn: Int
307411
let idToken: String
308412
let refreshToken: String
309-
let scope: String
310413
let tokenType: String
311414

312415
enum CodingKeys: String, CodingKey {
313416
case accessToken = "access_token"
314417
case expiresIn = "expires_in"
315418
case idToken = "id_token"
316419
case refreshToken = "refresh_token"
317-
case scope = "scope"
318420
case tokenType = "token_type"
319421
}
320422
}
321423

322-
struct CustomerAccountExchangeTokenResponse: Codable {
323-
let accessToken: String
324-
let expiresIn: Int
325-
let tokenType: String
326-
let scope: String
327-
let issuedTokenType: String
328-
329-
enum CodingKeys: String, CodingKey {
330-
case accessToken = "access_token"
331-
case expiresIn = "expires_in"
332-
case tokenType = "token_type"
333-
case scope = "scope"
334-
case issuedTokenType = "issued_token_type"
335-
}
424+
struct CustomerAccountRefreshedTokenResponse: Codable {
425+
let accessToken: String
426+
let expiresIn: Int
427+
let refreshToken: String
428+
let tokenType: String
429+
430+
enum CodingKeys: String, CodingKey {
431+
case accessToken = "access_token"
432+
case expiresIn = "expires_in"
433+
case refreshToken = "refresh_token"
434+
case tokenType = "token_type"
435+
}
336436
}
337437

338438
struct MutationRoot: Codable {

Diff for: Samples/MobileBuyIntegration/MobileBuyIntegration/Info.plist

+30-16
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,48 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>StorefrontDomain</key>
6-
<string>$(STOREFRONT_DOMAIN)</string>
7-
<key>ShopId</key>
8-
<string>$(SHOP_ID)</string>
9-
<key>CustomerAccountsClientId</key>
10-
<string>$(CUSTOMER_ACCOUNTS_API_CLIENT_ID)</string>
11-
<key>CustomerAccountsRedirectUri</key>
12-
<string>$(CUSTOMER_ACCOUNTS_API_REDIRECT_URI)</string>
13-
<key>StorefrontAccessToken</key>
14-
<string>$(STOREFRONT_ACCESS_TOKEN)</string>
155
<key>Address1</key>
166
<string>$(ADDRESS_1)</string>
177
<key>Address2</key>
188
<string>$(ADDRESS_2)</string>
9+
<key>CFBundleURLTypes</key>
10+
<array>
11+
<dict>
12+
<key>CFBundleTypeRole</key>
13+
<string>Editor</string>
14+
<key>CFBundleURLName</key>
15+
<string>AppCallback</string>
16+
<key>CFBundleURLSchemes</key>
17+
<array>
18+
<string>shop.63357649154.app</string>
19+
</array>
20+
</dict>
21+
<dict/>
22+
</array>
1923
<key>City</key>
2024
<string>$(CITY)</string>
2125
<key>Country</key>
2226
<string>$(COUNTRY)</string>
27+
<key>CustomerAccountsClientId</key>
28+
<string>$(CUSTOMER_ACCOUNTS_API_CLIENT_ID)</string>
29+
<key>CustomerAccountsRedirectUri</key>
30+
<string>$(CUSTOMER_ACCOUNTS_API_REDIRECT_URI)</string>
31+
<key>Email</key>
32+
<string>$(EMAIL)</string>
2333
<key>FirstName</key>
2434
<string>$(FIRST_NAME)</string>
2535
<key>LastName</key>
2636
<string>$(LAST_NAME)</string>
27-
<key>Province</key>
28-
<string>$(PROVINCE)</string>
29-
<key>Zip</key>
30-
<string>$(ZIP)</string>
31-
<key>Email</key>
32-
<string>$(EMAIL)</string>
3337
<key>Phone</key>
3438
<string>$(PHONE)</string>
39+
<key>Province</key>
40+
<string>$(PROVINCE)</string>
41+
<key>ShopId</key>
42+
<string>$(SHOP_ID)</string>
43+
<key>StorefrontAccessToken</key>
44+
<string>$(STOREFRONT_ACCESS_TOKEN)</string>
45+
<key>StorefrontDomain</key>
46+
<string>$(STOREFRONT_DOMAIN)</string>
3547
<key>UIApplicationSceneManifest</key>
3648
<dict>
3749
<key>UIApplicationSupportsMultipleScenes</key>
@@ -49,5 +61,7 @@
4961
</array>
5062
</dict>
5163
</dict>
64+
<key>Zip</key>
65+
<string>$(ZIP)</string>
5266
</dict>
5367
</plist>

0 commit comments

Comments
 (0)