Skip to content

Commit

Permalink
Merge pull request #793 from Iterable/feature/JWTImrovements
Browse files Browse the repository at this point in the history
[MOB- 9274]- Merging JWT feature into master
  • Loading branch information
Ayyanchira authored Aug 7, 2024
2 parents a78eea7 + cd2a283 commit f3e7bd2
Show file tree
Hide file tree
Showing 19 changed files with 368 additions and 78 deletions.
12 changes: 12 additions & 0 deletions swift-sdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,11 @@
ACFF42B02465B4AE00FDF10D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFF42AF2465B4AE00FDF10D /* AppDelegate.swift */; };
BA2BB8192BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
BA2BB81A2BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9003E002BF4DF15004AB45B /* RetryPolicy.swift */; };
E9BF47962B46D5DC0033DB69 /* IterableEmbeddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BF47952B46D5DC0033DB69 /* IterableEmbeddedView.swift */; };
E9BF47982B46DEB30033DB69 /* IterableEmbeddedView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9BF47972B46DEB30033DB69 /* IterableEmbeddedView.xib */; };
E9FF7FD12BFCBD90000409ED /* AuthFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */; };
E9FF7FD32BFCBDB9000409ED /* AuthFailureReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1063,6 +1069,9 @@
AC3C10F8213F46A900A9B839 /* IterableLogging.swift */,
ACA8D1A221910C66001B1332 /* IterableMessaging.swift */,
AC78F0E6253D7F09006378A5 /* IterablePushNotificationMetadata.swift */,
E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */,
E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */,
E9003E002BF4DF15004AB45B /* RetryPolicy.swift */,
);
path = "swift-sdk";
sourceTree = "<group>";
Expand Down Expand Up @@ -2089,6 +2098,7 @@
AC72A0D220CF4D12004D7997 /* IterableUtil.swift in Sources */,
AC32E16821DD55B900BD4F83 /* OrderedDictionary.swift in Sources */,
ACF406232507BC72005FD775 /* NetworkMonitor.swift in Sources */,
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */,
AC1712892416AEF400F2BB0E /* WebViewProtocol.swift in Sources */,
AC3C10F9213F46A900A9B839 /* IterableLogging.swift in Sources */,
55DD207F26A0D83800773CC7 /* IterableAuthManagerProtocol.swift in Sources */,
Expand Down Expand Up @@ -2117,6 +2127,7 @@
551FA7582988A8FC0072D0A9 /* IterableEmbeddedManagerProtocol.swift in Sources */,
AC776DA6211A1B8A00C27C27 /* IterableRequestUtil.swift in Sources */,
55DD2053269FA28200773CC7 /* IterableInAppManagerProtocol.swift in Sources */,
E9FF7FD32BFCBDB9000409ED /* AuthFailureReason.swift in Sources */,
ACEDF41D2183C2EC000B9BFE /* Pending.swift in Sources */,
552A0AA7280E1FDA00A80963 /* DeepLinkManager.swift in Sources */,
E9BF47962B46D5DC0033DB69 /* IterableEmbeddedView.swift in Sources */,
Expand Down Expand Up @@ -2149,6 +2160,7 @@
AC347B5C20E5A7E1003449CF /* APNSTypeChecker.swift in Sources */,
ACD2B84F25B15CFA005D7A90 /* RequestSender.swift in Sources */,
AC3A336D24F65579008225BA /* RequestProcessorUtil.swift in Sources */,
E9FF7FD12BFCBD90000409ED /* AuthFailure.swift in Sources */,
55DD2041269FA24400773CC7 /* IterableInAppMessage.swift in Sources */,
AC8E7CA524C7555E0039605F /* CoreDataUtil.swift in Sources */,
AC1AA1C624EBB2DC00F29C6B /* IterableTaskRunner.swift in Sources */,
Expand Down
33 changes: 33 additions & 0 deletions swift-sdk/AuthFailure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// AuthFailure.swift
// swift-sdk
//
// Created by HARDIK MASHRU on 21/05/24.
// Copyright © 2024 Iterable. All rights reserved.
//

import Foundation
@objc public class AuthFailure: NSObject {

/// userId or email of the signed-in user
public let userKey: String?

/// the authToken which caused the failure
public let failedAuthToken: String?

/// the timestamp of the failed request
public let failedRequestTime: Int

/// indicates a reason for failure
public let failureReason: AuthFailureReason

public init(userKey: String?,
failedAuthToken: String?,
failedRequestTime: Int,
failureReason: AuthFailureReason) {
self.userKey = userKey
self.failedAuthToken = failedAuthToken
self.failedRequestTime = failedRequestTime
self.failureReason = failureReason
}
}
22 changes: 22 additions & 0 deletions swift-sdk/AuthFailureReason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// AuthFailureReason.swift
// swift-sdk
//
// Created by HARDIK MASHRU on 21/05/24.
// Copyright © 2024 Iterable. All rights reserved.
//

import Foundation
@objc public enum AuthFailureReason: Int {
case authTokenExpired
case authTokenGenericError
case authTokenExpirationInvalid
case authTokenSignatureInvalid
case authTokenFormatInvalid
case authTokenInvalidated
case authTokenPayloadInvalid
case authTokenUserKeyInvalid
case authTokenNull
case authTokenGenerationError
case authTokenMissing
}
3 changes: 3 additions & 0 deletions swift-sdk/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum Const {

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

enum Http {
static let GET = "GET"
Expand Down Expand Up @@ -286,6 +287,8 @@ enum JsonValue {
enum Code {
static let badApiKey = "BadApiKey"
static let invalidJwtPayload = "InvalidJwtPayload"
static let badAuthorizationHeader = "BadAuthorizationHeader"
static let jwtUserIdentifiersMismatched = "JwtUserIdentifiersMismatched"
}
}

Expand Down
119 changes: 92 additions & 27 deletions swift-sdk/Internal/AuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import Foundation

class AuthManager: IterableAuthManagerProtocol {
init(delegate: IterableAuthDelegate?,
authRetryPolicy: RetryPolicy,
expirationRefreshPeriod: TimeInterval,
localStorage: LocalStorageProtocol,
dateProvider: DateProviderProtocol) {
ITBInfo()

self.delegate = delegate
self.authRetryPolicy = authRetryPolicy
self.localStorage = localStorage
self.dateProvider = dateProvider
self.expirationRefreshPeriod = expirationRefreshPeriod
Expand All @@ -35,26 +37,44 @@ class AuthManager: IterableAuthManagerProtocol {
hasFailedPriorAuth = false
}

func requestNewAuthToken(hasFailedPriorAuth: Bool = false, onSuccess: AuthTokenRetrievalHandler? = nil) {
func requestNewAuthToken(hasFailedPriorAuth: Bool = false,
onSuccess: AuthTokenRetrievalHandler? = nil,
shouldIgnoreRetryPolicy: Bool) {
ITBInfo()

guard !pendingAuth else {
return
}

guard !self.hasFailedPriorAuth || !hasFailedPriorAuth else {
if shouldPauseRetry(shouldIgnoreRetryPolicy) || pendingAuth || hasFailedAuth(hasFailedPriorAuth) {
return
}

self.hasFailedPriorAuth = hasFailedPriorAuth

pendingAuth = true

if shouldUseLastValidToken(shouldIgnoreRetryPolicy) {
// if some JWT retry had valid token it will not fetch the auth token again from developer function
onAuthTokenReceived(retrievedAuthToken: authToken, onSuccess: onSuccess)
return
}

delegate?.onAuthTokenRequested { [weak self] retrievedAuthToken in
self?.pendingAuth = false
self?.retryCount+=1
self?.onAuthTokenReceived(retrievedAuthToken: retrievedAuthToken, onSuccess: onSuccess)
}
}

private func hasFailedAuth(_ hasFailedPriorAuth: Bool) -> Bool {
return self.hasFailedPriorAuth && hasFailedPriorAuth
}

private func shouldPauseRetry(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
return (!shouldIgnoreRetryPolicy && pauseAuthRetry) ||
(retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)
}

private func shouldUseLastValidToken(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
return isLastAuthTokenValid && !shouldIgnoreRetryPolicy
}

func setNewToken(_ newToken: String) {
ITBInfo()

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

clearRefreshTimer()
isLastAuthTokenValid = false
}

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

private var authRetryPolicy: RetryPolicy
private var retryCount: Int = 0
private var isLastAuthTokenValid: Bool = false
private var pauseAuthRetry: Bool = false
private var isTimerScheduled: Bool = false

private weak var delegate: IterableAuthDelegate?
private let expirationRefreshPeriod: TimeInterval
private var localStorage: LocalStorageProtocol
private let dateProvider: DateProviderProtocol

func pauseAuthRetries(_ pauseAuthRetry: Bool) {
self.pauseAuthRetry = pauseAuthRetry
resetRetryCount()
}

func setIsLastAuthTokenValid(_ isValid: Bool) {
isLastAuthTokenValid = isValid
}

func getNextRetryInterval() -> Double {
var nextRetryInterval = Double(authRetryPolicy.retryInterval)
if authRetryPolicy.retryBackoff == .exponential {
nextRetryInterval = Double(nextRetryInterval) * pow(Const.exponentialFactor, Double(retryCount - 1))
}

return nextRetryInterval
}

private func resetRetryCount() {
retryCount = 0
}

private func storeAuthToken() {
localStorage.authToken = authToken
}
Expand All @@ -93,67 +142,83 @@ class AuthManager: IterableAuthManagerProtocol {

authToken = localStorage.authToken

queueAuthTokenExpirationRefresh(authToken)
_ = queueAuthTokenExpirationRefresh(authToken)
}

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

pendingAuth = false

guard retrievedAuthToken != nil else {
delegate?.onTokenRegistrationFailed("auth token was nil, scheduling auth token retrieval in 10 seconds")

/// by default, schedule a refresh for 10s
scheduleAuthTokenRefreshTimer(10)

return
if retrievedAuthToken != nil {
let isRefreshQueued = queueAuthTokenExpirationRefresh(retrievedAuthToken, onSuccess: onSuccess)
if !isRefreshQueued {
onSuccess?(authToken)
}
} else {
handleAuthFailure(failedAuthToken: nil, reason: .authTokenNull)
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)
}

authToken = retrievedAuthToken

storeAuthToken()

queueAuthTokenExpirationRefresh(authToken)

onSuccess?(authToken)
}

private func queueAuthTokenExpirationRefresh(_ authToken: String?) {
func handleAuthFailure(failedAuthToken: String?, reason: AuthFailureReason) {
delegate?.onAuthFailure(AuthFailure(userKey: IterableUtil.getEmailOrUserId(), failedAuthToken: failedAuthToken, failedRequestTime: IterableUtil.secondsFromEpoch(for: dateProvider.currentDate), failureReason: reason))
}

private func queueAuthTokenExpirationRefresh(_ authToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) -> Bool {
ITBInfo()

clearRefreshTimer()

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

/// schedule a default timer of 10 seconds if we fall into this case
scheduleAuthTokenRefreshTimer(10)
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)

return
return true
}

let timeIntervalToRefresh = TimeInterval(expirationDate) - dateProvider.currentDate.timeIntervalSince1970 - expirationRefreshPeriod

scheduleAuthTokenRefreshTimer(timeIntervalToRefresh)
if timeIntervalToRefresh > 0 {
scheduleAuthTokenRefreshTimer(interval: timeIntervalToRefresh, isScheduledRefresh: true, successCallback: onSuccess)
return true
}
return false
}

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

expirationRefreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
self?.isTimerScheduled = false
if self?.localStorage.email != nil || self?.localStorage.userId != nil {
self?.requestNewAuthToken(hasFailedPriorAuth: false)
self?.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: successCallback, shouldIgnoreRetryPolicy: isScheduledRefresh)
} else {
ITBDebug("Email or userId is not available. Skipping token refresh")
}
}

isTimerScheduled = true
}

private func shouldSkipTokenRefresh(isScheduledRefresh: Bool) -> Bool {
return (pauseAuthRetry && !isScheduledRefresh) || isTimerScheduled
}

private func clearRefreshTimer() {
ITBInfo()

expirationRefreshTimer?.invalidate()
isTimerScheduled = false
expirationRefreshTimer = nil
}

Expand Down
1 change: 1 addition & 0 deletions swift-sdk/Internal/DependencyContainerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension DependencyContainerProtocol {

func createAuthManager(config: IterableConfig) -> IterableAuthManagerProtocol {
AuthManager(delegate: config.authDelegate,
authRetryPolicy: config.retryPolicy,
expirationRefreshPeriod: config.expiringAuthTokenRefreshPeriod,
localStorage: localStorage,
dateProvider: dateProvider)
Expand Down
3 changes: 2 additions & 1 deletion swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
private func onLogin(_ authToken: String? = nil) {
ITBInfo()

self.authManager.pauseAuthRetries(false)
if let authToken = authToken {
self.authManager.setNewToken(authToken)
completeUserLogin()
Expand All @@ -599,7 +600,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
if token != nil {
self?.completeUserLogin()
}
})
}, shouldIgnoreRetryPolicy: true)
}

private func completeUserLogin() {
Expand Down
11 changes: 11 additions & 0 deletions swift-sdk/Internal/IterableUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ import UIKit
static func secondsFromEpoch(for date: Date) -> Int {
Int(date.timeIntervalSince1970)
}

static func getEmailOrUserId() -> String? {
let email = IterableAPI.email
let userId = IterableAPI.userId
if email != nil {
return email
} else if userId != nil {
return userId
}
return nil
}

// given "var1", "val1", "var2", "val2" as input
// this will return "var1: val1, var2: val2"
Expand Down
Loading

0 comments on commit f3e7bd2

Please sign in to comment.