Skip to content

Commit 10f6adc

Browse files
committed
Store UserExecutor requests that need auth
This PR adds a pendingAuthRequests dictionary that stores the requests that are waiting for an updated JWT keyed on externalId. When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again. Also update tests to account for the callback object change and add tests for the new case
1 parent 1d0b97f commit 10f6adc

File tree

6 files changed

+133
-33
lines changed

6 files changed

+133
-33
lines changed

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import OneSignalOSCore
3535
*/
3636
class OSUserExecutor {
3737
var userRequestQueue: [OSUserRequest] = []
38+
var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]()
3839
private let newRecordsState: OSNewRecordsState
3940
let jwtConfig: OSUserJwtConfig
4041

@@ -289,6 +290,22 @@ extension OSUserExecutor {
289290
appendToQueue(request)
290291
executePendingRequests()
291292
}
293+
294+
func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) {
295+
self.dispatchQueue.async {
296+
self.userRequestQueue.removeAll(where: { $0 == request})
297+
guard let externalId = externalId else {
298+
return
299+
}
300+
var requests = self.pendingAuthRequests[externalId] ?? []
301+
let inQueue = requests.contains(where: {$0 == request})
302+
guard !inQueue else {
303+
return
304+
}
305+
requests.append(request)
306+
self.pendingAuthRequests[externalId] = requests
307+
}
308+
}
292309

293310
func executeCreateUserRequest(_ request: OSRequestCreateUser) {
294311
guard !request.sentToClient else {
@@ -301,6 +318,11 @@ extension OSUserExecutor {
301318
request.pushSubscriptionModel = pushSubscriptionModel
302319
request.updatePushSubscriptionModel(pushSubscriptionModel)
303320
}
321+
322+
guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
323+
pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
324+
return
325+
}
304326

305327
guard request.prepareForExecution(newRecordsState: newRecordsState)
306328
else {
@@ -344,7 +366,7 @@ extension OSUserExecutor {
344366
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor no externalId for unauthorized request.")
345367
return
346368
}
347-
self.handleUnauthorizedError(externalId: externalId, error: nsError)
369+
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
348370
request.sentToClient = false
349371
} else if responseType != .retryable {
350372
// A failed create user request would leave the SDK in a bad state
@@ -361,8 +383,9 @@ extension OSUserExecutor {
361383
}
362384
}
363385

364-
func handleUnauthorizedError(externalId: String, error: NSError) {
386+
func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) {
365387
if (jwtConfig.isRequired ?? false) {
388+
self.pendRequestUntilAuthUpdated(request, externalId: externalId)
366389
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
367390
}
368391
}
@@ -376,6 +399,7 @@ extension OSUserExecutor {
376399

377400
/**
378401
For migrating legacy players from 3.x to 5.x. This request will fetch the identity object for a subscription ID, and we will use the returned onesignalId to fetch and hydrate the local user.
402+
ECM can this ever succeed with identity verification on?
379403
*/
380404
func executeFetchIdentityBySubscriptionRequest(_ request: OSRequestFetchIdentityBySubscription) {
381405
guard !request.sentToClient else {
@@ -485,7 +509,7 @@ extension OSUserExecutor {
485509
// This will hydrate the OneSignal ID for any pending requests
486510
self.createUser(aliasLabel: request.aliasLabel, aliasId: request.aliasId, identityModel: request.identityModelToUpdate)
487511
}
488-
} else if responseType == .invalid || responseType == .unauthorized {
512+
} else if responseType == .invalid || responseType == .unauthorized { //Identify User should never be called with identity verification on
489513
// Failed, no retry
490514
self.removeFromQueue(request)
491515
self.executePendingRequests()
@@ -574,7 +598,7 @@ extension OSUserExecutor {
574598
OneSignalUserManagerImpl.sharedInstance._logout()
575599
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
576600
if let externalId = request.identityModel.externalId {
577-
self.handleUnauthorizedError(externalId: externalId, error: nsError)
601+
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
578602
}
579603
request.sentToClient = false
580604
} else if responseType != .retryable {
@@ -707,15 +731,23 @@ extension OSUserExecutor: OSUserJwtConfigListener {
707731
}
708732

709733
func onJwtUpdated(externalId: String, token: String?) {
710-
/*
711-
ECM
712-
Do we actually even need this callback?
713-
Requests that are invalidated do not pass prepare for execution
714-
Once they are valid they will pass prepare for execution.
715-
We could use this callback to optimize sending requests immediately
716-
*/
734+
reQueuePendingRequestsForExternalId(externalId: externalId)
717735
print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))")
718736
}
737+
738+
private func reQueuePendingRequestsForExternalId(externalId: String) {
739+
self.dispatchQueue.async {
740+
guard let requests = self.pendingAuthRequests[externalId] else {
741+
return
742+
}
743+
for request in requests {
744+
self.userRequestQueue.append(request)
745+
}
746+
self.pendingAuthRequests[externalId] = nil
747+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue)
748+
self.executePendingRequests(withDelay: true)
749+
}
750+
}
719751

720752
private func removeInvalidRequests() {
721753
self.dispatchQueue.async {

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ public let userB_EUID = "test_user_b_external_id"
77
public let testPushSubId = "test_push_subscription_id"
88
public let testEmailSubId = "test_email_subscription_id"
99
public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1fadf3a04be69"
10-
public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
10+
public let userA_InvalidJwtToken = "byJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
11+
12+
public let userA_ValidJwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
13+
public let userB_ValidJwtToken = "fyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
1114

1215
public let userA_email = "[email protected]"
1316

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class IdentityExecutorTests: XCTestCase {
100100
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
101101

102102
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
103-
user.identityModel.jwtBearerToken = userA_JwtToken
103+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
104104
let aliases = userA_Aliases
105105
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
106106
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
@@ -120,14 +120,13 @@ final class IdentityExecutorTests: XCTestCase {
120120
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
121121

122122
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
123-
user.identityModel.jwtBearerToken = userA_JwtToken
123+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
124124
let aliases = userA_Aliases
125125
MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases)
126126
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
127127

128128
var invalidatedCallbackWasCalled = false
129129
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
130-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
131130
invalidatedCallbackWasCalled = true
132131
}
133132

@@ -147,14 +146,13 @@ final class IdentityExecutorTests: XCTestCase {
147146
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
148147

149148
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
150-
user.identityModel.jwtBearerToken = userA_JwtToken
149+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
151150
let aliases = userA_Aliases
152151
MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel)
153152
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
154153

155154
var invalidatedCallbackWasCalled = false
156155
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
157-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
158156
invalidatedCallbackWasCalled = true
159157
}
160158

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class PropertyExecutorTests: XCTestCase {
100100
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
101101

102102
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
103-
user.identityModel.jwtBearerToken = userA_JwtToken
103+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
104104
let tags = ["testUserA" : "true"]
105105
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
106106
mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
@@ -120,7 +120,7 @@ final class PropertyExecutorTests: XCTestCase {
120120
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
121121

122122
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
123-
user.identityModel.jwtBearerToken = userA_JwtToken
123+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
124124

125125

126126

@@ -130,7 +130,6 @@ final class PropertyExecutorTests: XCTestCase {
130130

131131
var invalidatedCallbackWasCalled = false
132132
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
133-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
134133
invalidatedCallbackWasCalled = true
135134
}
136135

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class SubscriptionExecutorTests: XCTestCase {
100100
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
101101

102102
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
103-
user.identityModel.jwtBearerToken = userA_JwtToken
103+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
104104
let email = userA_email
105105
MockUserRequests.setAddEmailResponse(with: mocks.client, email: email)
106106
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
@@ -120,14 +120,13 @@ final class SubscriptionExecutorTests: XCTestCase {
120120
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
121121

122122
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
123-
user.identityModel.jwtBearerToken = userA_JwtToken
123+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
124124
let email = userA_email
125125
MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email)
126126
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
127127

128128
var invalidatedCallbackWasCalled = false
129129
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
130-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
131130
invalidatedCallbackWasCalled = true
132131
}
133132

@@ -147,14 +146,13 @@ final class SubscriptionExecutorTests: XCTestCase {
147146
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
148147

149148
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
150-
user.identityModel.jwtBearerToken = userA_JwtToken
149+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
151150
let email = userA_email
152151
MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email)
153152
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
154153

155154
var invalidatedCallbackWasCalled = false
156155
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
157-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
158156
invalidatedCallbackWasCalled = true
159157
}
160158

@@ -174,14 +172,13 @@ final class SubscriptionExecutorTests: XCTestCase {
174172
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
175173

176174
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
177-
user.identityModel.jwtBearerToken = userA_JwtToken
175+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
178176
let token = testPushToken
179177
MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token)
180178
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value:token))
181179

182180
var invalidatedCallbackWasCalled = false
183181
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
184-
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
185182
invalidatedCallbackWasCalled = true
186183
}
187184

0 commit comments

Comments
 (0)