react-native-nitro-auth 0.5.6 → 0.5.8

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 (36) hide show
  1. package/README.md +288 -907
  2. package/android/src/main/cpp/PlatformAuth+Android.cpp +1 -1
  3. package/android/src/main/java/com/auth/AuthAdapter.kt +65 -27
  4. package/cpp/HybridAuth.cpp +58 -7
  5. package/cpp/HybridAuth.hpp +1 -0
  6. package/ios/AuthAdapter.swift +45 -16
  7. package/ios/PlatformAuth+iOS.mm +20 -1
  8. package/lib/commonjs/service.js +21 -17
  9. package/lib/commonjs/service.js.map +1 -1
  10. package/lib/commonjs/use-auth.js +26 -10
  11. package/lib/commonjs/use-auth.js.map +1 -1
  12. package/lib/commonjs/utils/auth-error.js +9 -2
  13. package/lib/commonjs/utils/auth-error.js.map +1 -1
  14. package/lib/module/service.js +21 -17
  15. package/lib/module/service.js.map +1 -1
  16. package/lib/module/use-auth.js +26 -10
  17. package/lib/module/use-auth.js.map +1 -1
  18. package/lib/module/utils/auth-error.js +9 -2
  19. package/lib/module/utils/auth-error.js.map +1 -1
  20. package/lib/typescript/commonjs/Auth.nitro.d.ts +2 -2
  21. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -1
  25. package/lib/typescript/module/Auth.nitro.d.ts +2 -2
  26. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  27. package/lib/typescript/module/service.d.ts.map +1 -1
  28. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  29. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -1
  30. package/nitro.json +4 -1
  31. package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -0
  32. package/package.json +10 -8
  33. package/src/Auth.nitro.ts +3 -1
  34. package/src/service.ts +22 -17
  35. package/src/use-auth.ts +35 -6
  36. package/src/utils/auth-error.ts +12 -1
@@ -531,7 +531,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginError(
531
531
  if (loginPromise) loginPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
532
532
  if (scopesPromise) scopesPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
533
533
  if (silentPromise) {
534
- if (errorStr == "No session") silentPromise->resolve(std::nullopt);
534
+ if (errorStr == "not_signed_in") silentPromise->resolve(std::nullopt);
535
535
  else silentPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
536
536
  }
537
537
  }
@@ -3,7 +3,8 @@
3
3
  // • getLastSignedInAccount – persists session across app restarts via GMS store; no drop-in replacement
4
4
  // • silentSignIn – AuthorizationClient.authorize() still requires an Activity for interactive fallback
5
5
  // • revokeAccess – no equivalent in Credential Manager or Identity.getAuthorizationClient()
6
- // All modern entry-points use Credential Manager (One-Tap). Legacy is a documented fallback only.
6
+ // All modern entry-points use Credential Manager (One-Tap) unless the caller explicitly needs
7
+ // Android's account chooser semantics, which still require the legacy Google Sign-In flow.
7
8
 
8
9
  package com.auth
9
10
 
@@ -40,11 +41,14 @@ import java.util.UUID
40
41
 
41
42
  object AuthAdapter {
42
43
  private const val TAG = "AuthAdapter"
44
+ private val defaultMicrosoftScopes =
45
+ listOf("openid", "email", "profile", "offline_access", "User.Read")
43
46
 
44
47
  @Volatile
45
48
  private var isInitialized = false
46
49
 
47
50
  private var appContext: Context? = null
51
+ @Volatile
48
52
  private var currentActivity: Activity? = null
49
53
  private var googleSignInClient: GoogleSignInClient? = null
50
54
  private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
@@ -65,18 +69,22 @@ object AuthAdapter {
65
69
  private var pendingMicrosoftClientId: String? = null
66
70
  @Volatile
67
71
  private var pendingMicrosoftB2cDomain: String? = null
72
+ @Volatile
73
+ private var microsoftAuthInProgress = false
68
74
 
69
75
  @Volatile
70
76
  private var inMemoryMicrosoftRefreshToken: String? = null
71
77
  @Volatile
72
78
  private var inMemoryMicrosoftScopes: List<String> =
73
- listOf("openid", "email", "profile", "offline_access", "User.Read")
79
+ defaultMicrosoftScopes
74
80
 
75
81
  // Module-scoped coroutine scope — cancelled on module invalidation via dispose().
76
82
  private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
77
83
 
78
84
  @JvmStatic
79
85
  private external fun nativeInitialize(context: Context)
86
+ @JvmStatic
87
+ private external fun nativeDispose()
80
88
 
81
89
  @JvmStatic
82
90
  private external fun nativeOnLoginSuccess(
@@ -134,8 +142,11 @@ object AuthAdapter {
134
142
  }
135
143
 
136
144
  fun dispose() {
145
+ clearPkceState()
137
146
  moduleScope.cancel()
138
147
  moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
148
+ runCatching { nativeDispose() }
149
+ .onFailure { Log.w(TAG, "Failed to dispose NitroAuth native bridge", it) }
139
150
 
140
151
  val app = appContext as? Application
141
152
  lifecycleCallbacks?.let { app?.unregisterActivityLifecycleCallbacks(it) }
@@ -200,7 +211,7 @@ object AuthAdapter {
200
211
  val requestedScopes = scopes?.toList() ?: listOf("email", "profile")
201
212
  pendingScopes = requestedScopes
202
213
 
203
- if (useLegacyGoogleSignIn) {
214
+ if (useLegacyGoogleSignIn || forceAccountPicker) {
204
215
  loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker, "login")
205
216
  return
206
217
  }
@@ -217,9 +228,21 @@ object AuthAdapter {
217
228
  pendingOrigin = origin
218
229
 
219
230
  val effectiveTenant = tenant ?: getMicrosoftTenantFromResources(ctx) ?: "common"
220
- val effectiveScopes = scopes?.toList() ?: listOf("openid", "email", "profile", "offline_access", "User.Read")
231
+ val effectiveScopes = scopes?.toList() ?: defaultMicrosoftScopes
221
232
  val effectivePrompt = prompt ?: "select_account"
222
- pendingMicrosoftScopes = effectiveScopes
233
+
234
+ synchronized(this) {
235
+ if (microsoftAuthInProgress) {
236
+ nativeOnLoginError(
237
+ origin,
238
+ "operation_in_progress",
239
+ "Microsoft authentication already in progress",
240
+ )
241
+ return
242
+ }
243
+ microsoftAuthInProgress = true
244
+ pendingMicrosoftScopes = effectiveScopes
245
+ }
223
246
 
224
247
  val codeVerifier = generateCodeVerifier()
225
248
  val codeChallenge = generateCodeChallenge(codeVerifier)
@@ -261,6 +284,7 @@ object AuthAdapter {
261
284
  ctx.startActivity(browserIntent)
262
285
  }
263
286
  } catch (e: Exception) {
287
+ clearPkceState()
264
288
  nativeOnLoginError(origin, "unknown", e.message)
265
289
  }
266
290
  }
@@ -286,7 +310,8 @@ object AuthAdapter {
286
310
  val origin = pendingOrigin
287
311
  if (error != null) {
288
312
  clearPkceState()
289
- nativeOnLoginError(origin, error, errorDescription)
313
+ val mappedError = mapMicrosoftOAuthError(error)
314
+ nativeOnLoginError(origin, mappedError, errorDescription ?: error)
290
315
  return
291
316
  }
292
317
  if (state != pendingState) {
@@ -368,7 +393,7 @@ object AuthAdapter {
368
393
  val error = json.optString("error", "token_error")
369
394
  val desc = json.optString("error_description", "Failed to exchange code for tokens")
370
395
  clearPkceState()
371
- nativeOnLoginError(origin, error, desc)
396
+ nativeOnLoginError(origin, mapMicrosoftOAuthError(error), desc)
372
397
  } catch (e: Exception) {
373
398
  clearPkceState()
374
399
  nativeOnLoginError(origin, "token_error", "Failed to exchange code for tokens")
@@ -399,16 +424,15 @@ object AuthAdapter {
399
424
 
400
425
  val email = claims["preferred_username"] ?: claims["email"]
401
426
  val name = claims["name"]
427
+ val grantedScopes = pendingMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
402
428
 
403
429
  if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
404
- inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
405
- listOf("openid", "email", "profile", "offline_access", "User.Read")
406
- }
430
+ inMemoryMicrosoftScopes = grantedScopes
407
431
 
408
432
  clearPkceState()
409
433
  nativeOnLoginSuccess(
410
434
  origin, "microsoft", email, name, null, idToken, accessToken, null,
411
- pendingMicrosoftScopes.toTypedArray(), expirationTime
435
+ grantedScopes.toTypedArray(), expirationTime
412
436
  )
413
437
  } catch (e: Exception) {
414
438
  clearPkceState()
@@ -416,6 +440,7 @@ object AuthAdapter {
416
440
  }
417
441
  }
418
442
 
443
+ @Synchronized
419
444
  private fun clearPkceState() {
420
445
  pendingOrigin = "login"
421
446
  pendingPkceVerifier = null
@@ -424,6 +449,18 @@ object AuthAdapter {
424
449
  pendingMicrosoftTenant = null
425
450
  pendingMicrosoftClientId = null
426
451
  pendingMicrosoftB2cDomain = null
452
+ pendingMicrosoftScopes = emptyList()
453
+ microsoftAuthInProgress = false
454
+ }
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
+ }
427
464
  }
428
465
 
429
466
  private fun decodeJwt(token: String): Map<String, String> {
@@ -570,8 +607,10 @@ object AuthAdapter {
570
607
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
571
608
  if (account != null) {
572
609
  val newScopes = scopes.map { Scope(it) }
610
+ val grantedScopes = account.grantedScopes?.map { it.scopeUri }.orEmpty()
611
+ val allScopes = (grantedScopes + scopes.toList()).distinct()
573
612
  if (GoogleSignIn.hasPermissions(account, *newScopes.toTypedArray())) {
574
- onSignInSuccess(account, (pendingScopes + scopes.toList()).distinct(), "scopes")
613
+ onSignInSuccess(account, allScopes, "scopes")
575
614
  return
576
615
  }
577
616
  val clientId = getClientIdFromResources(ctx)
@@ -579,7 +618,6 @@ object AuthAdapter {
579
618
  nativeOnLoginError("scopes", "configuration_error", "Google Client ID not configured")
580
619
  return
581
620
  }
582
- val allScopes = (pendingScopes + scopes.toList()).distinct()
583
621
  val intent = GoogleSignInActivity.createIntent(ctx, clientId, allScopes.toTypedArray(), account.email, origin = "scopes")
584
622
  ctx.startActivity(intent)
585
623
  return
@@ -590,7 +628,7 @@ object AuthAdapter {
590
628
  loginMicrosoft(ctx, mergedScopes.toTypedArray(), null, tenant, null, "scopes")
591
629
  return
592
630
  }
593
- nativeOnLoginError("scopes", "unknown", "No user logged in")
631
+ nativeOnLoginError("scopes", "not_signed_in", "No user logged in")
594
632
  }
595
633
 
596
634
  // refreshTokenSync uses the legacy silentSignIn because AuthorizationClient (the recommended
@@ -628,7 +666,7 @@ object AuthAdapter {
628
666
  refreshMicrosoftTokenForRefresh(ctx, refreshToken)
629
667
  return
630
668
  }
631
- nativeOnRefreshError("unknown", "No user logged in")
669
+ nativeOnRefreshError("not_signed_in", "No user logged in")
632
670
  }
633
671
 
634
672
  @JvmStatic
@@ -643,6 +681,7 @@ object AuthAdapter {
643
681
  @JvmStatic
644
682
  fun logoutSync(context: Context) {
645
683
  val ctx = appContext ?: context.applicationContext
684
+ clearPkceState()
646
685
  // Clear Credential Manager state (covers One-Tap / passkey credentials).
647
686
  moduleScope.launch {
648
687
  try {
@@ -659,12 +698,13 @@ object AuthAdapter {
659
698
  GoogleSignIn.getClient(ctx, gso).signOut()
660
699
  }
661
700
  inMemoryMicrosoftRefreshToken = null
662
- inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
701
+ inMemoryMicrosoftScopes = defaultMicrosoftScopes
663
702
  }
664
703
 
665
704
  @JvmStatic
666
705
  fun revokeAccessSync(context: Context) {
667
706
  val ctx = appContext ?: context.applicationContext
707
+ clearPkceState()
668
708
  val clientId = getClientIdFromResources(ctx)
669
709
  if (clientId != null) {
670
710
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
@@ -672,7 +712,7 @@ object AuthAdapter {
672
712
  GoogleSignIn.getClient(ctx, gso).revokeAccess()
673
713
  }
674
714
  inMemoryMicrosoftRefreshToken = null
675
- inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
715
+ inMemoryMicrosoftScopes = defaultMicrosoftScopes
676
716
  }
677
717
 
678
718
  private fun getClientIdFromResources(context: Context): String? {
@@ -695,7 +735,7 @@ object AuthAdapter {
695
735
  if (refreshToken != null) {
696
736
  refreshMicrosoftToken(ctx, refreshToken)
697
737
  } else {
698
- nativeOnLoginError("silent", "unknown", "No session")
738
+ nativeOnLoginError("silent", "not_signed_in", "No session")
699
739
  }
700
740
  }
701
741
  }
@@ -704,6 +744,7 @@ object AuthAdapter {
704
744
  val clientId = getMicrosoftClientIdFromResources(context)
705
745
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
706
746
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
747
+ val effectiveScopes = inMemoryMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
707
748
 
708
749
  if (clientId == null) {
709
750
  nativeOnLoginError("silent", "configuration_error", "Microsoft Client ID is required for refresh")
@@ -749,14 +790,12 @@ object AuthAdapter {
749
790
  val claims = decodeJwt(newIdToken)
750
791
 
751
792
  if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
752
- inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
753
- listOf("openid", "email", "profile", "offline_access", "User.Read")
754
- }
793
+ inMemoryMicrosoftScopes = effectiveScopes
755
794
 
756
795
  nativeOnLoginSuccess("silent", "microsoft",
757
796
  claims["preferred_username"] ?: claims["email"],
758
797
  claims["name"], null,
759
- newIdToken, newAccessToken, null, null, expirationTime)
798
+ newIdToken, newAccessToken, null, effectiveScopes.toTypedArray(), expirationTime)
760
799
  } else {
761
800
  if (responseCode in 400..499) {
762
801
  inMemoryMicrosoftRefreshToken = null // Token is invalid, clear it
@@ -765,7 +804,7 @@ object AuthAdapter {
765
804
  val json = org.json.JSONObject(responseBody)
766
805
  val errorCode = json.optString("error", "token_error")
767
806
  val errorDesc = json.optString("error_description", "Token refresh failed")
768
- Pair(errorCode, errorDesc)
807
+ Pair(mapMicrosoftOAuthError(errorCode), errorDesc)
769
808
  } catch (e: Exception) {
770
809
  Pair("token_error", "Token refresh failed")
771
810
  }
@@ -787,6 +826,7 @@ object AuthAdapter {
787
826
  val clientId = getMicrosoftClientIdFromResources(context)
788
827
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
789
828
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
829
+ val effectiveScopes = inMemoryMicrosoftScopes.ifEmpty { defaultMicrosoftScopes }
790
830
 
791
831
  if (clientId == null) {
792
832
  nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
@@ -831,9 +871,7 @@ object AuthAdapter {
831
871
  val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
832
872
 
833
873
  if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
834
- inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
835
- listOf("openid", "email", "profile", "offline_access", "User.Read")
836
- }
874
+ inMemoryMicrosoftScopes = effectiveScopes
837
875
 
838
876
  nativeOnRefreshSuccess(
839
877
  newIdToken.ifEmpty { null },
@@ -849,7 +887,7 @@ object AuthAdapter {
849
887
  val json = org.json.JSONObject(errorBody)
850
888
  val errorCode = json.optString("error", "token_error")
851
889
  val errorDesc = json.optString("error_description", "Token refresh failed")
852
- Pair(errorCode, errorDesc)
890
+ Pair(mapMicrosoftOAuthError(errorCode), errorDesc)
853
891
  } catch (e: Exception) {
854
892
  Pair("token_error", "Token refresh failed")
855
893
  }
@@ -2,6 +2,7 @@
2
2
  #include "PlatformAuth.hpp"
3
3
  #include <algorithm>
4
4
  #include <chrono>
5
+ #include <stdexcept>
5
6
 
6
7
  namespace margelo::nitro::NitroAuth {
7
8
 
@@ -69,10 +70,19 @@ std::function<void()> HybridAuth::onTokensRefreshed(const std::function<void(con
69
70
  }
70
71
 
71
72
  void HybridAuth::logout() {
73
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
72
74
  {
73
75
  std::lock_guard<std::recursive_mutex> lock(_mutex);
76
+ _sessionGeneration++;
74
77
  _currentUser = std::nullopt;
75
78
  _grantedScopes.clear();
79
+ refreshInFlight = _refreshInFlight;
80
+ _refreshInFlight = nullptr;
81
+ }
82
+ if (refreshInFlight) {
83
+ refreshInFlight->reject(
84
+ std::make_exception_ptr(std::runtime_error("not_signed_in"))
85
+ );
76
86
  }
77
87
  PlatformAuth::logout();
78
88
  notifyAuthStateChanged();
@@ -80,12 +90,21 @@ void HybridAuth::logout() {
80
90
 
81
91
  std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
82
92
  auto promise = Promise<void>::create();
93
+ uint64_t generation;
94
+ {
95
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
96
+ generation = _sessionGeneration;
97
+ }
83
98
  auto silentPromise = PlatformAuth::silentRestore();
84
99
  auto self = shared_from_this();
85
- silentPromise->addOnResolvedListener([self, promise](const std::optional<AuthUser>& user) {
100
+ silentPromise->addOnResolvedListener([self, promise, generation](const std::optional<AuthUser>& user) {
86
101
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
87
102
  {
88
103
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
104
+ if (auth->_sessionGeneration != generation) {
105
+ promise->resolve();
106
+ return;
107
+ }
89
108
  auth->_currentUser = user;
90
109
  if (user) {
91
110
  if (user->scopes) {
@@ -111,13 +130,24 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
111
130
 
112
131
  std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
113
132
  auto promise = Promise<void>::create();
133
+ uint64_t generation;
134
+ {
135
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
136
+ generation = _sessionGeneration;
137
+ }
114
138
 
115
139
  auto self = shared_from_this();
116
140
  auto loginPromise = PlatformAuth::login(provider, options);
117
- loginPromise->addOnResolvedListener([self, promise, options](const AuthUser& user) {
141
+ loginPromise->addOnResolvedListener([self, promise, options, generation](const AuthUser& user) {
118
142
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
119
143
  {
120
144
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
145
+ if (auth->_sessionGeneration != generation) {
146
+ promise->reject(
147
+ std::make_exception_ptr(std::runtime_error("cancelled"))
148
+ );
149
+ return;
150
+ }
121
151
  auth->_currentUser = user;
122
152
  if (user.scopes && !user.scopes->empty()) {
123
153
  auth->_grantedScopes = *user.scopes;
@@ -144,12 +174,23 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
144
174
 
145
175
  std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::string>& scopes) {
146
176
  auto promise = Promise<void>::create();
177
+ uint64_t generation;
178
+ {
179
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
180
+ generation = _sessionGeneration;
181
+ }
147
182
  auto self = shared_from_this();
148
183
  auto requestPromise = PlatformAuth::requestScopes(scopes);
149
- requestPromise->addOnResolvedListener([self, promise, scopes](const AuthUser& user) {
184
+ requestPromise->addOnResolvedListener([self, promise, scopes, generation](const AuthUser& user) {
150
185
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
151
186
  {
152
187
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
188
+ if (auth->_sessionGeneration != generation) {
189
+ promise->reject(
190
+ std::make_exception_ptr(std::runtime_error("cancelled"))
191
+ );
192
+ return;
193
+ }
153
194
  auth->_currentUser = user;
154
195
  for (const auto& scope : scopes) {
155
196
  if (std::find(auth->_grantedScopes.begin(), auth->_grantedScopes.end(), scope) == auth->_grantedScopes.end()) {
@@ -191,9 +232,11 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
191
232
  std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken() {
192
233
  auto promise = Promise<std::optional<std::string>>::create();
193
234
  bool needsRefresh = false;
235
+ std::optional<std::string> cachedAccessToken;
194
236
  {
195
237
  std::lock_guard<std::recursive_mutex> lock(_mutex);
196
238
  if (_currentUser && _currentUser->accessToken) {
239
+ cachedAccessToken = _currentUser->accessToken;
197
240
  if (_currentUser->expirationTime) {
198
241
  auto now = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1);
199
242
  if (now + 300000 > *_currentUser->expirationTime) needsRefresh = true;
@@ -210,8 +253,8 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
210
253
 
211
254
  if (needsRefresh) {
212
255
  auto refreshPromise = refreshToken();
213
- refreshPromise->addOnResolvedListener([promise](const AuthTokens& tokens) {
214
- promise->resolve(tokens.accessToken);
256
+ refreshPromise->addOnResolvedListener([promise, cachedAccessToken](const AuthTokens& tokens) {
257
+ promise->resolve(tokens.accessToken.has_value() ? tokens.accessToken : cachedAccessToken);
215
258
  });
216
259
  refreshPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
217
260
  promise->reject(error);
@@ -222,21 +265,26 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
222
265
 
223
266
  std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
224
267
  std::shared_ptr<Promise<AuthTokens>> promise;
268
+ uint64_t generation;
225
269
  {
226
270
  std::lock_guard<std::recursive_mutex> lock(_mutex);
227
271
  if (_refreshInFlight) {
228
272
  return _refreshInFlight;
229
273
  }
274
+ generation = _sessionGeneration;
230
275
  promise = Promise<AuthTokens>::create();
231
276
  _refreshInFlight = promise;
232
277
  }
233
278
 
234
279
  auto self = shared_from_this();
235
280
  auto refreshPromise = PlatformAuth::refreshToken();
236
- refreshPromise->addOnResolvedListener([self, promise](const AuthTokens& tokens) {
281
+ refreshPromise->addOnResolvedListener([self, promise, generation](const AuthTokens& tokens) {
237
282
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
238
283
  {
239
284
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
285
+ if (auth->_sessionGeneration != generation) {
286
+ return;
287
+ }
240
288
  if (auth->_currentUser) {
241
289
  if (tokens.accessToken.has_value()) {
242
290
  auth->_currentUser->accessToken = tokens.accessToken;
@@ -260,10 +308,13 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
260
308
  promise->resolve(tokens);
261
309
  });
262
310
 
263
- refreshPromise->addOnRejectedListener([self, promise](const std::exception_ptr& error) {
311
+ refreshPromise->addOnRejectedListener([self, promise, generation](const std::exception_ptr& error) {
264
312
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
265
313
  {
266
314
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
315
+ if (auth->_sessionGeneration != generation) {
316
+ return;
317
+ }
267
318
  if (auth->_refreshInFlight == promise) {
268
319
  auth->_refreshInFlight = nullptr;
269
320
  }
@@ -48,6 +48,7 @@ private:
48
48
  std::map<uint64_t, std::function<void(const AuthTokens&)>> _tokenListeners;
49
49
  uint64_t _nextTokenListenerId = 0;
50
50
  std::shared_ptr<Promise<AuthTokens>> _refreshInFlight;
51
+ uint64_t _sessionGeneration = 0;
51
52
 
52
53
  // recursive_mutex: listeners resolved inside a lock scope may re-enter Auth methods
53
54
  // that also acquire _mutex, causing deadlock with a non-recursive mutex.
@@ -25,8 +25,7 @@ public class AuthAdapter: NSObject {
25
25
  let serverClientId = Bundle.main.object(forInfoDictionaryKey: "GIDServerClientID") as? String
26
26
 
27
27
  DispatchQueue.main.async {
28
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
29
- let rootVC = windowScene.windows.first?.rootViewController else {
28
+ guard let rootVC = presentingViewController() else {
30
29
  completion(nil, "no_window")
31
30
  return
32
31
  }
@@ -62,8 +61,7 @@ public class AuthAdapter: NSObject {
62
61
  objc_setAssociatedObject(controller, &delegateHandle, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
63
62
 
64
63
  DispatchQueue.main.async {
65
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
66
- let window = windowScene.windows.first(where: { $0.isKeyWindow }) ?? windowScene.windows.first else {
64
+ guard let window = activeWindow() else {
67
65
  completion(nil, "no_window")
68
66
  return
69
67
  }
@@ -191,14 +189,8 @@ public class AuthAdapter: NSObject {
191
189
  completion: completion
192
190
  )
193
191
  }
194
-
195
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
196
- let window = windowScene.windows.first(where: { $0.isKeyWindow }) ?? windowScene.windows.first,
197
- let rootVC = window.rootViewController else {
198
- completion(nil, "no_window")
199
- return
200
- }
201
- guard let window = rootVC.view.window else {
192
+
193
+ guard let window = activeWindow() else {
202
194
  completion(nil, "no_window")
203
195
  return
204
196
  }
@@ -269,7 +261,7 @@ public class AuthAdapter: NSObject {
269
261
 
270
262
  URLSession.shared.dataTask(with: request) { data, response, error in
271
263
  DispatchQueue.main.async {
272
- if let error = error {
264
+ if error != nil {
273
265
  completion(nil, "network_error")
274
266
  return
275
267
  }
@@ -325,6 +317,7 @@ public class AuthAdapter: NSObject {
325
317
  "idToken": idToken,
326
318
  "accessToken": accessToken,
327
319
  "serverAuthCode": "",
320
+ "scopes": scopes,
328
321
  "expirationTime": expirationTime,
329
322
  "underlyingError": ""
330
323
  ]
@@ -431,8 +424,7 @@ public class AuthAdapter: NSObject {
431
424
  public static func addScopes(scopes: [String], completion: @escaping (NSDictionary?, String?) -> Void) {
432
425
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
433
426
  DispatchQueue.main.async {
434
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
435
- let rootVC = windowScene.windows.first?.rootViewController else {
427
+ guard let rootVC = presentingViewController() else {
436
428
  completion(nil, "no_window")
437
429
  return
438
430
  }
@@ -512,6 +504,7 @@ public class AuthAdapter: NSObject {
512
504
  private static func tryMicrosoftSilentRefresh(completion: @escaping (NSDictionary?) -> Void) {
513
505
  tokenStoreLock.lock()
514
506
  let refreshToken = inMemoryMicrosoftRefreshToken
507
+ let currentScopes = inMemoryMicrosoftScopes
515
508
  tokenStoreLock.unlock()
516
509
  guard let refreshToken = refreshToken else {
517
510
  completion(nil)
@@ -592,6 +585,7 @@ public class AuthAdapter: NSObject {
592
585
  "idToken": idToken,
593
586
  "accessToken": accessToken,
594
587
  "serverAuthCode": "",
588
+ "scopes": currentScopes,
595
589
  "expirationTime": expirationTime
596
590
  ]
597
591
  completion(resultData as NSDictionary)
@@ -632,7 +626,7 @@ public class AuthAdapter: NSObject {
632
626
  .data(using: .utf8)
633
627
  URLSession.shared.dataTask(with: request) { data, response, error in
634
628
  DispatchQueue.main.async {
635
- if let error = error {
629
+ if error != nil {
636
630
  completion(nil, "network_error")
637
631
  return
638
632
  }
@@ -697,6 +691,41 @@ public class AuthAdapter: NSObject {
697
691
  inMemoryGoogleServerAuthCode = nil
698
692
  tokenStoreLock.unlock()
699
693
  }
694
+
695
+ private static func activeWindow() -> UIWindow? {
696
+ let windowScenes = UIApplication.shared.connectedScenes
697
+ .compactMap { $0 as? UIWindowScene }
698
+ .filter {
699
+ $0.activationState == .foregroundActive ||
700
+ $0.activationState == .foregroundInactive
701
+ }
702
+
703
+ for scene in windowScenes {
704
+ if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }) {
705
+ return keyWindow
706
+ }
707
+ }
708
+
709
+ return windowScenes.lazy.compactMap { $0.windows.first }.first
710
+ }
711
+
712
+ private static func presentingViewController() -> UIViewController? {
713
+ guard let rootViewController = activeWindow()?.rootViewController else {
714
+ return nil
715
+ }
716
+
717
+ var current = rootViewController
718
+ while let presented = current.presentedViewController {
719
+ current = presented
720
+ }
721
+ if let navigationController = current as? UINavigationController {
722
+ return navigationController.visibleViewController ?? navigationController
723
+ }
724
+ if let tabBarController = current as? UITabBarController {
725
+ return tabBarController.selectedViewController ?? tabBarController
726
+ }
727
+ return current
728
+ }
700
729
  }
701
730
 
702
731
  private var delegateHandle: UInt8 = 0
@@ -18,7 +18,23 @@ namespace margelo::nitro::NitroAuth {
18
18
 
19
19
  inline std::optional<std::string> nsToStd(NSString* _Nullable ns) {
20
20
  if (ns == nil) return std::nullopt;
21
- return std::string([ns UTF8String]);
21
+ std::string value([ns UTF8String]);
22
+ if (value.empty()) return std::nullopt;
23
+ return value;
24
+ }
25
+
26
+ inline std::optional<std::vector<std::string>> nsArrayToStd(NSArray<NSString*>* _Nullable nsArray) {
27
+ if (nsArray == nil || nsArray.count == 0) return std::nullopt;
28
+
29
+ std::vector<std::string> values;
30
+ values.reserve(nsArray.count);
31
+ for (NSString* value in nsArray) {
32
+ if (value.length == 0) continue;
33
+ values.emplace_back([value UTF8String]);
34
+ }
35
+
36
+ if (values.empty()) return std::nullopt;
37
+ return values;
22
38
  }
23
39
 
24
40
  std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
@@ -85,6 +101,7 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
85
101
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
86
102
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
87
103
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
104
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
88
105
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
89
106
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
90
107
 
@@ -123,6 +140,7 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
123
140
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
124
141
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
125
142
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
143
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
126
144
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
127
145
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
128
146
  promise->resolve(user);
@@ -168,6 +186,7 @@ std::shared_ptr<Promise<std::optional<AuthUser>>> PlatformAuth::silentRestore()
168
186
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
169
187
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
170
188
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
189
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
171
190
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
172
191
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
173
192
  promise->resolve(user);