react-native-nitro-auth 0.5.8 → 0.5.10
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 +48 -0
- package/README.md +69 -50
- package/cpp/HybridAuth.cpp +135 -50
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +28 -9
- 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,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.10 - 2026-04-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fixed iOS Microsoft sign-in so `ASWebAuthenticationSession` is retained until callback or cancellation and duplicate sessions fail with `operation_in_progress`.
|
|
8
|
+
- Fixed the example app header so it displays the current package version.
|
|
9
|
+
|
|
10
|
+
### Verified
|
|
11
|
+
|
|
12
|
+
- `bun run check:ci`
|
|
13
|
+
- `bunx expo install --check --cwd apps/example`
|
|
14
|
+
- `bunx expo-doctor@latest apps/example`
|
|
15
|
+
- `bun run example:prebuild`
|
|
16
|
+
- `bun run publish-package:dry-run`
|
|
17
|
+
|
|
18
|
+
## 0.5.9 - 2026-04-24
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added shared JS service factory coverage and logger behavior tests.
|
|
23
|
+
- 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.
|
|
24
|
+
- Added example-app smoke checks for the public auth API, provider support, platform-gated behavior, and session-dependent methods.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Updated Expo SDK 55 patch dependencies, React Native 0.83.6, Nitro Modules 0.35.5, and related build tooling.
|
|
29
|
+
- Refactored native/web `AuthService` creation so native and web error mapping stay consistent.
|
|
30
|
+
- Hardened web OAuth state, cache parsing, token refresh, and provider error handling.
|
|
31
|
+
- Improved example app handling for unsupported providers and unavailable session actions.
|
|
32
|
+
- Improved release validation to include JS and C++ coverage gates and a faster publish dry run path.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Fixed native runtime crashes in the example app when optional Nitro methods are not available on the installed native object.
|
|
37
|
+
- Fixed startup `silentRestore()` errors in the example app so restore failures surface in status instead of becoming unhandled promises.
|
|
38
|
+
- Excluded C++ test sources from the iOS pod target to avoid app-target duplicate `main` symbols.
|
|
39
|
+
|
|
40
|
+
### Verified
|
|
41
|
+
|
|
42
|
+
- `bun run check:ci`
|
|
43
|
+
- `bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand`
|
|
44
|
+
- `bun run --cwd packages/react-native-nitro-auth test:cpp:coverage`
|
|
45
|
+
- `bun run publish-package:dry-run`
|
|
46
|
+
- `bun run example:prebuild`
|
|
47
|
+
- `bun run example:android`
|
|
48
|
+
- `bun run example:ios`
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# react-native-nitro-auth
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
3
8
|
Fast React Native authentication for Google Sign-In, Apple Sign-In, and Microsoft Entra ID, built on Nitro Modules and JSI.
|
|
4
9
|
|
|
5
10
|
`react-native-nitro-auth` gives Expo and React Native apps one typed API for native social login, web OAuth, token refresh, incremental scopes, and auth state listeners without owning your app's long-term token storage.
|
|
@@ -13,15 +18,16 @@ Fast React Native authentication for Google Sign-In, Apple Sign-In, and Microsof
|
|
|
13
18
|
- Typed `useAuth()` hook, `AuthService`, `SocialButton`, and `AuthError`.
|
|
14
19
|
- App-owned persistence model: tokens stay in memory unless your app stores a snapshot.
|
|
15
20
|
- Built-in flows for silent restore, token refresh, account picker, login hints, and incremental Google scopes.
|
|
21
|
+
- Consistent `AuthError` mapping for async `AuthService` failures on native and web.
|
|
16
22
|
|
|
17
23
|
## Choose Your Path
|
|
18
24
|
|
|
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
|
|
25
|
+
| Need | Use |
|
|
26
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
27
|
+
| Google, Apple, or Microsoft sign-in in an Expo or React Native app | `react-native-nitro-auth` |
|
|
28
|
+
| Generic OAuth or OIDC provider not covered by this package | `expo-auth-session` or `react-native-app-auth` |
|
|
23
29
|
| 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
|
|
30
|
+
| Server-side session validation | Your backend; client JWT decode is display-only |
|
|
25
31
|
|
|
26
32
|
## Install
|
|
27
33
|
|
|
@@ -43,12 +49,12 @@ cd ios && pod install
|
|
|
43
49
|
|
|
44
50
|
## Requirements
|
|
45
51
|
|
|
46
|
-
| Runtime
|
|
47
|
-
|
|
|
48
|
-
| React Native
|
|
49
|
-
| Nitro Modules
|
|
50
|
-
| iOS
|
|
51
|
-
| Android
|
|
52
|
+
| Runtime | Requirement |
|
|
53
|
+
| --------------------- | ---------------------------------------- |
|
|
54
|
+
| React Native | `>=0.75` |
|
|
55
|
+
| Nitro Modules | `>=0.35` |
|
|
56
|
+
| iOS | 15.1+ recommended |
|
|
57
|
+
| Android | min SDK 24+ recommended |
|
|
52
58
|
| Expo example baseline | Expo SDK 55, React Native 0.83, React 19 |
|
|
53
59
|
|
|
54
60
|
## Expo Setup
|
|
@@ -102,19 +108,19 @@ export default {
|
|
|
102
108
|
|
|
103
109
|
### Plugin Options
|
|
104
110
|
|
|
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
|
|
111
|
+
| Option | Platform | Purpose |
|
|
112
|
+
| ---------------------------- | -------- | ---------------------------------------------------------------------- |
|
|
113
|
+
| `ios.googleClientId` | iOS | Google iOS OAuth client ID |
|
|
114
|
+
| `ios.googleServerClientId` | iOS | Google web/server client ID for server auth code flows |
|
|
115
|
+
| `ios.googleUrlScheme` | iOS | Reversed iOS client ID URL scheme |
|
|
116
|
+
| `ios.appleSignIn` | iOS | Adds Apple Sign-In entitlement when `true` |
|
|
117
|
+
| `ios.microsoftClientId` | iOS | Microsoft app/client ID |
|
|
118
|
+
| `ios.microsoftTenant` | iOS | Microsoft tenant, `common`, `organizations`, `consumers`, or tenant ID |
|
|
119
|
+
| `ios.microsoftB2cDomain` | iOS | Azure AD B2C domain |
|
|
120
|
+
| `android.googleClientId` | Android | Google web OAuth client ID |
|
|
121
|
+
| `android.microsoftClientId` | Android | Microsoft app/client ID |
|
|
122
|
+
| `android.microsoftTenant` | Android | Microsoft tenant |
|
|
123
|
+
| `android.microsoftB2cDomain` | Android | Azure AD B2C domain |
|
|
118
124
|
|
|
119
125
|
## Provider Setup
|
|
120
126
|
|
|
@@ -239,16 +245,16 @@ await login("microsoft", {
|
|
|
239
245
|
});
|
|
240
246
|
```
|
|
241
247
|
|
|
242
|
-
| Option
|
|
243
|
-
|
|
|
244
|
-
| `scopes`
|
|
245
|
-
| `loginHint`
|
|
246
|
-
| `useOneTap`
|
|
247
|
-
| `useSheet`
|
|
248
|
-
| `forceAccountPicker`
|
|
249
|
-
| `useLegacyGoogleSignIn` | Android Google
|
|
250
|
-
| `tenant`
|
|
251
|
-
| `prompt`
|
|
248
|
+
| Option | Applies to | Notes |
|
|
249
|
+
| ----------------------- | ----------------- | ---------------------------------------------------- |
|
|
250
|
+
| `scopes` | Google, Microsoft | Requested OAuth scopes |
|
|
251
|
+
| `loginHint` | Google, Microsoft | Prefills account selection when supported |
|
|
252
|
+
| `useOneTap` | Android Google | Enables Credential Manager auto-select |
|
|
253
|
+
| `useSheet` | iOS Google | Uses native sign-in sheet behavior |
|
|
254
|
+
| `forceAccountPicker` | Google | Forces account picker |
|
|
255
|
+
| `useLegacyGoogleSignIn` | Android Google | Uses legacy Google Sign-In path for server auth code |
|
|
256
|
+
| `tenant` | Microsoft | Overrides configured tenant |
|
|
257
|
+
| `prompt` | Microsoft | `login`, `consent`, `select_account`, or `none` |
|
|
252
258
|
|
|
253
259
|
## Incremental Scopes
|
|
254
260
|
|
|
@@ -288,7 +294,7 @@ extra: {
|
|
|
288
294
|
|
|
289
295
|
## Error Contract
|
|
290
296
|
|
|
291
|
-
All public async APIs throw `AuthError
|
|
297
|
+
All public async APIs throw `AuthError`, including provider errors surfaced by native and web `AuthService` implementations.
|
|
292
298
|
|
|
293
299
|
```ts
|
|
294
300
|
try {
|
|
@@ -411,16 +417,16 @@ The demo includes:
|
|
|
411
417
|
|
|
412
418
|
## Troubleshooting
|
|
413
419
|
|
|
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`
|
|
420
|
+
| Symptom | Check |
|
|
421
|
+
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
422
|
+
| `configuration_error` on Google | Client ID is missing or wrong for the current platform |
|
|
423
|
+
| Google works in debug but not release | Add release SHA-1/SHA-256 fingerprints to Google Cloud Console |
|
|
424
|
+
| Android `hasPlayServices` is false | Use an emulator image with Google Play Services |
|
|
425
|
+
| Apple email/name missing | Apple only returns these fields on first authorization |
|
|
426
|
+
| Microsoft `invalid_state` | Redirect URI or app resume path is wrong, or an old auth redirect completed late |
|
|
427
|
+
| Microsoft `token_error` | Check tenant, client ID, redirect URI, and requested scopes |
|
|
428
|
+
| Web popup blocked | Call `login()` from a user gesture such as a button press |
|
|
429
|
+
| `operation_in_progress` | A provider flow is already active; wait for it to finish or sign out |
|
|
424
430
|
|
|
425
431
|
## Production Notes
|
|
426
432
|
|
|
@@ -433,13 +439,26 @@ The demo includes:
|
|
|
433
439
|
## Release Checks
|
|
434
440
|
|
|
435
441
|
```sh
|
|
436
|
-
bun run
|
|
437
|
-
|
|
442
|
+
bun run publish-package:dry-run
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
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`.
|
|
446
|
+
|
|
447
|
+
For faster local iteration before the full release dry run:
|
|
448
|
+
|
|
449
|
+
```sh
|
|
438
450
|
bun run check
|
|
439
451
|
bun run test:cpp
|
|
440
|
-
bun
|
|
441
|
-
bun
|
|
442
|
-
|
|
452
|
+
bun run --cwd packages/react-native-nitro-auth test:coverage -- --runInBand
|
|
453
|
+
bun run --cwd packages/react-native-nitro-auth test:cpp:coverage
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Before shipping provider or native config changes, also verify the example app:
|
|
457
|
+
|
|
458
|
+
```sh
|
|
459
|
+
bun run example:prebuild
|
|
460
|
+
bun run example:android
|
|
461
|
+
bun run example:ios
|
|
443
462
|
```
|
|
444
463
|
|
|
445
464
|
## 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
package/ios/AuthAdapter.swift
CHANGED
|
@@ -10,6 +10,7 @@ public class AuthAdapter: NSObject {
|
|
|
10
10
|
private static var inMemoryMicrosoftRefreshToken: String?
|
|
11
11
|
private static var inMemoryMicrosoftScopes: [String] = defaultMicrosoftScopes
|
|
12
12
|
private static var inMemoryGoogleServerAuthCode: String?
|
|
13
|
+
private static var activeMicrosoftWebAuthSession: ASWebAuthenticationSession?
|
|
13
14
|
private static let tokenStoreLock = NSLock()
|
|
14
15
|
|
|
15
16
|
@objc
|
|
@@ -135,22 +136,32 @@ public class AuthAdapter: NSObject {
|
|
|
135
136
|
let callbackScheme = "msauth.\(bundleId)"
|
|
136
137
|
|
|
137
138
|
DispatchQueue.main.async {
|
|
139
|
+
guard self.activeMicrosoftWebAuthSession == nil else {
|
|
140
|
+
completion(nil, "operation_in_progress")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let completeAndClearSession = { (data: NSDictionary?, error: String?) in
|
|
145
|
+
self.activeMicrosoftWebAuthSession = nil
|
|
146
|
+
completion(data, error)
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
|
|
139
150
|
if let error = error {
|
|
140
151
|
let nsError = error as NSError
|
|
141
152
|
if nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
|
|
142
|
-
|
|
153
|
+
completeAndClearSession(nil, "cancelled")
|
|
143
154
|
} else if nsError.domain.lowercased().contains("network") || nsError.code == NSURLErrorNotConnectedToInternet {
|
|
144
|
-
|
|
155
|
+
completeAndClearSession(nil, "network_error")
|
|
145
156
|
} else {
|
|
146
|
-
|
|
157
|
+
completeAndClearSession(nil, "unknown")
|
|
147
158
|
}
|
|
148
159
|
return
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
guard let callbackURL = callbackURL,
|
|
152
163
|
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else {
|
|
153
|
-
|
|
164
|
+
completeAndClearSession(nil, "unknown")
|
|
154
165
|
return
|
|
155
166
|
}
|
|
156
167
|
|
|
@@ -163,20 +174,21 @@ public class AuthAdapter: NSObject {
|
|
|
163
174
|
// OAuth error codes are already structured (e.g. "access_denied").
|
|
164
175
|
// Map well-known ones; fall back to "unknown".
|
|
165
176
|
let mapped = mapOAuthError(errorCode)
|
|
166
|
-
|
|
177
|
+
completeAndClearSession(nil, mapped)
|
|
167
178
|
return
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
guard let returnedState = params["state"], returnedState == state else {
|
|
171
|
-
|
|
182
|
+
completeAndClearSession(nil, "invalid_state")
|
|
172
183
|
return
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
guard let code = params["code"] else {
|
|
176
|
-
|
|
187
|
+
completeAndClearSession(nil, "unknown")
|
|
177
188
|
return
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
self.activeMicrosoftWebAuthSession = nil
|
|
180
192
|
exchangeCodeForTokens(
|
|
181
193
|
code: code,
|
|
182
194
|
codeVerifier: codeVerifier,
|
|
@@ -191,14 +203,17 @@ public class AuthAdapter: NSObject {
|
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
guard let window = activeWindow() else {
|
|
194
|
-
|
|
206
|
+
completeAndClearSession(nil, "no_window")
|
|
195
207
|
return
|
|
196
208
|
}
|
|
197
209
|
let contextProvider = WebAuthContextProvider(anchor: window)
|
|
198
210
|
session.presentationContextProvider = contextProvider
|
|
199
211
|
objc_setAssociatedObject(session, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
200
212
|
session.prefersEphemeralWebBrowserSession = false
|
|
201
|
-
session
|
|
213
|
+
self.activeMicrosoftWebAuthSession = session
|
|
214
|
+
if !session.start() {
|
|
215
|
+
completeAndClearSession(nil, "unknown")
|
|
216
|
+
}
|
|
202
217
|
}
|
|
203
218
|
}
|
|
204
219
|
|
|
@@ -685,6 +700,10 @@ public class AuthAdapter: NSObject {
|
|
|
685
700
|
@objc
|
|
686
701
|
public static func logout() {
|
|
687
702
|
GIDSignIn.sharedInstance.signOut()
|
|
703
|
+
DispatchQueue.main.async {
|
|
704
|
+
self.activeMicrosoftWebAuthSession?.cancel()
|
|
705
|
+
self.activeMicrosoftWebAuthSession = nil
|
|
706
|
+
}
|
|
688
707
|
tokenStoreLock.lock()
|
|
689
708
|
inMemoryMicrosoftRefreshToken = nil
|
|
690
709
|
inMemoryMicrosoftScopes = defaultMicrosoftScopes
|