react-native-nitro-auth 0.5.11 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.watchmanconfig +6 -0
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +88 -13
  4. package/android/build.gradle +4 -4
  5. package/android/gradle.properties +2 -2
  6. package/android/src/main/cpp/PlatformAuth+Android.cpp +87 -4
  7. package/android/src/main/java/com/auth/AuthAdapter.kt +86 -49
  8. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +12 -2
  9. package/cpp/HybridAuth.cpp +168 -18
  10. package/cpp/HybridAuth.hpp +7 -0
  11. package/cpp/PlatformAuth.hpp +1 -0
  12. package/ios/AuthAdapter.swift +101 -27
  13. package/ios/PlatformAuth+iOS.mm +37 -1
  14. package/lib/commonjs/Auth.web.js +74 -21
  15. package/lib/commonjs/Auth.web.js.map +1 -1
  16. package/lib/commonjs/create-auth-service.js +10 -0
  17. package/lib/commonjs/create-auth-service.js.map +1 -1
  18. package/lib/commonjs/index.js +12 -0
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +12 -0
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/provider-options.js +6 -0
  23. package/lib/commonjs/provider-options.js.map +1 -0
  24. package/lib/commonjs/service.js.map +1 -1
  25. package/lib/commonjs/service.web.js.map +1 -1
  26. package/lib/commonjs/ui/social-button.web.js +3 -3
  27. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  28. package/lib/commonjs/use-auth.js +21 -1
  29. package/lib/commonjs/use-auth.js.map +1 -1
  30. package/lib/commonjs/utils/auth-error.js +11 -0
  31. package/lib/commonjs/utils/auth-error.js.map +1 -1
  32. package/lib/module/Auth.web.js +74 -21
  33. package/lib/module/Auth.web.js.map +1 -1
  34. package/lib/module/create-auth-service.js +10 -0
  35. package/lib/module/create-auth-service.js.map +1 -1
  36. package/lib/module/global.d.js.map +1 -1
  37. package/lib/module/index.js +1 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/index.web.js +1 -0
  40. package/lib/module/index.web.js.map +1 -1
  41. package/lib/module/provider-options.js +4 -0
  42. package/lib/module/provider-options.js.map +1 -0
  43. package/lib/module/service.js.map +1 -1
  44. package/lib/module/service.web.js.map +1 -1
  45. package/lib/module/ui/social-button.web.js +3 -3
  46. package/lib/module/ui/social-button.web.js.map +1 -1
  47. package/lib/module/use-auth.js +21 -1
  48. package/lib/module/use-auth.js.map +1 -1
  49. package/lib/module/utils/auth-error.js +11 -0
  50. package/lib/module/utils/auth-error.js.map +1 -1
  51. package/lib/typescript/commonjs/Auth.nitro.d.ts +11 -0
  52. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/Auth.web.d.ts +4 -0
  54. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/create-auth-service.d.ts +2 -1
  56. package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/index.d.ts +1 -0
  58. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  60. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/provider-options.d.ts +23 -0
  62. package/lib/typescript/commonjs/provider-options.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/service.d.ts +2 -2
  64. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/service.web.d.ts +2 -2
  66. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/use-auth.d.ts +4 -2
  68. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -1
  70. package/lib/typescript/module/Auth.nitro.d.ts +11 -0
  71. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  72. package/lib/typescript/module/Auth.web.d.ts +4 -0
  73. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  74. package/lib/typescript/module/create-auth-service.d.ts +2 -1
  75. package/lib/typescript/module/create-auth-service.d.ts.map +1 -1
  76. package/lib/typescript/module/index.d.ts +1 -0
  77. package/lib/typescript/module/index.d.ts.map +1 -1
  78. package/lib/typescript/module/index.web.d.ts +1 -0
  79. package/lib/typescript/module/index.web.d.ts.map +1 -1
  80. package/lib/typescript/module/provider-options.d.ts +23 -0
  81. package/lib/typescript/module/provider-options.d.ts.map +1 -0
  82. package/lib/typescript/module/service.d.ts +2 -2
  83. package/lib/typescript/module/service.d.ts.map +1 -1
  84. package/lib/typescript/module/service.web.d.ts +2 -2
  85. package/lib/typescript/module/service.web.d.ts.map +1 -1
  86. package/lib/typescript/module/use-auth.d.ts +4 -2
  87. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  88. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -1
  89. package/nitrogen/generated/shared/c++/AuthUser.hpp +17 -1
  90. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +1 -0
  91. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +1 -0
  92. package/nitrogen/generated/shared/c++/LoginOptions.hpp +25 -1
  93. package/package.json +9 -3
  94. package/src/Auth.nitro.ts +11 -0
  95. package/src/Auth.web.ts +99 -16
  96. package/src/create-auth-service.ts +19 -9
  97. package/src/global.d.ts +2 -1
  98. package/src/index.ts +1 -0
  99. package/src/index.web.ts +1 -0
  100. package/src/provider-options.ts +62 -0
  101. package/src/service.ts +2 -1
  102. package/src/service.web.ts +2 -2
  103. package/src/ui/social-button.web.tsx +3 -3
  104. package/src/use-auth.ts +22 -8
  105. package/src/utils/auth-error.ts +14 -0
@@ -3,9 +3,14 @@
3
3
  #include <algorithm>
4
4
  #include <chrono>
5
5
  #include <exception>
6
+ #include <iostream>
6
7
  #include <stdexcept>
7
8
  #include <unordered_set>
8
9
 
10
+ #if defined(__ANDROID__)
11
+ #include <android/log.h>
12
+ #endif
13
+
9
14
  namespace margelo::nitro::NitroAuth {
10
15
 
11
16
  namespace {
@@ -20,6 +25,32 @@ void rejectIfPending(const std::shared_ptr<Promise<AuthTokens>>& promise, const
20
25
  }
21
26
  }
22
27
 
28
+ void rejectIfPending(const std::shared_ptr<Promise<void>>& promise, const char* message) {
29
+ if (promise && promise->isPending()) {
30
+ promise->reject(makeAuthError(message));
31
+ }
32
+ }
33
+
34
+ void resolveIfPending(const std::shared_ptr<Promise<void>>& promise) {
35
+ if (promise && promise->isPending()) {
36
+ promise->resolve();
37
+ }
38
+ }
39
+
40
+ void rejectPendingSessionPromises(const std::vector<std::shared_ptr<Promise<void>>>& promises, const char* message) {
41
+ for (const auto& promise : promises) {
42
+ rejectIfPending(promise, message);
43
+ }
44
+ }
45
+
46
+ void writeNativeLog(const std::string& message) {
47
+ #if defined(__ANDROID__)
48
+ __android_log_print(ANDROID_LOG_DEBUG, "NitroAuth", "%s", message.c_str());
49
+ #else
50
+ std::clog << "[NitroAuth] " << message << std::endl;
51
+ #endif
52
+ }
53
+
23
54
  void mergeGrantedScopes(std::vector<std::string>& grantedScopes, const std::vector<std::string>& scopes) {
24
55
  std::unordered_set<std::string> knownScopes(grantedScopes.begin(), grantedScopes.end());
25
56
  grantedScopes.reserve(grantedScopes.size() + scopes.size());
@@ -130,25 +161,65 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::advanceSessionGenerationLocked(
130
161
  return refreshInFlight;
131
162
  }
132
163
 
164
+ void HybridAuth::trackSessionPromiseLocked(const std::shared_ptr<Promise<void>>& promise) {
165
+ _sessionPromises.erase(
166
+ std::remove_if(_sessionPromises.begin(), _sessionPromises.end(), [](const std::weak_ptr<Promise<void>>& weak) {
167
+ auto promise = weak.lock();
168
+ return !promise || !promise->isPending();
169
+ }),
170
+ _sessionPromises.end()
171
+ );
172
+ _sessionPromises.push_back(promise);
173
+ }
174
+
175
+ std::vector<std::shared_ptr<Promise<void>>> HybridAuth::takePendingSessionPromisesLocked() {
176
+ std::vector<std::shared_ptr<Promise<void>>> pending;
177
+ for (const auto& weak : _sessionPromises) {
178
+ auto promise = weak.lock();
179
+ if (promise && promise->isPending()) {
180
+ pending.push_back(promise);
181
+ }
182
+ }
183
+ _sessionPromises.clear();
184
+ return pending;
185
+ }
186
+
187
+ void HybridAuth::log(const std::string& message) {
188
+ bool enabled;
189
+ {
190
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
191
+ enabled = _loggingEnabled;
192
+ }
193
+ if (enabled) {
194
+ writeNativeLog(message);
195
+ }
196
+ }
197
+
133
198
  void HybridAuth::logout() {
199
+ log("logout");
134
200
  std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
201
+ std::vector<std::shared_ptr<Promise<void>>> sessionPromises;
135
202
  {
136
203
  std::lock_guard<std::recursive_mutex> lock(_mutex);
204
+ sessionPromises = takePendingSessionPromisesLocked();
137
205
  refreshInFlight = advanceSessionGenerationLocked();
138
206
  _currentUser = std::nullopt;
139
207
  _grantedScopes.clear();
140
208
  }
141
209
  rejectIfPending(refreshInFlight, "not_signed_in");
210
+ rejectPendingSessionPromises(sessionPromises, "cancelled");
142
211
  PlatformAuth::logout();
143
212
  notifyAuthStateChanged();
144
213
  }
145
214
 
146
215
  std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
216
+ log("silentRestore start");
147
217
  auto promise = Promise<void>::create();
148
218
  uint64_t generation;
149
219
  {
150
220
  std::lock_guard<std::recursive_mutex> lock(_mutex);
151
221
  generation = _sessionGeneration;
222
+ trackSessionPromiseLocked(promise);
152
223
  }
153
224
  auto silentPromise = PlatformAuth::silentRestore();
154
225
  auto self = shared_from_this();
@@ -162,7 +233,8 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
162
233
  {
163
234
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
164
235
  if (auth->_sessionGeneration != generation) {
165
- promise->resolve();
236
+ auth->log("silentRestore cancelled");
237
+ resolveIfPending(promise);
166
238
  return;
167
239
  }
168
240
  refreshInFlight = auth->advanceSessionGenerationLocked();
@@ -178,42 +250,51 @@ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
178
250
  }
179
251
  }
180
252
  rejectIfPending(refreshInFlight, "cancelled");
181
- // Always resolve - no session is not an error, just means user is logged out
182
253
  auth->notifyAuthStateChanged();
183
- promise->resolve();
254
+ auth->log(user ? "silentRestore resolved with session" : "silentRestore resolved without session");
255
+ resolveIfPending(promise);
184
256
  });
185
257
 
186
- silentPromise->addOnRejectedListener([promise](const std::exception_ptr&) {
187
- // Silently ignore errors during restore - user will be logged out
188
- promise->resolve();
258
+ silentPromise->addOnRejectedListener([self, promise](const std::exception_ptr&) {
259
+ auto* auth = dynamic_cast<HybridAuth*>(self.get());
260
+ if (auth) {
261
+ auth->log("silentRestore rejected");
262
+ }
263
+ resolveIfPending(promise);
189
264
  });
190
265
  return promise;
191
266
  }
192
267
 
193
268
  std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
269
+ log("login start");
194
270
  auto promise = Promise<void>::create();
195
271
  uint64_t generation;
196
272
  std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
273
+ std::vector<std::shared_ptr<Promise<void>>> sessionPromises;
197
274
  {
198
275
  std::lock_guard<std::recursive_mutex> lock(_mutex);
276
+ sessionPromises = takePendingSessionPromisesLocked();
199
277
  refreshInFlight = advanceSessionGenerationLocked();
200
278
  generation = _sessionGeneration;
279
+ trackSessionPromiseLocked(promise);
201
280
  }
202
281
  rejectIfPending(refreshInFlight, "cancelled");
282
+ rejectPendingSessionPromises(sessionPromises, "cancelled");
203
283
 
204
284
  auto self = shared_from_this();
205
285
  auto loginPromise = PlatformAuth::login(provider, options);
206
286
  loginPromise->addOnResolvedListener([self, promise, options, generation](const AuthUser& user) {
207
287
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
208
288
  if (!auth) {
209
- promise->reject(makeAuthError("internal_error"));
289
+ rejectIfPending(promise, "internal_error");
210
290
  return;
211
291
  }
212
292
  std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
213
293
  {
214
294
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
215
295
  if (auth->_sessionGeneration != generation) {
216
- promise->reject(makeAuthError("cancelled"));
296
+ auth->log("login cancelled");
297
+ rejectIfPending(promise, "cancelled");
217
298
  return;
218
299
  }
219
300
  refreshInFlight = auth->advanceSessionGenerationLocked();
@@ -233,34 +314,44 @@ std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const st
233
314
  }
234
315
  rejectIfPending(refreshInFlight, "cancelled");
235
316
  auth->notifyAuthStateChanged();
236
- promise->resolve();
317
+ auth->log("login resolved");
318
+ resolveIfPending(promise);
237
319
  });
238
320
 
239
- loginPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
240
- promise->reject(error);
321
+ loginPromise->addOnRejectedListener([self, promise](const std::exception_ptr& error) {
322
+ auto* auth = dynamic_cast<HybridAuth*>(self.get());
323
+ if (auth) {
324
+ auth->log("login rejected");
325
+ }
326
+ if (promise->isPending()) {
327
+ promise->reject(error);
328
+ }
241
329
  });
242
330
  return promise;
243
331
  }
244
332
 
245
333
  std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::string>& scopes) {
334
+ log("requestScopes start");
246
335
  auto promise = Promise<void>::create();
247
336
  uint64_t generation;
248
337
  {
249
338
  std::lock_guard<std::recursive_mutex> lock(_mutex);
250
339
  generation = _sessionGeneration;
340
+ trackSessionPromiseLocked(promise);
251
341
  }
252
342
  auto self = shared_from_this();
253
343
  auto requestPromise = PlatformAuth::requestScopes(scopes);
254
344
  requestPromise->addOnResolvedListener([self, promise, scopes, generation](const AuthUser& user) {
255
345
  auto* auth = dynamic_cast<HybridAuth*>(self.get());
256
346
  if (!auth) {
257
- promise->reject(makeAuthError("internal_error"));
347
+ rejectIfPending(promise, "internal_error");
258
348
  return;
259
349
  }
260
350
  {
261
351
  std::lock_guard<std::recursive_mutex> lock(auth->_mutex);
262
352
  if (auth->_sessionGeneration != generation) {
263
- promise->reject(makeAuthError("cancelled"));
353
+ auth->log("requestScopes cancelled");
354
+ rejectIfPending(promise, "cancelled");
264
355
  return;
265
356
  }
266
357
  auth->_currentUser = user;
@@ -268,16 +359,24 @@ std::shared_ptr<Promise<void>> HybridAuth::requestScopes(const std::vector<std::
268
359
  if (auth->_currentUser) auth->_currentUser->scopes = auth->_grantedScopes;
269
360
  }
270
361
  auth->notifyAuthStateChanged();
271
- promise->resolve();
362
+ auth->log("requestScopes resolved");
363
+ resolveIfPending(promise);
272
364
  });
273
365
 
274
- requestPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
275
- promise->reject(error);
366
+ requestPromise->addOnRejectedListener([self, promise](const std::exception_ptr& error) {
367
+ auto* auth = dynamic_cast<HybridAuth*>(self.get());
368
+ if (auth) {
369
+ auth->log("requestScopes rejected");
370
+ }
371
+ if (promise->isPending()) {
372
+ promise->reject(error);
373
+ }
276
374
  });
277
375
  return promise;
278
376
  }
279
377
 
280
378
  std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::string>& scopes) {
379
+ log("revokeScopes");
281
380
  auto promise = Promise<void>::create();
282
381
  {
283
382
  std::lock_guard<std::recursive_mutex> lock(_mutex);
@@ -291,7 +390,48 @@ std::shared_ptr<Promise<void>> HybridAuth::revokeScopes(const std::vector<std::s
291
390
  return promise;
292
391
  }
293
392
 
393
+ std::shared_ptr<Promise<void>> HybridAuth::revokeAccess() {
394
+ log("revokeAccess start");
395
+ auto promise = Promise<void>::create();
396
+ std::shared_ptr<Promise<AuthTokens>> refreshInFlight;
397
+ std::vector<std::shared_ptr<Promise<void>>> sessionPromises;
398
+ {
399
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
400
+ sessionPromises = takePendingSessionPromisesLocked();
401
+ refreshInFlight = advanceSessionGenerationLocked();
402
+ trackSessionPromiseLocked(promise);
403
+ _currentUser = std::nullopt;
404
+ _grantedScopes.clear();
405
+ }
406
+ rejectIfPending(refreshInFlight, "cancelled");
407
+ rejectPendingSessionPromises(sessionPromises, "cancelled");
408
+
409
+ auto platformPromise = PlatformAuth::revokeAccess();
410
+ auto self = shared_from_this();
411
+ platformPromise->addOnResolvedListener([self, promise]() {
412
+ auto* auth = dynamic_cast<HybridAuth*>(self.get());
413
+ if (!auth) {
414
+ rejectIfPending(promise, "internal_error");
415
+ return;
416
+ }
417
+ auth->notifyAuthStateChanged();
418
+ auth->log("revokeAccess resolved");
419
+ resolveIfPending(promise);
420
+ });
421
+ platformPromise->addOnRejectedListener([self, promise](const std::exception_ptr& error) {
422
+ auto* auth = dynamic_cast<HybridAuth*>(self.get());
423
+ if (auth) {
424
+ auth->log("revokeAccess rejected");
425
+ }
426
+ if (promise->isPending()) {
427
+ promise->reject(error);
428
+ }
429
+ });
430
+ return promise;
431
+ }
432
+
294
433
  std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken() {
434
+ log("getAccessToken");
295
435
  auto promise = Promise<std::optional<std::string>>::create();
296
436
  bool needsRefresh = false;
297
437
  std::optional<std::string> cachedAccessToken;
@@ -326,6 +466,7 @@ std::shared_ptr<Promise<std::optional<std::string>>> HybridAuth::getAccessToken(
326
466
  }
327
467
 
328
468
  std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
469
+ log("refreshToken start");
329
470
  std::shared_ptr<Promise<AuthTokens>> promise;
330
471
  uint64_t generation;
331
472
  {
@@ -380,6 +521,7 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
380
521
  }
381
522
  auth->notifyTokensRefreshed(tokens);
382
523
  auth->notifyAuthStateChanged();
524
+ auth->log("refreshToken resolved");
383
525
  promise->resolve(tokens);
384
526
  });
385
527
 
@@ -402,16 +544,24 @@ std::shared_ptr<Promise<AuthTokens>> HybridAuth::refreshToken() {
402
544
  }
403
545
  }
404
546
  if (isStale) {
547
+ auth->log("refreshToken cancelled");
405
548
  rejectIfPending(promise, "cancelled");
406
549
  return;
407
550
  }
551
+ auth->log("refreshToken rejected");
408
552
  promise->reject(error);
409
553
  });
410
554
  return promise;
411
555
  }
412
556
 
413
- void HybridAuth::setLoggingEnabled(bool /* enabled */) {
414
- // Reserved for future use — logging not yet implemented in C++ layer
557
+ void HybridAuth::setLoggingEnabled(bool enabled) {
558
+ {
559
+ std::lock_guard<std::recursive_mutex> lock(_mutex);
560
+ _loggingEnabled = enabled;
561
+ }
562
+ if (enabled) {
563
+ writeNativeLog("native logging enabled");
564
+ }
415
565
  }
416
566
 
417
567
  void HybridAuth::notifyTokensRefreshed(const AuthTokens& tokens) {
@@ -10,6 +10,7 @@
10
10
  #include <memory>
11
11
  #include <string>
12
12
  #include <map>
13
+ #include <vector>
13
14
 
14
15
  namespace margelo::nitro::NitroAuth {
15
16
 
@@ -24,6 +25,7 @@ public:
24
25
  std::shared_ptr<Promise<void>> login(AuthProvider provider, const std::optional<LoginOptions>& options) override;
25
26
  std::shared_ptr<Promise<void>> requestScopes(const std::vector<std::string>& scopes) override;
26
27
  std::shared_ptr<Promise<void>> revokeScopes(const std::vector<std::string>& scopes) override;
28
+ std::shared_ptr<Promise<void>> revokeAccess() override;
27
29
  std::shared_ptr<Promise<std::optional<std::string>>> getAccessToken() override;
28
30
  std::shared_ptr<Promise<AuthTokens>> refreshToken() override;
29
31
 
@@ -39,6 +41,9 @@ private:
39
41
  void notifyAuthStateChanged();
40
42
  void notifyTokensRefreshed(const AuthTokens& tokens);
41
43
  std::shared_ptr<Promise<AuthTokens>> advanceSessionGenerationLocked();
44
+ void trackSessionPromiseLocked(const std::shared_ptr<Promise<void>>& promise);
45
+ std::vector<std::shared_ptr<Promise<void>>> takePendingSessionPromisesLocked();
46
+ void log(const std::string& message);
42
47
 
43
48
  private:
44
49
  std::optional<AuthUser> _currentUser;
@@ -49,7 +54,9 @@ private:
49
54
  std::map<uint64_t, std::function<void(const AuthTokens&)>> _tokenListeners;
50
55
  uint64_t _nextTokenListenerId = 0;
51
56
  std::shared_ptr<Promise<AuthTokens>> _refreshInFlight;
57
+ std::vector<std::weak_ptr<Promise<void>>> _sessionPromises;
52
58
  uint64_t _sessionGeneration = 0;
59
+ bool _loggingEnabled = false;
53
60
 
54
61
  // recursive_mutex: listeners resolved inside a lock scope may re-enter Auth methods
55
62
  // that also acquire _mutex, causing deadlock with a non-recursive mutex.
@@ -21,6 +21,7 @@ public:
21
21
  static std::shared_ptr<Promise<std::optional<AuthUser>>> silentRestore();
22
22
  static bool hasPlayServices();
23
23
  static void logout();
24
+ static std::shared_ptr<Promise<void>> revokeAccess();
24
25
  };
25
26
 
26
27
  } // namespace margelo::nitro::NitroAuth
@@ -12,14 +12,42 @@ public class AuthAdapter: NSObject {
12
12
  private static var inMemoryGoogleServerAuthCode: String?
13
13
  private static var activeMicrosoftWebAuthSession: ASWebAuthenticationSession?
14
14
  private static let tokenStoreLock = NSLock()
15
+ private static let interactiveAuthLock = NSLock()
16
+ private static var interactiveAuthInProgress = false
17
+
18
+ private static func beginInteractiveAuth() -> Bool {
19
+ interactiveAuthLock.lock()
20
+ defer { interactiveAuthLock.unlock() }
21
+ if interactiveAuthInProgress {
22
+ return false
23
+ }
24
+ interactiveAuthInProgress = true
25
+ return true
26
+ }
27
+
28
+ private static func finishInteractiveAuth() {
29
+ interactiveAuthLock.lock()
30
+ interactiveAuthInProgress = false
31
+ interactiveAuthLock.unlock()
32
+ }
33
+
34
+ private static func completeInteractiveAuth(_ completion: @escaping (NSDictionary?, String?) -> Void) -> (NSDictionary?, String?) -> Void {
35
+ return { data, error in
36
+ finishInteractiveAuth()
37
+ completion(data, error)
38
+ }
39
+ }
15
40
 
16
41
  @objc
17
- public static func login(provider: String, scopes: [String], loginHint: String?, useSheet: Bool, forceAccountPicker: Bool = false, tenant: String? = nil, prompt: String? = nil, completion: @escaping (NSDictionary?, String?) -> Void) {
18
- // useSheet is accepted for API compatibility with Android but has no effect on iOS.
19
- // Google Sign-In SDK controls its own presentation style.
42
+ public static func login(provider: String, scopes: [String], loginHint: String?, nonce: String?, useSheet: Bool, forceAccountPicker: Bool = false, tenant: String? = nil, prompt: String? = nil, hostedDomain: String? = nil, openIDRealm: String? = nil, completion: @escaping (NSDictionary?, String?) -> Void) {
20
43
  if provider == "google" {
44
+ guard beginInteractiveAuth() else {
45
+ completion(nil, "operation_in_progress")
46
+ return
47
+ }
48
+ let complete = completeInteractiveAuth(completion)
21
49
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String, !clientId.isEmpty else {
22
- completion(nil, "configuration_error")
50
+ complete(nil, "configuration_error")
23
51
  return
24
52
  }
25
53
 
@@ -27,23 +55,24 @@ public class AuthAdapter: NSObject {
27
55
 
28
56
  DispatchQueue.main.async {
29
57
  guard let rootVC = presentingViewController() else {
30
- completion(nil, "no_window")
58
+ complete(nil, "configuration_error")
31
59
  return
32
60
  }
33
61
 
34
- let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId)
62
+ let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId, hostedDomain: hostedDomain, openIDRealm: openIDRealm)
35
63
  GIDSignIn.sharedInstance.configuration = config
36
64
 
37
65
  let additionalScopes = scopes.isEmpty ? nil : scopes
38
- let effectiveHint = forceAccountPicker ? nil : loginHint
66
+ let shouldForceAccountPicker = forceAccountPicker || useSheet
67
+ let effectiveHint = shouldForceAccountPicker ? nil : loginHint
39
68
 
40
69
  let performSignIn = {
41
- GIDSignIn.sharedInstance.signIn(withPresenting: rootVC, hint: effectiveHint, additionalScopes: additionalScopes) { result, error in
42
- self.handleGoogleResult(result, error: error, completion: completion)
70
+ GIDSignIn.sharedInstance.signIn(withPresenting: rootVC, hint: effectiveHint, additionalScopes: additionalScopes, nonce: nonce) { result, error in
71
+ self.handleGoogleResult(result, error: error, completion: complete)
43
72
  }
44
73
  }
45
74
 
46
- if forceAccountPicker {
75
+ if shouldForceAccountPicker {
47
76
  GIDSignIn.sharedInstance.disconnect { _ in
48
77
  performSignIn()
49
78
  }
@@ -52,18 +81,34 @@ public class AuthAdapter: NSObject {
52
81
  }
53
82
  }
54
83
  } else if provider == "apple" {
84
+ guard beginInteractiveAuth() else {
85
+ completion(nil, "operation_in_progress")
86
+ return
87
+ }
88
+ let complete = completeInteractiveAuth(completion)
55
89
  let appleIDProvider = ASAuthorizationAppleIDProvider()
56
90
  let request = appleIDProvider.createRequest()
57
- request.requestedScopes = [.fullName, .email]
91
+ request.requestedScopes = scopes.isEmpty
92
+ ? [.fullName, .email]
93
+ : scopes.compactMap { scope in
94
+ switch scope {
95
+ case "fullName", "name": return .fullName
96
+ case "email": return .email
97
+ default: return nil
98
+ }
99
+ }
100
+ if let nonce = nonce {
101
+ request.nonce = nonce
102
+ }
58
103
 
59
104
  let controller = ASAuthorizationController(authorizationRequests: [request])
60
- let delegate = AppleSignInDelegate(completion: completion)
105
+ let delegate = AppleSignInDelegate(completion: complete)
61
106
  controller.delegate = delegate
62
107
  objc_setAssociatedObject(controller, &delegateHandle, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
63
108
 
64
109
  DispatchQueue.main.async {
65
110
  guard let window = activeWindow() else {
66
- completion(nil, "no_window")
111
+ complete(nil, "configuration_error")
67
112
  return
68
113
  }
69
114
  let contextProvider = AppleSignInContextProvider(anchor: window)
@@ -83,6 +128,11 @@ public class AuthAdapter: NSObject {
83
128
  completion(nil, "configuration_error")
84
129
  return
85
130
  }
131
+ guard beginInteractiveAuth() else {
132
+ completion(nil, "operation_in_progress")
133
+ return
134
+ }
135
+ let complete = completeInteractiveAuth(completion)
86
136
 
87
137
  let effectiveTenant = tenant ?? Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
88
138
  let bundleId = Bundle.main.bundleIdentifier ?? ""
@@ -91,11 +141,11 @@ public class AuthAdapter: NSObject {
91
141
  let effectivePrompt = prompt ?? "select_account"
92
142
 
93
143
  guard let codeVerifier = generateCodeVerifier() else {
94
- completion(nil, "configuration_error")
144
+ complete(nil, "configuration_error")
95
145
  return
96
146
  }
97
147
  guard let codeChallenge = generateCodeChallenge(codeVerifier) else {
98
- completion(nil, "configuration_error")
148
+ complete(nil, "configuration_error")
99
149
  return
100
150
  }
101
151
  let state = UUID().uuidString
@@ -103,12 +153,12 @@ public class AuthAdapter: NSObject {
103
153
 
104
154
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
105
155
  guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: effectiveTenant, b2cDomain: b2cDomain) else {
106
- completion(nil, "configuration_error")
156
+ complete(nil, "configuration_error")
107
157
  return
108
158
  }
109
159
 
110
160
  guard var urlComponents = URLComponents(string: "\(authBaseUrl)oauth2/v2.0/authorize") else {
111
- completion(nil, "configuration_error")
161
+ complete(nil, "configuration_error")
112
162
  return
113
163
  }
114
164
  urlComponents.queryItems = [
@@ -129,7 +179,7 @@ public class AuthAdapter: NSObject {
129
179
  }
130
180
 
131
181
  guard let authUrl = urlComponents.url else {
132
- completion(nil, "configuration_error")
182
+ complete(nil, "configuration_error")
133
183
  return
134
184
  }
135
185
 
@@ -137,13 +187,13 @@ public class AuthAdapter: NSObject {
137
187
 
138
188
  DispatchQueue.main.async {
139
189
  guard self.activeMicrosoftWebAuthSession == nil else {
140
- completion(nil, "operation_in_progress")
190
+ complete(nil, "operation_in_progress")
141
191
  return
142
192
  }
143
193
 
144
194
  let completeAndClearSession = { (data: NSDictionary?, error: String?) in
145
195
  self.activeMicrosoftWebAuthSession = nil
146
- completion(data, error)
196
+ complete(data, error)
147
197
  }
148
198
 
149
199
  let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
@@ -184,7 +234,7 @@ public class AuthAdapter: NSObject {
184
234
  }
185
235
 
186
236
  guard let code = params["code"] else {
187
- completeAndClearSession(nil, "unknown")
237
+ completeAndClearSession(nil, "token_error")
188
238
  return
189
239
  }
190
240
 
@@ -198,12 +248,12 @@ public class AuthAdapter: NSObject {
198
248
  b2cDomain: b2cDomain,
199
249
  expectedNonce: nonce,
200
250
  scopes: effectiveScopes,
201
- completion: completion
251
+ completion: complete
202
252
  )
203
253
  }
204
254
 
205
255
  guard let window = activeWindow() else {
206
- completeAndClearSession(nil, "no_window")
256
+ completeAndClearSession(nil, "configuration_error")
207
257
  return
208
258
  }
209
259
  let contextProvider = WebAuthContextProvider(anchor: window)
@@ -391,6 +441,8 @@ public class AuthAdapter: NSObject {
391
441
  "idToken": user.idToken?.tokenString ?? "",
392
442
  "accessToken": user.accessToken.tokenString,
393
443
  "serverAuthCode": serverAuthCode,
444
+ "userId": user.userID ?? "",
445
+ "hostedDomain": user.configuration.hostedDomain ?? "",
394
446
  "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000,
395
447
  "underlyingError": ""
396
448
  ]
@@ -440,7 +492,7 @@ public class AuthAdapter: NSObject {
440
492
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
441
493
  DispatchQueue.main.async {
442
494
  guard let rootVC = presentingViewController() else {
443
- completion(nil, "no_window")
495
+ completion(nil, "configuration_error")
444
496
  return
445
497
  }
446
498
  currentUser.addScopes(scopes, presenting: rootVC) { result, error in
@@ -502,9 +554,11 @@ public class AuthAdapter: NSObject {
502
554
  "name": user.profile?.name ?? "",
503
555
  "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
504
556
  "idToken": user.idToken?.tokenString ?? "",
505
- "accessToken": user.accessToken.tokenString,
506
- "serverAuthCode": cachedServerAuthCode ?? "",
507
- "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
557
+ "accessToken": user.accessToken.tokenString,
558
+ "serverAuthCode": cachedServerAuthCode ?? "",
559
+ "userId": user.userID ?? "",
560
+ "hostedDomain": user.configuration.hostedDomain ?? "",
561
+ "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
508
562
  ]
509
563
  completion(data as NSDictionary)
510
564
  return
@@ -704,6 +758,7 @@ public class AuthAdapter: NSObject {
704
758
  self.activeMicrosoftWebAuthSession?.cancel()
705
759
  self.activeMicrosoftWebAuthSession = nil
706
760
  }
761
+ finishInteractiveAuth()
707
762
  tokenStoreLock.lock()
708
763
  inMemoryMicrosoftRefreshToken = nil
709
764
  inMemoryMicrosoftScopes = defaultMicrosoftScopes
@@ -711,6 +766,23 @@ public class AuthAdapter: NSObject {
711
766
  tokenStoreLock.unlock()
712
767
  }
713
768
 
769
+ @objc
770
+ public static func revokeAccess(completion: @escaping (String?) -> Void) {
771
+ GIDSignIn.sharedInstance.disconnect { error in
772
+ tokenStoreLock.lock()
773
+ inMemoryMicrosoftRefreshToken = nil
774
+ inMemoryMicrosoftScopes = defaultMicrosoftScopes
775
+ inMemoryGoogleServerAuthCode = nil
776
+ tokenStoreLock.unlock()
777
+
778
+ if let error = error {
779
+ completion(mapError(error))
780
+ return
781
+ }
782
+ completion(nil)
783
+ }
784
+ }
785
+
714
786
  private static func activeWindow() -> UIWindow? {
715
787
  let windowScenes = UIApplication.shared.connectedScenes
716
788
  .compactMap { $0 as? UIWindowScene }
@@ -769,6 +841,8 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
769
841
  "email": email ?? "",
770
842
  "name": name,
771
843
  "idToken": idToken ?? "",
844
+ "authorizationCode": appleIDCredential.authorizationCode.flatMap { String(data: $0, encoding: .utf8) } ?? "",
845
+ "userId": appleIDCredential.user,
772
846
  "underlyingError": ""
773
847
  ]
774
848
  completion(data as NSDictionary, nil)