react-native-nitro-auth 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +295 -906
  3. package/android/src/main/java/com/auth/AuthAdapter.kt +22 -6
  4. package/cpp/HybridAuth.cpp +175 -39
  5. package/cpp/HybridAuth.hpp +2 -0
  6. package/ios/AuthAdapter.swift +2 -2
  7. package/lib/commonjs/Auth.web.js +141 -73
  8. package/lib/commonjs/Auth.web.js.map +1 -1
  9. package/lib/commonjs/create-auth-service.js +71 -0
  10. package/lib/commonjs/create-auth-service.js.map +1 -0
  11. package/lib/commonjs/service.js +2 -79
  12. package/lib/commonjs/service.js.map +1 -1
  13. package/lib/commonjs/service.web.js +2 -79
  14. package/lib/commonjs/service.web.js.map +1 -1
  15. package/lib/commonjs/use-auth.js +6 -3
  16. package/lib/commonjs/use-auth.js.map +1 -1
  17. package/lib/commonjs/utils/auth-error.js +8 -1
  18. package/lib/commonjs/utils/auth-error.js.map +1 -1
  19. package/lib/module/Auth.web.js +141 -73
  20. package/lib/module/Auth.web.js.map +1 -1
  21. package/lib/module/create-auth-service.js +67 -0
  22. package/lib/module/create-auth-service.js.map +1 -0
  23. package/lib/module/service.js +2 -79
  24. package/lib/module/service.js.map +1 -1
  25. package/lib/module/service.web.js +2 -79
  26. package/lib/module/service.web.js.map +1 -1
  27. package/lib/module/use-auth.js +6 -3
  28. package/lib/module/use-auth.js.map +1 -1
  29. package/lib/module/utils/auth-error.js +8 -1
  30. package/lib/module/utils/auth-error.js.map +1 -1
  31. package/lib/typescript/commonjs/Auth.web.d.ts +4 -2
  32. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/create-auth-service.d.ts +5 -0
  34. package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -0
  35. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -1
  39. package/lib/typescript/module/Auth.web.d.ts +4 -2
  40. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  41. package/lib/typescript/module/create-auth-service.d.ts +5 -0
  42. package/lib/typescript/module/create-auth-service.d.ts.map +1 -0
  43. package/lib/typescript/module/service.d.ts.map +1 -1
  44. package/lib/typescript/module/service.web.d.ts.map +1 -1
  45. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  46. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -1
  47. package/package.json +12 -9
  48. package/react-native-nitro-auth.podspec +1 -0
  49. package/src/Auth.web.ts +261 -102
  50. package/src/create-auth-service.ts +97 -0
  51. package/src/service.ts +3 -101
  52. package/src/service.web.ts +3 -101
  53. package/src/use-auth.ts +7 -3
  54. package/src/utils/auth-error.ts +10 -1
@@ -142,6 +142,7 @@ object AuthAdapter {
142
142
  }
143
143
 
144
144
  fun dispose() {
145
+ clearPkceState()
145
146
  moduleScope.cancel()
146
147
  moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
147
148
  runCatching { nativeDispose() }
@@ -309,7 +310,8 @@ object AuthAdapter {
309
310
  val origin = pendingOrigin
310
311
  if (error != null) {
311
312
  clearPkceState()
312
- nativeOnLoginError(origin, error, errorDescription)
313
+ val mappedError = mapMicrosoftOAuthError(error)
314
+ nativeOnLoginError(origin, mappedError, errorDescription ?: error)
313
315
  return
314
316
  }
315
317
  if (state != pendingState) {
@@ -391,7 +393,7 @@ object AuthAdapter {
391
393
  val error = json.optString("error", "token_error")
392
394
  val desc = json.optString("error_description", "Failed to exchange code for tokens")
393
395
  clearPkceState()
394
- nativeOnLoginError(origin, error, desc)
396
+ nativeOnLoginError(origin, mapMicrosoftOAuthError(error), desc)
395
397
  } catch (e: Exception) {
396
398
  clearPkceState()
397
399
  nativeOnLoginError(origin, "token_error", "Failed to exchange code for tokens")
@@ -438,6 +440,7 @@ object AuthAdapter {
438
440
  }
439
441
  }
440
442
 
443
+ @Synchronized
441
444
  private fun clearPkceState() {
442
445
  pendingOrigin = "login"
443
446
  pendingPkceVerifier = null
@@ -450,6 +453,16 @@ object AuthAdapter {
450
453
  microsoftAuthInProgress = false
451
454
  }
452
455
 
456
+ private fun mapMicrosoftOAuthError(error: String): String {
457
+ return when (error) {
458
+ "access_denied", "interaction_required" -> "cancelled"
459
+ "invalid_client", "unauthorized_client" -> "configuration_error"
460
+ "invalid_grant", "invalid_request", "invalid_scope" -> "token_error"
461
+ "temporarily_unavailable", "server_error" -> "network_error"
462
+ else -> "token_error"
463
+ }
464
+ }
465
+
453
466
  private fun decodeJwt(token: String): Map<String, String> {
454
467
  return try {
455
468
  val parts = token.split(".")
@@ -594,8 +607,10 @@ object AuthAdapter {
594
607
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
595
608
  if (account != null) {
596
609
  val newScopes = scopes.map { Scope(it) }
610
+ val grantedScopes = account.grantedScopes?.map { it.scopeUri }.orEmpty()
611
+ val allScopes = (grantedScopes + scopes.toList()).distinct()
597
612
  if (GoogleSignIn.hasPermissions(account, *newScopes.toTypedArray())) {
598
- onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct(), "scopes")
613
+ onSignInSuccess(account, allScopes, "scopes")
599
614
  return
600
615
  }
601
616
  val clientId = getClientIdFromResources(ctx)
@@ -603,7 +618,6 @@ object AuthAdapter {
603
618
  nativeOnLoginError("scopes", "configuration_error", "Google Client ID not configured")
604
619
  return
605
620
  }
606
- val allScopes = (pendingScopes + scopes.toList()).distinct()
607
621
  val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email, origin = "scopes")
608
622
  ctx.startActivity(intent)
609
623
  return
@@ -667,6 +681,7 @@ object AuthAdapter {
667
681
  @JvmStatic
668
682
  fun logoutSync(context: Context) {
669
683
  val ctx = appContext ?: context.applicationContext
684
+ clearPkceState()
670
685
  // Clear Credential Manager state (covers One-Tap / passkey credentials).
671
686
  moduleScope.launch {
672
687
  try {
@@ -689,6 +704,7 @@ object AuthAdapter {
689
704
  @JvmStatic
690
705
  fun revokeAccessSync(context: Context) {
691
706
  val ctx = appContext ?: context.applicationContext
707
+ clearPkceState()
692
708
  val clientId = getClientIdFromResources(ctx)
693
709
  if (clientId != null) {
694
710
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
@@ -788,7 +804,7 @@ object AuthAdapter {
788
804
  val json = org.json.JSONObject(responseBody)
789
805
  val errorCode = json.optString("error", "token_error")
790
806
  val errorDesc = json.optString("error_description", "Token refresh failed")
791
- Pair(errorCode, errorDesc)
807
+ Pair(mapMicrosoftOAuthError(errorCode), errorDesc)
792
808
  } catch (e: Exception) {
793
809
  Pair("token_error", "Token refresh failed")
794
810
  }
@@ -871,7 +887,7 @@ object AuthAdapter {
871
887
  val json = org.json.JSONObject(errorBody)
872
888
  val errorCode = json.optString("error", "token_error")
873
889
  val errorDesc = json.optString("error_description", "Token refresh failed")
874
- Pair(errorCode, errorDesc)
890
+ Pair(mapMicrosoftOAuthError(errorCode), errorDesc)
875
891
  } catch (e: Exception) {
876
892
  Pair("token_error", "Token refresh failed")
877
893
  }
@@ -2,9 +2,63 @@
2
2
  #include "PlatformAuth.hpp"
3
3
  #include <algorithm>
4
4
  #include <chrono>
5
+ #include <exception>
6
+ #include <stdexcept>
7
+ #include <unordered_set>
5
8
 
6
9
  namespace margelo::nitro::NitroAuth {
7
10
 
11
+ namespace {
12
+
13
+ std::exception_ptr makeAuthError(const char* message) {
14
+ return std::make_exception_ptr(std::runtime_error(message));
15
+ }
16
+
17
+ void rejectIfPending(const std::shared_ptr<Promise<AuthTokens>>& promise, const char* message) {
18
+ if (promise && promise->isPending()) {
19
+ promise->reject(makeAuthError(message));
20
+ }
21
+ }
22
+
23
+ void mergeGrantedScopes(std::vector<std::string>& grantedScopes, const std::vector<std::string>& scopes) {
24
+ std::unordered_set<std::string> knownScopes(grantedScopes.begin(), grantedScopes.end());
25
+ grantedScopes.reserve(grantedScopes.size() + scopes.size());
26
+
27
+ for (const auto& scope : scopes) {
28
+ if (knownScopes.insert(scope).second) {
29
+ grantedScopes.push_back(scope);
30
+ }
31
+ }
32
+ }
33
+
34
+ void removeGrantedScopes(std::vector<std::string>& grantedScopes, const std::vector<std::string>& scopes) {
35
+ if (scopes.empty() || grantedScopes.empty()) {
36
+ return;
37
+ }
38
+
39
+ const std::unordered_set<std::string> scopesToRemove(scopes.begin(), scopes.end());
40
+ grantedScopes.erase(
41
+ std::remove_if(grantedScopes.begin(), grantedScopes.end(),
42
+ [&scopesToRemove](const std::string& scope) {
43
+ return scopesToRemove.find(scope) != scopesToRemove.end();
44
+ }),
45
+ grantedScopes.end()
46
+ );
47
+ }
48
+
49
+ template <typename TCallback, typename TValue>
50
+ void invokeListenersSafely(const std::vector<TCallback>& listeners, const TValue& value) {
51
+ for (const auto& listener : listeners) {
52
+ try {
53
+ listener(value);
54
+ } catch (...) {
55
+ // Callback failures are isolated so one listener cannot block core state updates.
56
+ }
57
+ }
58
+ }
59
+
60
+ } // namespace
61
+
8
62
  HybridAuth::HybridAuth() : HybridObject(TAG) {
9
63
  // In-memory only - no internal persistence.
10
64
  }
@@ -29,13 +83,12 @@ void HybridAuth::notifyAuthStateChanged() {
29
83
  {
30
84
  std::lock_guard<std::recursive_mutex> lock(_mutex);
31
85
  user = _currentUser;
86
+ listeners.reserve(_listeners.size());
32
87
  for (auto const& [id, listener] : _listeners) {
33
88
  listeners.push_back(listener);
34
89
  }
35
90
  }
36
- for (const auto& listener : listeners) {
37
- listener(user);
38
- }
91
+ invokeListenersSafely(listeners, user);
39
92
  }
40
93
 
41
94
  std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(const std::optional<AuthUser>&)>& callback) {
@@ -48,6 +101,7 @@ std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(co
48
101
  auto self = weak.lock();
49
102
  if (!self) return;
50
103
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
104
+ if (!auth) return;
51
105
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
52
106
  auth->_listeners.erase(id);
53
107
  };
@@ -63,29 +117,55 @@ std::function<void()> HybridAuth::onTokensRefreshed(const std::function<void(con
63
117
  auto self = weak.lock();
64
118
  if (!self) return;
65
119
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
120
+ if (!auth) return;
66
121
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
67
122
  auth->_tokenListeners.erase(id);
68
123
  };
69
124
  }
70
125
 
126
+ std::shared_ptr<Promise<AuthTokens>> HybridAuth::advanceSessionGenerationLocked() {
127
+ _sessionGeneration++;
128
+ auto refreshInFlight = _refreshInFlight;
129
+ _refreshInFlight = nullptr;
130
+ return refreshInFlight;
131
+ }
132
+
71
133
  void HybridAuth::logout() {
134
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
72
135
  {
73
136
  std::lock_guard<std::recursive_mutex> lock(_mutex);
137
+ refreshInFlight = advanceSessionGenerationLocked();
74
138
  _currentUser = std::nullopt;
75
139
  _grantedScopes.clear();
76
140
  }
141
+ rejectIfPending(refreshInFlight, "not_signed_in");
77
142
  PlatformAuth::logout();
78
143
  notifyAuthStateChanged();
79
144
  }
80
145
 
81
146
  std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
82
147
  auto promise = Promise<void>::create();
148
+ uint64_t generation;
149
+ {
150
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
151
+ generation = _sessionGeneration;
152
+ }
83
153
  auto silentPromise = PlatformAuth::silentRestore();
84
154
  auto self = shared_from_this();
85
- silentPromise->addOnResolvedListener([self, promise](const std::optional<AuthUser>& user) {
155
+ silentPromise->addOnResolvedListener([self, promise, generation](const std::optional<AuthUser>& user) {
86
156
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
157
+ if (!auth) {
158
+ promise->reject(makeAuthError("internal_error"));
159
+ return;
160
+ }
161
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
87
162
  {
88
163
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
164
+ if (auth->_sessionGeneration != generation) {
165
+ promise->resolve();
166
+ return;
167
+ }
168
+ refreshInFlight = auth->advanceSessionGenerationLocked();
89
169
  auth->_currentUser = user;
90
170
  if (user) {
91
171
  if (user->scopes) {
@@ -97,6 +177,7 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
97
177
  auth->_grantedScopes.clear();
98
178
  }
99
179
  }
180
+ rejectIfPending(refreshInFlight, "cancelled");
100
181
  // Always resolve - no session is not an error, just means user is logged out
101
182
  auth->notifyAuthStateChanged();
102
183
  promise->resolve();
@@ -111,13 +192,31 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
111
192
 
112
193
  std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
113
194
  auto promise = Promise<void>::create();
195
+ uint64_t generation;
196
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
197
+ {
198
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
199
+ refreshInFlight = advanceSessionGenerationLocked();
200
+ generation = _sessionGeneration;
201
+ }
202
+ rejectIfPending(refreshInFlight, "cancelled");
114
203
 
115
204
  auto self = shared_from_this();
116
205
  auto loginPromise = PlatformAuth::login(provider, options);
117
- loginPromise->addOnResolvedListener([self, promise, options](const AuthUser& user) {
206
+ loginPromise->addOnResolvedListener([self, promise, options, generation](const AuthUser& user) {
118
207
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
208
+ if (!auth) {
209
+ promise->reject(makeAuthError("internal_error"));
210
+ return;
211
+ }
212
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
119
213
  {
120
214
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
215
+ if (auth->_sessionGeneration != generation) {
216
+ promise->reject(makeAuthError("cancelled"));
217
+ return;
218
+ }
219
+ refreshInFlight = auth->advanceSessionGenerationLocked();
121
220
  auth->_currentUser = user;
122
221
  if (user.scopes && !user.scopes->empty()) {
123
222
  auth->_grantedScopes = *user.scopes;
@@ -132,6 +231,7 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
132
231
  : std::make_optional(auth->_grantedScopes);
133
232
  }
134
233
  }
234
+ rejectIfPending(refreshInFlight, "cancelled");
135
235
  auth->notifyAuthStateChanged();
136
236
  promise->resolve();
137
237
  });
@@ -144,18 +244,27 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
144
244
 
145
245
  std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::string>& scopes) {
146
246
  auto promise = Promise<void>::create();
247
+ uint64_t generation;
248
+ {
249
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
250
+ generation = _sessionGeneration;
251
+ }
147
252
  auto self = shared_from_this();
148
253
  auto requestPromise = PlatformAuth::requestScopes(scopes);
149
- requestPromise->addOnResolvedListener([self, promise, scopes](const AuthUser& user) {
254
+ requestPromise->addOnResolvedListener([self, promise, scopes, generation](const AuthUser& user) {
150
255
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
256
+ if (!auth) {
257
+ promise->reject(makeAuthError("internal_error"));
258
+ return;
259
+ }
151
260
  {
152
261
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
153
- auth->_currentUser = user;
154
- for (const auto& scope : scopes) {
155
- if (std::find(auth->_grantedScopes.begin(), auth->_grantedScopes.end(), scope) == auth->_grantedScopes.end()) {
156
- auth->_grantedScopes.push_back(scope);
157
- }
262
+ if (auth->_sessionGeneration != generation) {
263
+ promise->reject(makeAuthError("cancelled"));
264
+ return;
158
265
  }
266
+ auth->_currentUser = user;
267
+ mergeGrantedScopes(auth->_grantedScopes, scopes);
159
268
  if (auth->_currentUser) auth->_currentUser->scopes = auth->_grantedScopes;
160
269
  }
161
270
  auth->notifyAuthStateChanged();
@@ -172,13 +281,7 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
172
281
  auto promise = Promise<void>::create();
173
282
  {
174
283
  std::lock_guard<std::recursive_mutex> lock(_mutex);
175
- _grantedScopes.erase(
176
- std::remove_if(_grantedScopes.begin(), _grantedScopes.end(),
177
- [&scopes](const std::string& s) {
178
- return std::find(scopes.begin(), scopes.end(), s) != scopes.end();
179
- }),
180
- _grantedScopes.end()
181
- );
284
+ removeGrantedScopes(_grantedScopes, scopes);
182
285
  if (_currentUser) {
183
286
  _currentUser->scopes = _grantedScopes;
184
287
  }
@@ -191,9 +294,11 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
191
294
  std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken() {
192
295
  auto promise = Promise<std::optional<std::string>>::create();
193
296
  bool needsRefresh = false;
297
+ std::optional<std::string> cachedAccessToken;
194
298
  {
195
299
  std::lock_guard<std::recursive_mutex> lock(_mutex);
196
300
  if (_currentUser && _currentUser->accessToken) {
301
+ cachedAccessToken = _currentUser->accessToken;
197
302
  if (_currentUser->expirationTime) {
198
303
  auto now = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1);
199
304
  if (now + 300000 > *_currentUser->expirationTime) needsRefresh = true;
@@ -210,8 +315,8 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
210
315
 
211
316
  if (needsRefresh) {
212
317
  auto refreshPromise = refreshToken();
213
- refreshPromise->addOnResolvedListener([promise](const AuthTokens& tokens) {
214
- promise->resolve(tokens.accessToken);
318
+ refreshPromise->addOnResolvedListener([promise, cachedAccessToken](const AuthTokens& tokens) {
319
+ promise->resolve(tokens.accessToken.has_value() ? tokens.accessToken : cachedAccessToken);
215
320
  });
216
321
  refreshPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
217
322
  promise->reject(error);
@@ -222,52 +327,84 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
222
327
 
223
328
  std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
224
329
  std::shared_ptr<Promise<AuthTokens>> promise;
330
+ uint64_t generation;
225
331
  {
226
332
  std::lock_guard<std::recursive_mutex> lock(_mutex);
227
333
  if (_refreshInFlight) {
228
334
  return _refreshInFlight;
229
335
  }
336
+ generation = _sessionGeneration;
230
337
  promise = Promise<AuthTokens>::create();
231
338
  _refreshInFlight = promise;
232
339
  }
233
340
 
234
341
  auto self = shared_from_this();
235
342
  auto refreshPromise = PlatformAuth::refreshToken();
236
- refreshPromise->addOnResolvedListener([self, promise](const AuthTokens& tokens) {
343
+ refreshPromise->addOnResolvedListener([self, promise, generation](const AuthTokens& tokens) {
237
344
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
345
+ if (!auth) {
346
+ promise->reject(makeAuthError("internal_error"));
347
+ return;
348
+ }
349
+ bool isStale = false;
238
350
  {
239
351
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
240
- if (auth->_currentUser) {
241
- if (tokens.accessToken.has_value()) {
242
- auth->_currentUser->accessToken = tokens.accessToken;
243
- }
244
- if (tokens.idToken.has_value()) {
245
- auth->_currentUser->idToken = tokens.idToken;
352
+ if (auth->_sessionGeneration != generation) {
353
+ if (auth->_refreshInFlight == promise) {
354
+ auth->_refreshInFlight = nullptr;
246
355
  }
247
- if (tokens.refreshToken.has_value()) {
248
- auth->_currentUser->refreshToken = tokens.refreshToken;
356
+ isStale = true;
357
+ } else {
358
+ if (auth->_currentUser) {
359
+ if (tokens.accessToken.has_value()) {
360
+ auth->_currentUser->accessToken = tokens.accessToken;
361
+ }
362
+ if (tokens.idToken.has_value()) {
363
+ auth->_currentUser->idToken = tokens.idToken;
364
+ }
365
+ if (tokens.refreshToken.has_value()) {
366
+ auth->_currentUser->refreshToken = tokens.refreshToken;
367
+ }
368
+ if (tokens.expirationTime.has_value()) {
369
+ auth->_currentUser->expirationTime = tokens.expirationTime;
370
+ }
249
371
  }
250
- if (tokens.expirationTime.has_value()) {
251
- auth->_currentUser->expirationTime = tokens.expirationTime;
372
+ if (auth->_refreshInFlight == promise) {
373
+ auth->_refreshInFlight = nullptr;
252
374
  }
253
375
  }
254
- if (auth->_refreshInFlight == promise) {
255
- auth->_refreshInFlight = nullptr;
256
- }
376
+ }
377
+ if (isStale) {
378
+ rejectIfPending(promise, "cancelled");
379
+ return;
257
380
  }
258
381
  auth->notifyTokensRefreshed(tokens);
259
382
  auth->notifyAuthStateChanged();
260
383
  promise->resolve(tokens);
261
384
  });
262
385
 
263
- refreshPromise->addOnRejectedListener([self, promise](const std::exception_ptr& error) {
386
+ refreshPromise->addOnRejectedListener([self, promise, generation](const std::exception_ptr& error) {
264
387
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
388
+ if (!auth) {
389
+ promise->reject(makeAuthError("internal_error"));
390
+ return;
391
+ }
392
+ bool isStale = false;
265
393
  {
266
394
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
267
- if (auth->_refreshInFlight == promise) {
395
+ if (auth->_sessionGeneration != generation) {
396
+ if (auth->_refreshInFlight == promise) {
397
+ auth->_refreshInFlight = nullptr;
398
+ }
399
+ isStale = true;
400
+ } else if (auth->_refreshInFlight == promise) {
268
401
  auth->_refreshInFlight = nullptr;
269
402
  }
270
403
  }
404
+ if (isStale) {
405
+ rejectIfPending(promise, "cancelled");
406
+ return;
407
+ }
271
408
  promise->reject(error);
272
409
  });
273
410
  return promise;
@@ -281,13 +418,12 @@ void HybridAuth::notifyTokensRefreshed(const AuthTokens& tokens) {
281
418
  std::vector<std::function<void(const AuthTokens&)>> listeners;
282
419
  {
283
420
  std::lock_guard<std::recursive_mutex> lock(_mutex);
421
+ listeners.reserve(_tokenListeners.size());
284
422
  for (auto const& [id, listener] : _tokenListeners) {
285
423
  listeners.push_back(listener);
286
424
  }
287
425
  }
288
- for (const auto& listener : listeners) {
289
- listener(tokens);
290
- }
426
+ invokeListenersSafely(listeners, tokens);
291
427
  }
292
428
 
293
429
  } // namespace margelo::nitro::NitroAuth
@@ -38,6 +38,7 @@ public:
38
38
  private:
39
39
  void notifyAuthStateChanged();
40
40
  void notifyTokensRefreshed(const AuthTokens& tokens);
41
+ std::shared_ptr<Promise<AuthTokens>> advanceSessionGenerationLocked();
41
42
 
42
43
  private:
43
44
  std::optional<AuthUser> _currentUser;
@@ -48,6 +49,7 @@ private:
48
49
  std::map<uint64_t, std::function<void(const AuthTokens&)>> _tokenListeners;
49
50
  uint64_t _nextTokenListenerId = 0;
50
51
  std::shared_ptr<Promise<AuthTokens>> _refreshInFlight;
52
+ uint64_t _sessionGeneration = 0;
51
53
 
52
54
  // recursive_mutex: listeners resolved inside a lock scope may re-enter Auth methods
53
55
  // that also acquire _mutex, causing deadlock with a non-recursive mutex.
@@ -261,7 +261,7 @@ public class AuthAdapter: NSObject {
261
261
 
262
262
  URLSession.shared.dataTask(with: request) { data, response, error in
263
263
  DispatchQueue.main.async {
264
- if let error = error {
264
+ if error != nil {
265
265
  completion(nil, "network_error")
266
266
  return
267
267
  }
@@ -626,7 +626,7 @@ public class AuthAdapter: NSObject {
626
626
  .data(using: .utf8)
627
627
  URLSession.shared.dataTask(with: request) { data, response, error in
628
628
  DispatchQueue.main.async {
629
- if let error = error {
629
+ if error != nil {
630
630
  completion(nil, "network_error")
631
631
  return
632
632
  }