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.
- package/CHANGELOG.md +33 -0
- package/README.md +295 -906
- package/android/src/main/java/com/auth/AuthAdapter.kt +22 -6
- package/cpp/HybridAuth.cpp +175 -39
- package/cpp/HybridAuth.hpp +2 -0
- package/ios/AuthAdapter.swift +2 -2
- package/lib/commonjs/Auth.web.js +141 -73
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/create-auth-service.js +71 -0
- package/lib/commonjs/create-auth-service.js.map +1 -0
- package/lib/commonjs/service.js +2 -79
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +2 -79
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +6 -3
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +8 -1
- package/lib/commonjs/utils/auth-error.js.map +1 -1
- package/lib/module/Auth.web.js +141 -73
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/create-auth-service.js +67 -0
- package/lib/module/create-auth-service.js.map +1 -0
- package/lib/module/service.js +2 -79
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +2 -79
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/use-auth.js +6 -3
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +8 -1
- package/lib/module/utils/auth-error.js.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +4 -2
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/create-auth-service.d.ts +5 -0
- package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -0
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.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.web.d.ts +4 -2
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/create-auth-service.d.ts +5 -0
- package/lib/typescript/module/create-auth-service.d.ts.map +1 -0
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.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/package.json +12 -9
- package/react-native-nitro-auth.podspec +1 -0
- package/src/Auth.web.ts +261 -102
- package/src/create-auth-service.ts +97 -0
- package/src/service.ts +3 -101
- package/src/service.web.ts +3 -101
- package/src/use-auth.ts +7 -3
- 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
|
-
|
|
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,
|
|
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
|
}
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -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
|
-
|
|
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->
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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->
|
|
241
|
-
if (
|
|
242
|
-
auth->
|
|
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
|
-
|
|
248
|
-
|
|
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 (
|
|
251
|
-
auth->
|
|
372
|
+
if (auth->_refreshInFlight == promise) {
|
|
373
|
+
auth->_refreshInFlight = nullptr;
|
|
252
374
|
}
|
|
253
375
|
}
|
|
254
|
-
|
|
255
|
-
|
|
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->
|
|
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
|
-
|
|
289
|
-
listener(tokens);
|
|
290
|
-
}
|
|
426
|
+
invokeListenersSafely(listeners, tokens);
|
|
291
427
|
}
|
|
292
428
|
|
|
293
429
|
} // namespace margelo::nitro::NitroAuth
|
package/cpp/HybridAuth.hpp
CHANGED
|
@@ -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.
|
package/ios/AuthAdapter.swift
CHANGED
|
@@ -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
|
|
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
|
|
629
|
+
if error != nil {
|
|
630
630
|
completion(nil, "network_error")
|
|
631
631
|
return
|
|
632
632
|
}
|