Skip to content

Commit f3e7bd2

Browse files
authored
Merge pull request #793 from Iterable/feature/JWTImrovements
[MOB- 9274]- Merging JWT feature into master
2 parents a78eea7 + cd2a283 commit f3e7bd2

19 files changed

+368
-78
lines changed

swift-sdk.xcodeproj/project.pbxproj

+12
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,11 @@
401401
ACFF42B02465B4AE00FDF10D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFF42AF2465B4AE00FDF10D /* AppDelegate.swift */; };
402402
BA2BB8192BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
403403
BA2BB81A2BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
404+
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9003E002BF4DF15004AB45B /* RetryPolicy.swift */; };
404405
E9BF47962B46D5DC0033DB69 /* IterableEmbeddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BF47952B46D5DC0033DB69 /* IterableEmbeddedView.swift */; };
405406
E9BF47982B46DEB30033DB69 /* IterableEmbeddedView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9BF47972B46DEB30033DB69 /* IterableEmbeddedView.xib */; };
407+
E9FF7FD12BFCBD90000409ED /* AuthFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */; };
408+
E9FF7FD32BFCBDB9000409ED /* AuthFailureReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */; };
406409
/* End PBXBuildFile section */
407410

408411
/* Begin PBXContainerItemProxy section */
@@ -805,8 +808,11 @@
805808
ACFF42AE24656ECF00FDF10D /* ui-tests-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ui-tests-app.entitlements"; sourceTree = "<group>"; };
806809
ACFF42AF2465B4AE00FDF10D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
807810
BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
811+
E9003E002BF4DF15004AB45B /* RetryPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryPolicy.swift; sourceTree = "<group>"; };
808812
E9BF47952B46D5DC0033DB69 /* IterableEmbeddedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IterableEmbeddedView.swift; sourceTree = "<group>"; };
809813
E9BF47972B46DEB30033DB69 /* IterableEmbeddedView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IterableEmbeddedView.xib; sourceTree = "<group>"; };
814+
E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFailure.swift; sourceTree = "<group>"; };
815+
E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFailureReason.swift; sourceTree = "<group>"; };
810816
/* End PBXFileReference section */
811817

812818
/* Begin PBXFrameworksBuildPhase section */
@@ -1063,6 +1069,9 @@
10631069
AC3C10F8213F46A900A9B839 /* IterableLogging.swift */,
10641070
ACA8D1A221910C66001B1332 /* IterableMessaging.swift */,
10651071
AC78F0E6253D7F09006378A5 /* IterablePushNotificationMetadata.swift */,
1072+
E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */,
1073+
E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */,
1074+
E9003E002BF4DF15004AB45B /* RetryPolicy.swift */,
10661075
);
10671076
path = "swift-sdk";
10681077
sourceTree = "<group>";
@@ -2089,6 +2098,7 @@
20892098
AC72A0D220CF4D12004D7997 /* IterableUtil.swift in Sources */,
20902099
AC32E16821DD55B900BD4F83 /* OrderedDictionary.swift in Sources */,
20912100
ACF406232507BC72005FD775 /* NetworkMonitor.swift in Sources */,
2101+
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */,
20922102
AC1712892416AEF400F2BB0E /* WebViewProtocol.swift in Sources */,
20932103
AC3C10F9213F46A900A9B839 /* IterableLogging.swift in Sources */,
20942104
55DD207F26A0D83800773CC7 /* IterableAuthManagerProtocol.swift in Sources */,
@@ -2117,6 +2127,7 @@
21172127
551FA7582988A8FC0072D0A9 /* IterableEmbeddedManagerProtocol.swift in Sources */,
21182128
AC776DA6211A1B8A00C27C27 /* IterableRequestUtil.swift in Sources */,
21192129
55DD2053269FA28200773CC7 /* IterableInAppManagerProtocol.swift in Sources */,
2130+
E9FF7FD32BFCBDB9000409ED /* AuthFailureReason.swift in Sources */,
21202131
ACEDF41D2183C2EC000B9BFE /* Pending.swift in Sources */,
21212132
552A0AA7280E1FDA00A80963 /* DeepLinkManager.swift in Sources */,
21222133
E9BF47962B46D5DC0033DB69 /* IterableEmbeddedView.swift in Sources */,
@@ -2149,6 +2160,7 @@
21492160
AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */,
21502161
ACD2B84F25B15CFA005D7A90 /* RequestSender.swift in Sources */,
21512162
AC3A336D24F65579008225BA /* RequestProcessorUtil.swift in Sources */,
2163+
E9FF7FD12BFCBD90000409ED /* AuthFailure.swift in Sources */,
21522164
55DD2041269FA24400773CC7 /* IterableInAppMessage.swift in Sources */,
21532165
AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */,
21542166
AC1AA1C624EBB2DC00F29C6B /* IterableTaskRunner.swift in Sources */,

swift-sdk/AuthFailure.swift

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// AuthFailure.swift
3+
// swift-sdk
4+
//
5+
// Created by HARDIK MASHRU on 21/05/24.
6+
// Copyright © 2024 Iterable. All rights reserved.
7+
//
8+
9+
import Foundation
10+
@objc public class AuthFailure: NSObject {
11+
12+
/// userId or email of the signed-in user
13+
public let userKey: String?
14+
15+
/// the authToken which caused the failure
16+
public let failedAuthToken: String?
17+
18+
/// the timestamp of the failed request
19+
public let failedRequestTime: Int
20+
21+
/// indicates a reason for failure
22+
public let failureReason: AuthFailureReason
23+
24+
public init(userKey: String?,
25+
failedAuthToken: String?,
26+
failedRequestTime: Int,
27+
failureReason: AuthFailureReason) {
28+
self.userKey = userKey
29+
self.failedAuthToken = failedAuthToken
30+
self.failedRequestTime = failedRequestTime
31+
self.failureReason = failureReason
32+
}
33+
}

swift-sdk/AuthFailureReason.swift

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// AuthFailureReason.swift
3+
// swift-sdk
4+
//
5+
// Created by HARDIK MASHRU on 21/05/24.
6+
// Copyright © 2024 Iterable. All rights reserved.
7+
//
8+
9+
import Foundation
10+
@objc public enum AuthFailureReason: Int {
11+
case authTokenExpired
12+
case authTokenGenericError
13+
case authTokenExpirationInvalid
14+
case authTokenSignatureInvalid
15+
case authTokenFormatInvalid
16+
case authTokenInvalidated
17+
case authTokenPayloadInvalid
18+
case authTokenUserKeyInvalid
19+
case authTokenNull
20+
case authTokenGenerationError
21+
case authTokenMissing
22+
}

swift-sdk/Constants.swift

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum Const {
1515

1616
static let deepLinkRegex = "/a/[a-zA-Z0-9]+"
1717
static let href = "href"
18+
static let exponentialFactor = 2.0
1819

1920
enum Http {
2021
static let GET = "GET"
@@ -286,6 +287,8 @@ enum JsonValue {
286287
enum Code {
287288
static let badApiKey = "BadApiKey"
288289
static let invalidJwtPayload = "InvalidJwtPayload"
290+
static let badAuthorizationHeader = "BadAuthorizationHeader"
291+
static let jwtUserIdentifiersMismatched = "JwtUserIdentifiersMismatched"
289292
}
290293
}
291294

swift-sdk/Internal/AuthManager.swift

+92-27
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import Foundation
66

77
class AuthManager: IterableAuthManagerProtocol {
88
init(delegate: IterableAuthDelegate?,
9+
authRetryPolicy: RetryPolicy,
910
expirationRefreshPeriod: TimeInterval,
1011
localStorage: LocalStorageProtocol,
1112
dateProvider: DateProviderProtocol) {
1213
ITBInfo()
1314

1415
self.delegate = delegate
16+
self.authRetryPolicy = authRetryPolicy
1517
self.localStorage = localStorage
1618
self.dateProvider = dateProvider
1719
self.expirationRefreshPeriod = expirationRefreshPeriod
@@ -35,26 +37,44 @@ class AuthManager: IterableAuthManagerProtocol {
3537
hasFailedPriorAuth = false
3638
}
3739

38-
func requestNewAuthToken(hasFailedPriorAuth: Bool = false, onSuccess: AuthTokenRetrievalHandler? = nil) {
40+
func requestNewAuthToken(hasFailedPriorAuth: Bool = false,
41+
onSuccess: AuthTokenRetrievalHandler? = nil,
42+
shouldIgnoreRetryPolicy: Bool) {
3943
ITBInfo()
4044

41-
guard !pendingAuth else {
42-
return
43-
}
44-
45-
guard !self.hasFailedPriorAuth || !hasFailedPriorAuth else {
45+
if shouldPauseRetry(shouldIgnoreRetryPolicy) || pendingAuth || hasFailedAuth(hasFailedPriorAuth) {
4646
return
4747
}
4848

4949
self.hasFailedPriorAuth = hasFailedPriorAuth
50-
5150
pendingAuth = true
5251

52+
if shouldUseLastValidToken(shouldIgnoreRetryPolicy) {
53+
// if some JWT retry had valid token it will not fetch the auth token again from developer function
54+
onAuthTokenReceived(retrievedAuthToken: authToken, onSuccess: onSuccess)
55+
return
56+
}
57+
5358
delegate?.onAuthTokenRequested { [weak self] retrievedAuthToken in
59+
self?.pendingAuth = false
60+
self?.retryCount+=1
5461
self?.onAuthTokenReceived(retrievedAuthToken: retrievedAuthToken, onSuccess: onSuccess)
5562
}
5663
}
5764

65+
private func hasFailedAuth(_ hasFailedPriorAuth: Bool) -> Bool {
66+
return self.hasFailedPriorAuth && hasFailedPriorAuth
67+
}
68+
69+
private func shouldPauseRetry(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
70+
return (!shouldIgnoreRetryPolicy && pauseAuthRetry) ||
71+
(retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)
72+
}
73+
74+
private func shouldUseLastValidToken(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
75+
return isLastAuthTokenValid && !shouldIgnoreRetryPolicy
76+
}
77+
5878
func setNewToken(_ newToken: String) {
5979
ITBInfo()
6080

@@ -69,6 +89,7 @@ class AuthManager: IterableAuthManagerProtocol {
6989
storeAuthToken()
7090

7191
clearRefreshTimer()
92+
isLastAuthTokenValid = false
7293
}
7394

7495
// MARK: - Private/Internal
@@ -79,11 +100,39 @@ class AuthManager: IterableAuthManagerProtocol {
79100
private var pendingAuth: Bool = false
80101
private var hasFailedPriorAuth: Bool = false
81102

103+
private var authRetryPolicy: RetryPolicy
104+
private var retryCount: Int = 0
105+
private var isLastAuthTokenValid: Bool = false
106+
private var pauseAuthRetry: Bool = false
107+
private var isTimerScheduled: Bool = false
108+
82109
private weak var delegate: IterableAuthDelegate?
83110
private let expirationRefreshPeriod: TimeInterval
84111
private var localStorage: LocalStorageProtocol
85112
private let dateProvider: DateProviderProtocol
86113

114+
func pauseAuthRetries(_ pauseAuthRetry: Bool) {
115+
self.pauseAuthRetry = pauseAuthRetry
116+
resetRetryCount()
117+
}
118+
119+
func setIsLastAuthTokenValid(_ isValid: Bool) {
120+
isLastAuthTokenValid = isValid
121+
}
122+
123+
func getNextRetryInterval() -> Double {
124+
var nextRetryInterval = Double(authRetryPolicy.retryInterval)
125+
if authRetryPolicy.retryBackoff == .exponential {
126+
nextRetryInterval = Double(nextRetryInterval) * pow(Const.exponentialFactor, Double(retryCount - 1))
127+
}
128+
129+
return nextRetryInterval
130+
}
131+
132+
private func resetRetryCount() {
133+
retryCount = 0
134+
}
135+
87136
private func storeAuthToken() {
88137
localStorage.authToken = authToken
89138
}
@@ -93,67 +142,83 @@ class AuthManager: IterableAuthManagerProtocol {
93142

94143
authToken = localStorage.authToken
95144

96-
queueAuthTokenExpirationRefresh(authToken)
145+
_ = queueAuthTokenExpirationRefresh(authToken)
97146
}
98147

99148
private func onAuthTokenReceived(retrievedAuthToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) {
100149
ITBInfo()
101150

102151
pendingAuth = false
103152

104-
guard retrievedAuthToken != nil else {
105-
delegate?.onTokenRegistrationFailed("auth token was nil, scheduling auth token retrieval in 10 seconds")
106-
107-
/// by default, schedule a refresh for 10s
108-
scheduleAuthTokenRefreshTimer(10)
109-
110-
return
153+
if retrievedAuthToken != nil {
154+
let isRefreshQueued = queueAuthTokenExpirationRefresh(retrievedAuthToken, onSuccess: onSuccess)
155+
if !isRefreshQueued {
156+
onSuccess?(authToken)
157+
}
158+
} else {
159+
handleAuthFailure(failedAuthToken: nil, reason: .authTokenNull)
160+
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)
111161
}
112162

113163
authToken = retrievedAuthToken
114164

115165
storeAuthToken()
116-
117-
queueAuthTokenExpirationRefresh(authToken)
118-
119-
onSuccess?(authToken)
120166
}
121167

122-
private func queueAuthTokenExpirationRefresh(_ authToken: String?) {
168+
func handleAuthFailure(failedAuthToken: String?, reason: AuthFailureReason) {
169+
delegate?.onAuthFailure(AuthFailure(userKey: IterableUtil.getEmailOrUserId(), failedAuthToken: failedAuthToken, failedRequestTime: IterableUtil.secondsFromEpoch(for: dateProvider.currentDate), failureReason: reason))
170+
}
171+
172+
private func queueAuthTokenExpirationRefresh(_ authToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) -> Bool {
123173
ITBInfo()
124174

125175
clearRefreshTimer()
126176

127177
guard let authToken = authToken, let expirationDate = AuthManager.decodeExpirationDateFromAuthToken(authToken) else {
128-
delegate?.onTokenRegistrationFailed("auth token was nil or could not decode an expiration date, scheduling auth token retrieval in 10 seconds")
178+
handleAuthFailure(failedAuthToken: authToken, reason: .authTokenPayloadInvalid)
129179

130180
/// schedule a default timer of 10 seconds if we fall into this case
131-
scheduleAuthTokenRefreshTimer(10)
181+
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)
132182

133-
return
183+
return true
134184
}
135185

136186
let timeIntervalToRefresh = TimeInterval(expirationDate) - dateProvider.currentDate.timeIntervalSince1970 - expirationRefreshPeriod
137-
138-
scheduleAuthTokenRefreshTimer(timeIntervalToRefresh)
187+
if timeIntervalToRefresh > 0 {
188+
scheduleAuthTokenRefreshTimer(interval: timeIntervalToRefresh, isScheduledRefresh: true, successCallback: onSuccess)
189+
return true
190+
}
191+
return false
139192
}
140193

141-
private func scheduleAuthTokenRefreshTimer(_ interval: TimeInterval) {
194+
func scheduleAuthTokenRefreshTimer(interval: TimeInterval, isScheduledRefresh: Bool = false, successCallback: AuthTokenRetrievalHandler? = nil) {
142195
ITBInfo()
196+
if shouldSkipTokenRefresh(isScheduledRefresh: isScheduledRefresh) {
197+
// we only stop schedule token refresh if it is called from retry (in case of failure). The normal auth token refresh schedule would work
198+
return
199+
}
143200

144201
expirationRefreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
202+
self?.isTimerScheduled = false
145203
if self?.localStorage.email != nil || self?.localStorage.userId != nil {
146-
self?.requestNewAuthToken(hasFailedPriorAuth: false)
204+
self?.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: successCallback, shouldIgnoreRetryPolicy: isScheduledRefresh)
147205
} else {
148206
ITBDebug("Email or userId is not available. Skipping token refresh")
149207
}
150208
}
209+
210+
isTimerScheduled = true
211+
}
212+
213+
private func shouldSkipTokenRefresh(isScheduledRefresh: Bool) -> Bool {
214+
return (pauseAuthRetry && !isScheduledRefresh) || isTimerScheduled
151215
}
152216

153217
private func clearRefreshTimer() {
154218
ITBInfo()
155219

156220
expirationRefreshTimer?.invalidate()
221+
isTimerScheduled = false
157222
expirationRefreshTimer = nil
158223
}
159224

swift-sdk/Internal/DependencyContainerProtocol.swift

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension DependencyContainerProtocol {
5151

5252
func createAuthManager(config: IterableConfig) -> IterableAuthManagerProtocol {
5353
AuthManager(delegate: config.authDelegate,
54+
authRetryPolicy: config.retryPolicy,
5455
expirationRefreshPeriod: config.expiringAuthTokenRefreshPeriod,
5556
localStorage: localStorage,
5657
dateProvider: dateProvider)

swift-sdk/Internal/InternalIterableAPI.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
582582
private func onLogin(_ authToken: String? = nil) {
583583
ITBInfo()
584584

585+
self.authManager.pauseAuthRetries(false)
585586
if let authToken = authToken {
586587
self.authManager.setNewToken(authToken)
587588
completeUserLogin()
@@ -599,7 +600,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
599600
if token != nil {
600601
self?.completeUserLogin()
601602
}
602-
})
603+
}, shouldIgnoreRetryPolicy: true)
603604
}
604605

605606
private func completeUserLogin() {

swift-sdk/Internal/IterableUtil.swift

+11
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ import UIKit
5151
static func secondsFromEpoch(for date: Date) -> Int {
5252
Int(date.timeIntervalSince1970)
5353
}
54+
55+
static func getEmailOrUserId() -> String? {
56+
let email = IterableAPI.email
57+
let userId = IterableAPI.userId
58+
if email != nil {
59+
return email
60+
} else if userId != nil {
61+
return userId
62+
}
63+
return nil
64+
}
5465

5566
// given "var1", "val1", "var2", "val2" as input
5667
// this will return "var1: val1, var2: val2"

0 commit comments

Comments
 (0)