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.
- package/README.md +288 -907
- package/android/src/main/cpp/PlatformAuth+Android.cpp +1 -1
- package/android/src/main/java/com/auth/AuthAdapter.kt +65 -27
- package/cpp/HybridAuth.cpp +58 -7
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +45 -16
- package/ios/PlatformAuth+iOS.mm +20 -1
- package/lib/commonjs/service.js +21 -17
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/use-auth.js +26 -10
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +9 -2
- package/lib/commonjs/utils/auth-error.js.map +1 -1
- package/lib/module/service.js +21 -17
- package/lib/module/service.js.map +1 -1
- package/lib/module/use-auth.js +26 -10
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +9 -2
- package/lib/module/utils/auth-error.js.map +1 -1
- package/lib/typescript/commonjs/Auth.nitro.d.ts +2 -2
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +2 -2
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/auth-error.d.ts.map +1 -1
- package/nitro.json +4 -1
- package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -0
- package/package.json +10 -8
- package/src/Auth.nitro.ts +3 -1
- package/src/service.ts +22 -17
- package/src/use-auth.ts +35 -6
- 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 == "
|
|
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)
|
|
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
|
-
|
|
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() ?:
|
|
231
|
+
val effectiveScopes = scopes?.toList() ?: defaultMicrosoftScopes
|
|
221
232
|
val effectivePrompt = prompt ?: "select_account"
|
|
222
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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", "
|
|
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("
|
|
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 =
|
|
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 =
|
|
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", "
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
}
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -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
|
}
|
package/cpp/HybridAuth.hpp
CHANGED
|
@@ -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.
|
package/ios/AuthAdapter.swift
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/ios/PlatformAuth+iOS.mm
CHANGED
|
@@ -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
|
-
|
|
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);
|