react-native-nitro-auth 0.5.8 → 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 +64 -50
- package/cpp/HybridAuth.cpp +135 -50
- package/cpp/HybridAuth.hpp +1 -0
- 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/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/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/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/package.json +7 -5
- 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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.9 - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added shared JS service factory coverage and logger behavior tests.
|
|
8
|
+
- Added C++ coverage support with `test:cpp:coverage` and expanded native tests for session restore, token refresh, access-token fallback, scope updates, listener isolation, logout cancellation, and serializer behavior.
|
|
9
|
+
- Added example-app smoke checks for the public auth API, provider support, platform-gated behavior, and session-dependent methods.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated Expo SDK 55 patch dependencies, React Native 0.83.6, Nitro Modules 0.35.5, and related build tooling.
|
|
14
|
+
- Refactored native/web `AuthService` creation so native and web error mapping stay consistent.
|
|
15
|
+
- Hardened web OAuth state, cache parsing, token refresh, and provider error handling.
|
|
16
|
+
- Improved example app handling for unsupported providers and unavailable session actions.
|
|
17
|
+
- Improved release validation to include JS and C++ coverage gates and a faster publish dry run path.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Fixed native runtime crashes in the example app when optional Nitro methods are not available on the installed native object.
|
|
22
|
+
- Fixed startup `silentRestore()` errors in the example app so restore failures surface in status instead of becoming unhandled promises.
|
|
23
|
+
- Excluded C++ test sources from the iOS pod target to avoid app-target duplicate `main` symbols.
|
|
24
|
+
|
|
25
|
+
### Verified
|
|
26
|
+
|
|
27
|
+
- `bun run check:ci`
|
|
28
|
+
- `bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand`
|
|
29
|
+
- `bun run --cwd packages/react-native-nitro-auth test:cpp:coverage`
|
|
30
|
+
- `bun run publish-package:dry-run`
|
|
31
|
+
- `bun run example:prebuild`
|
|
32
|
+
- `bun run example:android`
|
|
33
|
+
- `bun run example:ios`
|
package/README.md
CHANGED
|
@@ -13,15 +13,16 @@ Fast React Native authentication for Google Sign-In, Apple Sign-In, and Microsof
|
|
|
13
13
|
- Typed `useAuth()` hook, `AuthService`, `SocialButton`, and `AuthError`.
|
|
14
14
|
- App-owned persistence model: tokens stay in memory unless your app stores a snapshot.
|
|
15
15
|
- Built-in flows for silent restore, token refresh, account picker, login hints, and incremental Google scopes.
|
|
16
|
+
- Consistent `AuthError` mapping for async `AuthService` failures on native and web.
|
|
16
17
|
|
|
17
18
|
## Choose Your Path
|
|
18
19
|
|
|
19
|
-
| Need
|
|
20
|
-
|
|
|
21
|
-
| Google, Apple, or Microsoft sign-in in an Expo or React Native app
|
|
22
|
-
| Generic OAuth or OIDC provider not covered by this package
|
|
20
|
+
| Need | Use |
|
|
21
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
22
|
+
| Google, Apple, or Microsoft sign-in in an Expo or React Native app | `react-native-nitro-auth` |
|
|
23
|
+
| Generic OAuth or OIDC provider not covered by this package | `expo-auth-session` or `react-native-app-auth` |
|
|
23
24
|
| Firebase user management, password auth, MFA, and hosted auth platform | `@react-native-firebase/auth`, Auth0, Authgear, or your IDaaS SDK |
|
|
24
|
-
| Server-side session validation
|
|
25
|
+
| Server-side session validation | Your backend; client JWT decode is display-only |
|
|
25
26
|
|
|
26
27
|
## Install
|
|
27
28
|
|
|
@@ -43,12 +44,12 @@ cd ios && pod install
|
|
|
43
44
|
|
|
44
45
|
## Requirements
|
|
45
46
|
|
|
46
|
-
| Runtime
|
|
47
|
-
|
|
|
48
|
-
| React Native
|
|
49
|
-
| Nitro Modules
|
|
50
|
-
| iOS
|
|
51
|
-
| Android
|
|
47
|
+
| Runtime | Requirement |
|
|
48
|
+
| --------------------- | ---------------------------------------- |
|
|
49
|
+
| React Native | `>=0.75` |
|
|
50
|
+
| Nitro Modules | `>=0.35` |
|
|
51
|
+
| iOS | 15.1+ recommended |
|
|
52
|
+
| Android | min SDK 24+ recommended |
|
|
52
53
|
| Expo example baseline | Expo SDK 55, React Native 0.83, React 19 |
|
|
53
54
|
|
|
54
55
|
## Expo Setup
|
|
@@ -102,19 +103,19 @@ export default {
|
|
|
102
103
|
|
|
103
104
|
### Plugin Options
|
|
104
105
|
|
|
105
|
-
| Option
|
|
106
|
-
|
|
|
107
|
-
| `ios.googleClientId`
|
|
108
|
-
| `ios.googleServerClientId`
|
|
109
|
-
| `ios.googleUrlScheme`
|
|
110
|
-
| `ios.appleSignIn`
|
|
111
|
-
| `ios.microsoftClientId`
|
|
112
|
-
| `ios.microsoftTenant`
|
|
113
|
-
| `ios.microsoftB2cDomain`
|
|
114
|
-
| `android.googleClientId`
|
|
115
|
-
| `android.microsoftClientId`
|
|
116
|
-
| `android.microsoftTenant`
|
|
117
|
-
| `android.microsoftB2cDomain` | Android
|
|
106
|
+
| Option | Platform | Purpose |
|
|
107
|
+
| ---------------------------- | -------- | ---------------------------------------------------------------------- |
|
|
108
|
+
| `ios.googleClientId` | iOS | Google iOS OAuth client ID |
|
|
109
|
+
| `ios.googleServerClientId` | iOS | Google web/server client ID for server auth code flows |
|
|
110
|
+
| `ios.googleUrlScheme` | iOS | Reversed iOS client ID URL scheme |
|
|
111
|
+
| `ios.appleSignIn` | iOS | Adds Apple Sign-In entitlement when `true` |
|
|
112
|
+
| `ios.microsoftClientId` | iOS | Microsoft app/client ID |
|
|
113
|
+
| `ios.microsoftTenant` | iOS | Microsoft tenant, `common`, `organizations`, `consumers`, or tenant ID |
|
|
114
|
+
| `ios.microsoftB2cDomain` | iOS | Azure AD B2C domain |
|
|
115
|
+
| `android.googleClientId` | Android | Google web OAuth client ID |
|
|
116
|
+
| `android.microsoftClientId` | Android | Microsoft app/client ID |
|
|
117
|
+
| `android.microsoftTenant` | Android | Microsoft tenant |
|
|
118
|
+
| `android.microsoftB2cDomain` | Android | Azure AD B2C domain |
|
|
118
119
|
|
|
119
120
|
## Provider Setup
|
|
120
121
|
|
|
@@ -239,16 +240,16 @@ await login("microsoft", {
|
|
|
239
240
|
});
|
|
240
241
|
```
|
|
241
242
|
|
|
242
|
-
| Option
|
|
243
|
-
|
|
|
244
|
-
| `scopes`
|
|
245
|
-
| `loginHint`
|
|
246
|
-
| `useOneTap`
|
|
247
|
-
| `useSheet`
|
|
248
|
-
| `forceAccountPicker`
|
|
249
|
-
| `useLegacyGoogleSignIn` | Android Google
|
|
250
|
-
| `tenant`
|
|
251
|
-
| `prompt`
|
|
243
|
+
| Option | Applies to | Notes |
|
|
244
|
+
| ----------------------- | ----------------- | ---------------------------------------------------- |
|
|
245
|
+
| `scopes` | Google, Microsoft | Requested OAuth scopes |
|
|
246
|
+
| `loginHint` | Google, Microsoft | Prefills account selection when supported |
|
|
247
|
+
| `useOneTap` | Android Google | Enables Credential Manager auto-select |
|
|
248
|
+
| `useSheet` | iOS Google | Uses native sign-in sheet behavior |
|
|
249
|
+
| `forceAccountPicker` | Google | Forces account picker |
|
|
250
|
+
| `useLegacyGoogleSignIn` | Android Google | Uses legacy Google Sign-In path for server auth code |
|
|
251
|
+
| `tenant` | Microsoft | Overrides configured tenant |
|
|
252
|
+
| `prompt` | Microsoft | `login`, `consent`, `select_account`, or `none` |
|
|
252
253
|
|
|
253
254
|
## Incremental Scopes
|
|
254
255
|
|
|
@@ -288,7 +289,7 @@ extra: {
|
|
|
288
289
|
|
|
289
290
|
## Error Contract
|
|
290
291
|
|
|
291
|
-
All public async APIs throw `AuthError
|
|
292
|
+
All public async APIs throw `AuthError`, including provider errors surfaced by native and web `AuthService` implementations.
|
|
292
293
|
|
|
293
294
|
```ts
|
|
294
295
|
try {
|
|
@@ -411,16 +412,16 @@ The demo includes:
|
|
|
411
412
|
|
|
412
413
|
## Troubleshooting
|
|
413
414
|
|
|
414
|
-
| Symptom
|
|
415
|
-
|
|
|
416
|
-
| `configuration_error` on Google
|
|
417
|
-
| Google works in debug but not release | Add release SHA-1/SHA-256 fingerprints to Google Cloud Console
|
|
418
|
-
| Android `hasPlayServices` is false
|
|
419
|
-
| Apple email/name missing
|
|
420
|
-
| Microsoft `invalid_state`
|
|
421
|
-
| Microsoft `token_error`
|
|
422
|
-
| Web popup blocked
|
|
423
|
-
| `operation_in_progress`
|
|
415
|
+
| Symptom | Check |
|
|
416
|
+
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
417
|
+
| `configuration_error` on Google | Client ID is missing or wrong for the current platform |
|
|
418
|
+
| Google works in debug but not release | Add release SHA-1/SHA-256 fingerprints to Google Cloud Console |
|
|
419
|
+
| Android `hasPlayServices` is false | Use an emulator image with Google Play Services |
|
|
420
|
+
| Apple email/name missing | Apple only returns these fields on first authorization |
|
|
421
|
+
| Microsoft `invalid_state` | Redirect URI or app resume path is wrong, or an old auth redirect completed late |
|
|
422
|
+
| Microsoft `token_error` | Check tenant, client ID, redirect URI, and requested scopes |
|
|
423
|
+
| Web popup blocked | Call `login()` from a user gesture such as a button press |
|
|
424
|
+
| `operation_in_progress` | A provider flow is already active; wait for it to finish or sign out |
|
|
424
425
|
|
|
425
426
|
## Production Notes
|
|
426
427
|
|
|
@@ -433,13 +434,26 @@ The demo includes:
|
|
|
433
434
|
## Release Checks
|
|
434
435
|
|
|
435
436
|
```sh
|
|
436
|
-
bun run
|
|
437
|
-
|
|
437
|
+
bun run publish-package:dry-run
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The publish script runs frozen install, core-version verification, codegen, build, lint, typecheck, Jest, JS coverage, C++ tests, C++ coverage, Expo Doctor, package docs sync, pack dry run, and `bun publish --dry-run --ignore-scripts`.
|
|
441
|
+
|
|
442
|
+
For faster local iteration before the full release dry run:
|
|
443
|
+
|
|
444
|
+
```sh
|
|
438
445
|
bun run check
|
|
439
446
|
bun run test:cpp
|
|
440
|
-
bun
|
|
441
|
-
bun
|
|
442
|
-
|
|
447
|
+
bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand
|
|
448
|
+
bun run --cwd packages/react-native-nitro-auth test:cpp:coverage
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Before shipping provider or native config changes, also verify the example app:
|
|
452
|
+
|
|
453
|
+
```sh
|
|
454
|
+
bun run example:prebuild
|
|
455
|
+
bun run example:android
|
|
456
|
+
bun run example:ios
|
|
443
457
|
```
|
|
444
458
|
|
|
445
459
|
## License
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -2,10 +2,63 @@
|
|
|
2
2
|
#include "PlatformAuth.hpp"
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <chrono>
|
|
5
|
+
#include <exception>
|
|
5
6
|
#include <stdexcept>
|
|
7
|
+
#include <unordered_set>
|
|
6
8
|
|
|
7
9
|
namespace margelo::nitro::NitroAuth {
|
|
8
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
|
+
|
|
9
62
|
HybridAuth::HybridAuth() : HybridObject(TAG) {
|
|
10
63
|
// In-memory only - no internal persistence.
|
|
11
64
|
}
|
|
@@ -30,13 +83,12 @@ void HybridAuth::notifyAuthStateChanged() {
|
|
|
30
83
|
{
|
|
31
84
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
32
85
|
user = _currentUser;
|
|
86
|
+
listeners.reserve(_listeners.size());
|
|
33
87
|
for (auto const& [id, listener] : _listeners) {
|
|
34
88
|
listeners.push_back(listener);
|
|
35
89
|
}
|
|
36
90
|
}
|
|
37
|
-
|
|
38
|
-
listener(user);
|
|
39
|
-
}
|
|
91
|
+
invokeListenersSafely(listeners, user);
|
|
40
92
|
}
|
|
41
93
|
|
|
42
94
|
std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(const std::optional<AuthUser>&)>& callback) {
|
|
@@ -49,6 +101,7 @@ std::function<void()> HybridAuth::onAuthStateChanged(const std::function<void(co
|
|
|
49
101
|
auto self = weak.lock();
|
|
50
102
|
if (!self) return;
|
|
51
103
|
auto* auth = dynamic_cast<HybridAuth*>(self.get());
|
|
104
|
+
if (!auth) return;
|
|
52
105
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
53
106
|
auth->_listeners.erase(id);
|
|
54
107
|
};
|
|
@@ -64,26 +117,28 @@ std::function<void()> HybridAuth::onTokensRefreshed(const std::function<void(con
|
|
|
64
117
|
auto self = weak.lock();
|
|
65
118
|
if (!self) return;
|
|
66
119
|
auto* auth = dynamic_cast<HybridAuth*>(self.get());
|
|
120
|
+
if (!auth) return;
|
|
67
121
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
68
122
|
auth->_tokenListeners.erase(id);
|
|
69
123
|
};
|
|
70
124
|
}
|
|
71
125
|
|
|
126
|
+
std::shared_ptr<Promise<AuthTokens>> HybridAuth::advanceSessionGenerationLocked() {
|
|
127
|
+
_sessionGeneration++;
|
|
128
|
+
auto refreshInFlight = _refreshInFlight;
|
|
129
|
+
_refreshInFlight = nullptr;
|
|
130
|
+
return refreshInFlight;
|
|
131
|
+
}
|
|
132
|
+
|
|
72
133
|
void HybridAuth::logout() {
|
|
73
134
|
std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
|
|
74
135
|
{
|
|
75
136
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
76
|
-
|
|
137
|
+
refreshInFlight = advanceSessionGenerationLocked();
|
|
77
138
|
_currentUser = std::nullopt;
|
|
78
139
|
_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
|
-
);
|
|
86
140
|
}
|
|
141
|
+
rejectIfPending(refreshInFlight, "not_signed_in");
|
|
87
142
|
PlatformAuth::logout();
|
|
88
143
|
notifyAuthStateChanged();
|
|
89
144
|
}
|
|
@@ -99,12 +154,18 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
|
|
|
99
154
|
auto self = shared_from_this();
|
|
100
155
|
silentPromise->addOnResolvedListener([self, promise, generation](const std::optional<AuthUser>& user) {
|
|
101
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;
|
|
102
162
|
{
|
|
103
163
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
104
164
|
if (auth->_sessionGeneration != generation) {
|
|
105
165
|
promise->resolve();
|
|
106
166
|
return;
|
|
107
167
|
}
|
|
168
|
+
refreshInFlight = auth->advanceSessionGenerationLocked();
|
|
108
169
|
auth->_currentUser = user;
|
|
109
170
|
if (user) {
|
|
110
171
|
if (user->scopes) {
|
|
@@ -116,6 +177,7 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
|
|
|
116
177
|
auth->_grantedScopes.clear();
|
|
117
178
|
}
|
|
118
179
|
}
|
|
180
|
+
rejectIfPending(refreshInFlight, "cancelled");
|
|
119
181
|
// Always resolve - no session is not an error, just means user is logged out
|
|
120
182
|
auth->notifyAuthStateChanged();
|
|
121
183
|
promise->resolve();
|
|
@@ -131,23 +193,30 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
|
|
|
131
193
|
std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
|
|
132
194
|
auto promise = Promise<void>::create();
|
|
133
195
|
uint64_t generation;
|
|
196
|
+
std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
|
|
134
197
|
{
|
|
135
198
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
199
|
+
refreshInFlight = advanceSessionGenerationLocked();
|
|
136
200
|
generation = _sessionGeneration;
|
|
137
201
|
}
|
|
202
|
+
rejectIfPending(refreshInFlight, "cancelled");
|
|
138
203
|
|
|
139
204
|
auto self = shared_from_this();
|
|
140
205
|
auto loginPromise = PlatformAuth::login(provider, options);
|
|
141
206
|
loginPromise->addOnResolvedListener([self, promise, options, generation](const AuthUser& user) {
|
|
142
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;
|
|
143
213
|
{
|
|
144
214
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
145
215
|
if (auth->_sessionGeneration != generation) {
|
|
146
|
-
promise->reject(
|
|
147
|
-
std::make_exception_ptr(std::runtime_error("cancelled"))
|
|
148
|
-
);
|
|
216
|
+
promise->reject(makeAuthError("cancelled"));
|
|
149
217
|
return;
|
|
150
218
|
}
|
|
219
|
+
refreshInFlight = auth->advanceSessionGenerationLocked();
|
|
151
220
|
auth->_currentUser = user;
|
|
152
221
|
if (user.scopes && !user.scopes->empty()) {
|
|
153
222
|
auth->_grantedScopes = *user.scopes;
|
|
@@ -162,6 +231,7 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
|
|
|
162
231
|
: std::make_optional(auth->_grantedScopes);
|
|
163
232
|
}
|
|
164
233
|
}
|
|
234
|
+
rejectIfPending(refreshInFlight, "cancelled");
|
|
165
235
|
auth->notifyAuthStateChanged();
|
|
166
236
|
promise->resolve();
|
|
167
237
|
});
|
|
@@ -183,20 +253,18 @@ std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::
|
|
|
183
253
|
auto requestPromise = PlatformAuth::requestScopes(scopes);
|
|
184
254
|
requestPromise->addOnResolvedListener([self, promise, scopes, generation](const AuthUser& user) {
|
|
185
255
|
auto* auth = dynamic_cast<HybridAuth*>(self.get());
|
|
256
|
+
if (!auth) {
|
|
257
|
+
promise->reject(makeAuthError("internal_error"));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
186
260
|
{
|
|
187
261
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
188
262
|
if (auth->_sessionGeneration != generation) {
|
|
189
|
-
promise->reject(
|
|
190
|
-
std::make_exception_ptr(std::runtime_error("cancelled"))
|
|
191
|
-
);
|
|
263
|
+
promise->reject(makeAuthError("cancelled"));
|
|
192
264
|
return;
|
|
193
265
|
}
|
|
194
266
|
auth->_currentUser = user;
|
|
195
|
-
|
|
196
|
-
if (std::find(auth->_grantedScopes.begin(), auth->_grantedScopes.end(), scope) == auth->_grantedScopes.end()) {
|
|
197
|
-
auth->_grantedScopes.push_back(scope);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
267
|
+
mergeGrantedScopes(auth->_grantedScopes, scopes);
|
|
200
268
|
if (auth->_currentUser) auth->_currentUser->scopes = auth->_grantedScopes;
|
|
201
269
|
}
|
|
202
270
|
auth->notifyAuthStateChanged();
|
|
@@ -213,13 +281,7 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
|
|
|
213
281
|
auto promise = Promise<void>::create();
|
|
214
282
|
{
|
|
215
283
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
216
|
-
_grantedScopes
|
|
217
|
-
std::remove_if(_grantedScopes.begin(), _grantedScopes.end(),
|
|
218
|
-
[&scopes](const std::string& s) {
|
|
219
|
-
return std::find(scopes.begin(), scopes.end(), s) != scopes.end();
|
|
220
|
-
}),
|
|
221
|
-
_grantedScopes.end()
|
|
222
|
-
);
|
|
284
|
+
removeGrantedScopes(_grantedScopes, scopes);
|
|
223
285
|
if (_currentUser) {
|
|
224
286
|
_currentUser->scopes = _grantedScopes;
|
|
225
287
|
}
|
|
@@ -280,28 +342,41 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
|
|
|
280
342
|
auto refreshPromise = PlatformAuth::refreshToken();
|
|
281
343
|
refreshPromise->addOnResolvedListener([self, promise, generation](const AuthTokens& tokens) {
|
|
282
344
|
auto* auth = dynamic_cast<HybridAuth*>(self.get());
|
|
345
|
+
if (!auth) {
|
|
346
|
+
promise->reject(makeAuthError("internal_error"));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
bool isStale = false;
|
|
283
350
|
{
|
|
284
351
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
285
352
|
if (auth->_sessionGeneration != generation) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (auth->_currentUser) {
|
|
289
|
-
if (tokens.accessToken.has_value()) {
|
|
290
|
-
auth->_currentUser->accessToken = tokens.accessToken;
|
|
353
|
+
if (auth->_refreshInFlight == promise) {
|
|
354
|
+
auth->_refreshInFlight = nullptr;
|
|
291
355
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
371
|
}
|
|
298
|
-
if (
|
|
299
|
-
auth->
|
|
372
|
+
if (auth->_refreshInFlight == promise) {
|
|
373
|
+
auth->_refreshInFlight = nullptr;
|
|
300
374
|
}
|
|
301
375
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
376
|
+
}
|
|
377
|
+
if (isStale) {
|
|
378
|
+
rejectIfPending(promise, "cancelled");
|
|
379
|
+
return;
|
|
305
380
|
}
|
|
306
381
|
auth->notifyTokensRefreshed(tokens);
|
|
307
382
|
auth->notifyAuthStateChanged();
|
|
@@ -310,15 +385,26 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
|
|
|
310
385
|
|
|
311
386
|
refreshPromise->addOnRejectedListener([self, promise, generation](const std::exception_ptr& error) {
|
|
312
387
|
auto* auth = dynamic_cast<HybridAuth*>(self.get());
|
|
388
|
+
if (!auth) {
|
|
389
|
+
promise->reject(makeAuthError("internal_error"));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
bool isStale = false;
|
|
313
393
|
{
|
|
314
394
|
std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
|
|
315
395
|
if (auth->_sessionGeneration != generation) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
396
|
+
if (auth->_refreshInFlight == promise) {
|
|
397
|
+
auth->_refreshInFlight = nullptr;
|
|
398
|
+
}
|
|
399
|
+
isStale = true;
|
|
400
|
+
} else if (auth->_refreshInFlight == promise) {
|
|
319
401
|
auth->_refreshInFlight = nullptr;
|
|
320
402
|
}
|
|
321
403
|
}
|
|
404
|
+
if (isStale) {
|
|
405
|
+
rejectIfPending(promise, "cancelled");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
322
408
|
promise->reject(error);
|
|
323
409
|
});
|
|
324
410
|
return promise;
|
|
@@ -332,13 +418,12 @@ void HybridAuth::notifyTokensRefreshed(const AuthTokens& tokens) {
|
|
|
332
418
|
std::vector<std::function<void(const AuthTokens&)>> listeners;
|
|
333
419
|
{
|
|
334
420
|
std::lock_guard<std::recursive_mutex> lock(_mutex);
|
|
421
|
+
listeners.reserve(_tokenListeners.size());
|
|
335
422
|
for (auto const& [id, listener] : _tokenListeners) {
|
|
336
423
|
listeners.push_back(listener);
|
|
337
424
|
}
|
|
338
425
|
}
|
|
339
|
-
|
|
340
|
-
listener(tokens);
|
|
341
|
-
}
|
|
426
|
+
invokeListenersSafely(listeners, tokens);
|
|
342
427
|
}
|
|
343
428
|
|
|
344
429
|
} // namespace margelo::nitro::NitroAuth
|
package/cpp/HybridAuth.hpp
CHANGED