react-native-nitro-auth 0.5.12 → 0.6.1

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 (93) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +132 -32
  3. package/android/build.gradle +4 -4
  4. package/android/gradle.properties +2 -2
  5. package/android/src/main/cpp/PlatformAuth+Android.cpp +87 -4
  6. package/android/src/main/java/com/auth/AuthAdapter.kt +85 -48
  7. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +12 -2
  8. package/cpp/HybridAuth.cpp +168 -18
  9. package/cpp/HybridAuth.hpp +7 -0
  10. package/cpp/PlatformAuth.hpp +1 -0
  11. package/ios/AuthAdapter.swift +101 -24
  12. package/ios/PlatformAuth+iOS.mm +37 -1
  13. package/lib/commonjs/Auth.web.js +74 -21
  14. package/lib/commonjs/Auth.web.js.map +1 -1
  15. package/lib/commonjs/create-auth-service.js +10 -0
  16. package/lib/commonjs/create-auth-service.js.map +1 -1
  17. package/lib/commonjs/index.js +12 -0
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/index.web.js +12 -0
  20. package/lib/commonjs/index.web.js.map +1 -1
  21. package/lib/commonjs/provider-options.js +6 -0
  22. package/lib/commonjs/provider-options.js.map +1 -0
  23. package/lib/commonjs/service.js.map +1 -1
  24. package/lib/commonjs/service.web.js.map +1 -1
  25. package/lib/commonjs/use-auth.js +21 -1
  26. package/lib/commonjs/use-auth.js.map +1 -1
  27. package/lib/module/Auth.web.js +74 -21
  28. package/lib/module/Auth.web.js.map +1 -1
  29. package/lib/module/create-auth-service.js +10 -0
  30. package/lib/module/create-auth-service.js.map +1 -1
  31. package/lib/module/global.d.js.map +1 -1
  32. package/lib/module/index.js +1 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/index.web.js +1 -0
  35. package/lib/module/index.web.js.map +1 -1
  36. package/lib/module/provider-options.js +4 -0
  37. package/lib/module/provider-options.js.map +1 -0
  38. package/lib/module/service.js.map +1 -1
  39. package/lib/module/service.web.js.map +1 -1
  40. package/lib/module/use-auth.js +21 -1
  41. package/lib/module/use-auth.js.map +1 -1
  42. package/lib/typescript/commonjs/Auth.nitro.d.ts +11 -0
  43. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/Auth.web.d.ts +4 -0
  45. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/create-auth-service.d.ts +2 -1
  47. package/lib/typescript/commonjs/create-auth-service.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/index.d.ts +1 -0
  49. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  51. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/provider-options.d.ts +23 -0
  53. package/lib/typescript/commonjs/provider-options.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/service.d.ts +2 -2
  55. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/service.web.d.ts +2 -2
  57. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/use-auth.d.ts +4 -2
  59. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  60. package/lib/typescript/module/Auth.nitro.d.ts +11 -0
  61. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  62. package/lib/typescript/module/Auth.web.d.ts +4 -0
  63. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  64. package/lib/typescript/module/create-auth-service.d.ts +2 -1
  65. package/lib/typescript/module/create-auth-service.d.ts.map +1 -1
  66. package/lib/typescript/module/index.d.ts +1 -0
  67. package/lib/typescript/module/index.d.ts.map +1 -1
  68. package/lib/typescript/module/index.web.d.ts +1 -0
  69. package/lib/typescript/module/index.web.d.ts.map +1 -1
  70. package/lib/typescript/module/provider-options.d.ts +23 -0
  71. package/lib/typescript/module/provider-options.d.ts.map +1 -0
  72. package/lib/typescript/module/service.d.ts +2 -2
  73. package/lib/typescript/module/service.d.ts.map +1 -1
  74. package/lib/typescript/module/service.web.d.ts +2 -2
  75. package/lib/typescript/module/service.web.d.ts.map +1 -1
  76. package/lib/typescript/module/use-auth.d.ts +4 -2
  77. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  78. package/nitrogen/generated/shared/c++/AuthUser.hpp +17 -1
  79. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +1 -0
  80. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +1 -0
  81. package/nitrogen/generated/shared/c++/LoginOptions.hpp +25 -1
  82. package/package.json +7 -7
  83. package/react-native-nitro-auth.podspec +1 -1
  84. package/src/Auth.nitro.ts +11 -0
  85. package/src/Auth.web.ts +99 -16
  86. package/src/create-auth-service.ts +19 -9
  87. package/src/global.d.ts +2 -1
  88. package/src/index.ts +1 -0
  89. package/src/index.web.ts +1 -0
  90. package/src/provider-options.ts +62 -0
  91. package/src/service.ts +2 -1
  92. package/src/service.web.ts +2 -2
  93. package/src/use-auth.ts +22 -8
@@ -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
@@ -11,15 +11,45 @@ public class AuthAdapter: NSObject {
11
11
  private static var inMemoryMicrosoftScopes: [String] = defaultMicrosoftScopes
12
12
  private static var inMemoryGoogleServerAuthCode: String?
13
13
  private static var activeMicrosoftWebAuthSession: ASWebAuthenticationSession?
14
+ private static var activeAppleSignInController: ASAuthorizationController?
14
15
  private static let tokenStoreLock = NSLock()
16
+ private static let interactiveAuthLock = NSLock()
17
+ private static var interactiveAuthInProgress = false
18
+
19
+ private static func beginInteractiveAuth() -> Bool {
20
+ interactiveAuthLock.lock()
21
+ defer { interactiveAuthLock.unlock() }
22
+ if interactiveAuthInProgress {
23
+ return false
24
+ }
25
+ interactiveAuthInProgress = true
26
+ return true
27
+ }
28
+
29
+ private static func finishInteractiveAuth() {
30
+ activeAppleSignInController = nil
31
+ interactiveAuthLock.lock()
32
+ interactiveAuthInProgress = false
33
+ interactiveAuthLock.unlock()
34
+ }
35
+
36
+ private static func completeInteractiveAuth(_ completion: @escaping (NSDictionary?, String?) -> Void) -> (NSDictionary?, String?) -> Void {
37
+ return { data, error in
38
+ finishInteractiveAuth()
39
+ completion(data, error)
40
+ }
41
+ }
15
42
 
16
43
  @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.
44
+ 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
45
  if provider == "google" {
46
+ guard beginInteractiveAuth() else {
47
+ completion(nil, "operation_in_progress")
48
+ return
49
+ }
50
+ let complete = completeInteractiveAuth(completion)
21
51
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String, !clientId.isEmpty else {
22
- completion(nil, "configuration_error")
52
+ complete(nil, "configuration_error")
23
53
  return
24
54
  }
25
55
 
@@ -27,23 +57,24 @@ public class AuthAdapter: NSObject {
27
57
 
28
58
  DispatchQueue.main.async {
29
59
  guard let rootVC = presentingViewController() else {
30
- completion(nil, "configuration_error")
60
+ complete(nil, "configuration_error")
31
61
  return
32
62
  }
33
63
 
34
- let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId)
64
+ let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId, hostedDomain: hostedDomain, openIDRealm: openIDRealm)
35
65
  GIDSignIn.sharedInstance.configuration = config
36
66
 
37
67
  let additionalScopes = scopes.isEmpty ? nil : scopes
38
- let effectiveHint = forceAccountPicker ? nil : loginHint
68
+ let shouldForceAccountPicker = forceAccountPicker || useSheet
69
+ let effectiveHint = shouldForceAccountPicker ? nil : loginHint
39
70
 
40
71
  let performSignIn = {
41
- GIDSignIn.sharedInstance.signIn(withPresenting: rootVC, hint: effectiveHint, additionalScopes: additionalScopes) { result, error in
42
- self.handleGoogleResult(result, error: error, completion: completion)
72
+ GIDSignIn.sharedInstance.signIn(withPresenting: rootVC, hint: effectiveHint, additionalScopes: additionalScopes, nonce: nonce) { result, error in
73
+ self.handleGoogleResult(result, error: error, completion: complete)
43
74
  }
44
75
  }
45
76
 
46
- if forceAccountPicker {
77
+ if shouldForceAccountPicker {
47
78
  GIDSignIn.sharedInstance.disconnect { _ in
48
79
  performSignIn()
49
80
  }
@@ -52,18 +83,35 @@ public class AuthAdapter: NSObject {
52
83
  }
53
84
  }
54
85
  } else if provider == "apple" {
86
+ guard beginInteractiveAuth() else {
87
+ completion(nil, "operation_in_progress")
88
+ return
89
+ }
90
+ let complete = completeInteractiveAuth(completion)
55
91
  let appleIDProvider = ASAuthorizationAppleIDProvider()
56
92
  let request = appleIDProvider.createRequest()
57
- request.requestedScopes = [.fullName, .email]
93
+ request.requestedScopes = scopes.isEmpty
94
+ ? [.fullName, .email]
95
+ : scopes.compactMap { scope in
96
+ switch scope {
97
+ case "fullName", "name": return .fullName
98
+ case "email": return .email
99
+ default: return nil
100
+ }
101
+ }
102
+ if let nonce = nonce {
103
+ request.nonce = nonce
104
+ }
58
105
 
59
106
  let controller = ASAuthorizationController(authorizationRequests: [request])
60
- let delegate = AppleSignInDelegate(completion: completion)
107
+ let delegate = AppleSignInDelegate(completion: complete)
61
108
  controller.delegate = delegate
109
+ activeAppleSignInController = controller
62
110
  objc_setAssociatedObject(controller, &delegateHandle, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
63
111
 
64
112
  DispatchQueue.main.async {
65
113
  guard let window = activeWindow() else {
66
- completion(nil, "configuration_error")
114
+ complete(nil, "configuration_error")
67
115
  return
68
116
  }
69
117
  let contextProvider = AppleSignInContextProvider(anchor: window)
@@ -83,6 +131,11 @@ public class AuthAdapter: NSObject {
83
131
  completion(nil, "configuration_error")
84
132
  return
85
133
  }
134
+ guard beginInteractiveAuth() else {
135
+ completion(nil, "operation_in_progress")
136
+ return
137
+ }
138
+ let complete = completeInteractiveAuth(completion)
86
139
 
87
140
  let effectiveTenant = tenant ?? Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
88
141
  let bundleId = Bundle.main.bundleIdentifier ?? ""
@@ -91,11 +144,11 @@ public class AuthAdapter: NSObject {
91
144
  let effectivePrompt = prompt ?? "select_account"
92
145
 
93
146
  guard let codeVerifier = generateCodeVerifier() else {
94
- completion(nil, "configuration_error")
147
+ complete(nil, "configuration_error")
95
148
  return
96
149
  }
97
150
  guard let codeChallenge = generateCodeChallenge(codeVerifier) else {
98
- completion(nil, "configuration_error")
151
+ complete(nil, "configuration_error")
99
152
  return
100
153
  }
101
154
  let state = UUID().uuidString
@@ -103,12 +156,12 @@ public class AuthAdapter: NSObject {
103
156
 
104
157
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
105
158
  guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: effectiveTenant, b2cDomain: b2cDomain) else {
106
- completion(nil, "configuration_error")
159
+ complete(nil, "configuration_error")
107
160
  return
108
161
  }
109
162
 
110
163
  guard var urlComponents = URLComponents(string: "\(authBaseUrl)oauth2/v2.0/authorize") else {
111
- completion(nil, "configuration_error")
164
+ complete(nil, "configuration_error")
112
165
  return
113
166
  }
114
167
  urlComponents.queryItems = [
@@ -129,7 +182,7 @@ public class AuthAdapter: NSObject {
129
182
  }
130
183
 
131
184
  guard let authUrl = urlComponents.url else {
132
- completion(nil, "configuration_error")
185
+ complete(nil, "configuration_error")
133
186
  return
134
187
  }
135
188
 
@@ -137,13 +190,13 @@ public class AuthAdapter: NSObject {
137
190
 
138
191
  DispatchQueue.main.async {
139
192
  guard self.activeMicrosoftWebAuthSession == nil else {
140
- completion(nil, "operation_in_progress")
193
+ complete(nil, "operation_in_progress")
141
194
  return
142
195
  }
143
196
 
144
197
  let completeAndClearSession = { (data: NSDictionary?, error: String?) in
145
198
  self.activeMicrosoftWebAuthSession = nil
146
- completion(data, error)
199
+ complete(data, error)
147
200
  }
148
201
 
149
202
  let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
@@ -198,7 +251,7 @@ public class AuthAdapter: NSObject {
198
251
  b2cDomain: b2cDomain,
199
252
  expectedNonce: nonce,
200
253
  scopes: effectiveScopes,
201
- completion: completion
254
+ completion: complete
202
255
  )
203
256
  }
204
257
 
@@ -391,6 +444,8 @@ public class AuthAdapter: NSObject {
391
444
  "idToken": user.idToken?.tokenString ?? "",
392
445
  "accessToken": user.accessToken.tokenString,
393
446
  "serverAuthCode": serverAuthCode,
447
+ "userId": user.userID ?? "",
448
+ "hostedDomain": user.configuration.hostedDomain ?? "",
394
449
  "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000,
395
450
  "underlyingError": ""
396
451
  ]
@@ -502,9 +557,11 @@ public class AuthAdapter: NSObject {
502
557
  "name": user.profile?.name ?? "",
503
558
  "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
504
559
  "idToken": user.idToken?.tokenString ?? "",
505
- "accessToken": user.accessToken.tokenString,
506
- "serverAuthCode": cachedServerAuthCode ?? "",
507
- "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
560
+ "accessToken": user.accessToken.tokenString,
561
+ "serverAuthCode": cachedServerAuthCode ?? "",
562
+ "userId": user.userID ?? "",
563
+ "hostedDomain": user.configuration.hostedDomain ?? "",
564
+ "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
508
565
  ]
509
566
  completion(data as NSDictionary)
510
567
  return
@@ -704,6 +761,7 @@ public class AuthAdapter: NSObject {
704
761
  self.activeMicrosoftWebAuthSession?.cancel()
705
762
  self.activeMicrosoftWebAuthSession = nil
706
763
  }
764
+ finishInteractiveAuth()
707
765
  tokenStoreLock.lock()
708
766
  inMemoryMicrosoftRefreshToken = nil
709
767
  inMemoryMicrosoftScopes = defaultMicrosoftScopes
@@ -711,6 +769,23 @@ public class AuthAdapter: NSObject {
711
769
  tokenStoreLock.unlock()
712
770
  }
713
771
 
772
+ @objc
773
+ public static func revokeAccess(completion: @escaping (String?) -> Void) {
774
+ GIDSignIn.sharedInstance.disconnect { error in
775
+ tokenStoreLock.lock()
776
+ inMemoryMicrosoftRefreshToken = nil
777
+ inMemoryMicrosoftScopes = defaultMicrosoftScopes
778
+ inMemoryGoogleServerAuthCode = nil
779
+ tokenStoreLock.unlock()
780
+
781
+ if let error = error {
782
+ completion(mapError(error))
783
+ return
784
+ }
785
+ completion(nil)
786
+ }
787
+ }
788
+
714
789
  private static func activeWindow() -> UIWindow? {
715
790
  let windowScenes = UIApplication.shared.connectedScenes
716
791
  .compactMap { $0 as? UIWindowScene }
@@ -769,6 +844,8 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
769
844
  "email": email ?? "",
770
845
  "name": name,
771
846
  "idToken": idToken ?? "",
847
+ "authorizationCode": appleIDCredential.authorizationCode.flatMap { String(data: $0, encoding: .utf8) } ?? "",
848
+ "userId": appleIDCredential.user,
772
849
  "underlyingError": ""
773
850
  ]
774
851
  completion(data as NSDictionary, nil)