react-native-nitro-auth 0.5.5 → 0.5.6

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 (82) hide show
  1. package/README.md +22 -17
  2. package/android/proguard-rules.pro +7 -1
  3. package/android/src/main/AndroidManifest.xml +12 -0
  4. package/android/src/main/cpp/PlatformAuth+Android.cpp +260 -67
  5. package/android/src/main/java/com/auth/AuthAdapter.kt +217 -146
  6. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +9 -5
  7. package/cpp/HybridAuth.cpp +79 -64
  8. package/cpp/HybridAuth.hpp +9 -7
  9. package/cpp/JSONSerializer.hpp +3 -0
  10. package/ios/AuthAdapter.swift +173 -60
  11. package/ios/PlatformAuth+iOS.mm +10 -3
  12. package/lib/commonjs/Auth.web.js +50 -10
  13. package/lib/commonjs/Auth.web.js.map +1 -1
  14. package/lib/commonjs/index.web.js +30 -12
  15. package/lib/commonjs/index.web.js.map +1 -1
  16. package/lib/commonjs/service.js +9 -7
  17. package/lib/commonjs/service.js.map +1 -1
  18. package/lib/commonjs/service.web.js +65 -13
  19. package/lib/commonjs/service.web.js.map +1 -1
  20. package/lib/commonjs/ui/social-button.js +19 -14
  21. package/lib/commonjs/ui/social-button.js.map +1 -1
  22. package/lib/commonjs/ui/social-button.web.js +16 -10
  23. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  24. package/lib/commonjs/use-auth.js +13 -5
  25. package/lib/commonjs/use-auth.js.map +1 -1
  26. package/lib/commonjs/utils/logger.js +1 -0
  27. package/lib/commonjs/utils/logger.js.map +1 -1
  28. package/lib/module/Auth.web.js +50 -10
  29. package/lib/module/Auth.web.js.map +1 -1
  30. package/lib/module/global.d.js.map +1 -1
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/index.web.js +2 -1
  33. package/lib/module/index.web.js.map +1 -1
  34. package/lib/module/service.js +9 -7
  35. package/lib/module/service.js.map +1 -1
  36. package/lib/module/service.web.js +65 -13
  37. package/lib/module/service.web.js.map +1 -1
  38. package/lib/module/ui/social-button.js +19 -14
  39. package/lib/module/ui/social-button.js.map +1 -1
  40. package/lib/module/ui/social-button.web.js +16 -10
  41. package/lib/module/ui/social-button.web.js.map +1 -1
  42. package/lib/module/use-auth.js +13 -5
  43. package/lib/module/use-auth.js.map +1 -1
  44. package/lib/module/utils/logger.js +1 -0
  45. package/lib/module/utils/logger.js.map +1 -1
  46. package/lib/typescript/commonjs/Auth.web.d.ts +5 -1
  47. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/index.d.ts +1 -1
  49. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/index.web.d.ts +2 -1
  51. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/service.web.d.ts +2 -18
  54. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
  59. package/lib/typescript/module/Auth.web.d.ts +5 -1
  60. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  61. package/lib/typescript/module/index.d.ts +1 -1
  62. package/lib/typescript/module/index.d.ts.map +1 -1
  63. package/lib/typescript/module/index.web.d.ts +2 -1
  64. package/lib/typescript/module/index.web.d.ts.map +1 -1
  65. package/lib/typescript/module/service.d.ts.map +1 -1
  66. package/lib/typescript/module/service.web.d.ts +2 -18
  67. package/lib/typescript/module/service.web.d.ts.map +1 -1
  68. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  69. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  70. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  71. package/lib/typescript/module/utils/logger.d.ts.map +1 -1
  72. package/package.json +2 -4
  73. package/src/Auth.web.ts +77 -11
  74. package/src/global.d.ts +0 -11
  75. package/src/index.ts +5 -1
  76. package/src/index.web.ts +6 -1
  77. package/src/service.ts +9 -7
  78. package/src/service.web.ts +84 -15
  79. package/src/ui/social-button.tsx +21 -9
  80. package/src/ui/social-button.web.tsx +17 -4
  81. package/src/use-auth.ts +35 -8
  82. package/src/utils/logger.ts +1 -0
@@ -2,7 +2,6 @@ import Foundation
2
2
  import GoogleSignIn
3
3
  import AuthenticationServices
4
4
  import NitroModules
5
- import ObjectiveC
6
5
  import CommonCrypto
7
6
 
8
7
  @objc
@@ -11,9 +10,12 @@ public class AuthAdapter: NSObject {
11
10
  private static var inMemoryMicrosoftRefreshToken: String?
12
11
  private static var inMemoryMicrosoftScopes: [String] = defaultMicrosoftScopes
13
12
  private static var inMemoryGoogleServerAuthCode: String?
13
+ private static let tokenStoreLock = NSLock()
14
14
 
15
15
  @objc
16
16
  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) {
17
+ // useSheet is accepted for API compatibility with Android but has no effect on iOS.
18
+ // Google Sign-In SDK controls its own presentation style.
17
19
  if provider == "google" {
18
20
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String, !clientId.isEmpty else {
19
21
  completion(nil, "configuration_error")
@@ -25,10 +27,10 @@ public class AuthAdapter: NSObject {
25
27
  DispatchQueue.main.async {
26
28
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
27
29
  let rootVC = windowScene.windows.first?.rootViewController else {
28
- completion(nil, "No root view controller found")
30
+ completion(nil, "no_window")
29
31
  return
30
32
  }
31
-
33
+
32
34
  let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId)
33
35
  GIDSignIn.sharedInstance.configuration = config
34
36
 
@@ -58,7 +60,18 @@ public class AuthAdapter: NSObject {
58
60
  let delegate = AppleSignInDelegate(completion: completion)
59
61
  controller.delegate = delegate
60
62
  objc_setAssociatedObject(controller, &delegateHandle, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
61
- controller.performRequests()
63
+
64
+ DispatchQueue.main.async {
65
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
66
+ let window = windowScene.windows.first(where: { $0.isKeyWindow }) ?? windowScene.windows.first else {
67
+ completion(nil, "no_window")
68
+ return
69
+ }
70
+ let contextProvider = AppleSignInContextProvider(anchor: window)
71
+ controller.presentationContextProvider = contextProvider
72
+ objc_setAssociatedObject(controller, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
73
+ controller.performRequests()
74
+ }
62
75
  } else if provider == "microsoft" {
63
76
  loginMicrosoft(scopes: scopes, loginHint: loginHint, tenant: tenant, prompt: prompt, completion: completion)
64
77
  } else {
@@ -78,15 +91,27 @@ public class AuthAdapter: NSObject {
78
91
  let effectiveScopes = scopes.isEmpty ? ["openid", "email", "profile", "offline_access", "User.Read"] : scopes
79
92
  let effectivePrompt = prompt ?? "select_account"
80
93
 
81
- let codeVerifier = generateCodeVerifier()
82
- let codeChallenge = generateCodeChallenge(codeVerifier)
94
+ guard let codeVerifier = generateCodeVerifier() else {
95
+ completion(nil, "configuration_error")
96
+ return
97
+ }
98
+ guard let codeChallenge = generateCodeChallenge(codeVerifier) else {
99
+ completion(nil, "configuration_error")
100
+ return
101
+ }
83
102
  let state = UUID().uuidString
84
103
  let nonce = UUID().uuidString
85
104
 
86
105
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
87
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: effectiveTenant, b2cDomain: b2cDomain)
88
-
89
- var urlComponents = URLComponents(string: "\(authBaseUrl)oauth2/v2.0/authorize")!
106
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: effectiveTenant, b2cDomain: b2cDomain) else {
107
+ completion(nil, "configuration_error")
108
+ return
109
+ }
110
+
111
+ guard var urlComponents = URLComponents(string: "\(authBaseUrl)oauth2/v2.0/authorize") else {
112
+ completion(nil, "configuration_error")
113
+ return
114
+ }
90
115
  urlComponents.queryItems = [
91
116
  URLQueryItem(name: "client_id", value: clientId),
92
117
  URLQueryItem(name: "redirect_uri", value: redirectUri),
@@ -168,12 +193,16 @@ public class AuthAdapter: NSObject {
168
193
  }
169
194
 
170
195
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
171
- let rootVC = windowScene.windows.first?.rootViewController else {
172
- completion(nil, "No root view controller found")
196
+ let window = windowScene.windows.first(where: { $0.isKeyWindow }) ?? windowScene.windows.first,
197
+ let rootVC = window.rootViewController else {
198
+ completion(nil, "no_window")
173
199
  return
174
200
  }
175
-
176
- let contextProvider = WebAuthContextProvider(anchor: rootVC.view.window!)
201
+ guard let window = rootVC.view.window else {
202
+ completion(nil, "no_window")
203
+ return
204
+ }
205
+ let contextProvider = WebAuthContextProvider(anchor: window)
177
206
  session.presentationContextProvider = contextProvider
178
207
  objc_setAssociatedObject(session, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
179
208
  session.prefersEphemeralWebBrowserSession = false
@@ -181,17 +210,19 @@ public class AuthAdapter: NSObject {
181
210
  }
182
211
  }
183
212
 
184
- private static func generateCodeVerifier() -> String {
213
+ private static func generateCodeVerifier() -> String? {
185
214
  var bytes = [UInt8](repeating: 0, count: 32)
186
- _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
215
+ guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
216
+ return nil
217
+ }
187
218
  return Data(bytes).base64EncodedString()
188
219
  .replacingOccurrences(of: "+", with: "-")
189
220
  .replacingOccurrences(of: "/", with: "_")
190
221
  .replacingOccurrences(of: "=", with: "")
191
222
  }
192
223
 
193
- private static func generateCodeChallenge(_ verifier: String) -> String {
194
- guard let data = verifier.data(using: .ascii) else { return "" }
224
+ private static func generateCodeChallenge(_ verifier: String) -> String? {
225
+ guard let data = verifier.data(using: .ascii) else { return nil }
195
226
  var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
196
227
  data.withUnsafeBytes {
197
228
  _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
@@ -213,13 +244,16 @@ public class AuthAdapter: NSObject {
213
244
  scopes: [String],
214
245
  completion: @escaping (NSDictionary?, String?) -> Void
215
246
  ) {
216
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
217
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
218
-
247
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
248
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
249
+ DispatchQueue.main.async { completion(nil, "configuration_error") }
250
+ return
251
+ }
252
+
219
253
  var request = URLRequest(url: tokenUrl)
220
254
  request.httpMethod = "POST"
221
255
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
222
-
256
+
223
257
  let bodyParams = [
224
258
  "client_id": clientId,
225
259
  "code": code,
@@ -236,18 +270,17 @@ public class AuthAdapter: NSObject {
236
270
  URLSession.shared.dataTask(with: request) { data, response, error in
237
271
  DispatchQueue.main.async {
238
272
  if let error = error {
239
- let nsError = error as NSError
240
- if nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut {
241
- completion(nil, "network_error")
242
- } else {
243
- completion(nil, "network_error")
244
- }
273
+ completion(nil, "network_error")
245
274
  return
246
275
  }
247
276
 
248
277
  guard let data = data,
249
278
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
250
- completion(nil, "parse_error")
279
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
280
+ completion(nil, "network_error")
281
+ } else {
282
+ completion(nil, "parse_error")
283
+ }
251
284
  return
252
285
  }
253
286
 
@@ -256,6 +289,11 @@ public class AuthAdapter: NSObject {
256
289
  return
257
290
  }
258
291
 
292
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
293
+ completion(nil, "network_error")
294
+ return
295
+ }
296
+
259
297
  guard let idToken = json["id_token"] as? String else {
260
298
  completion(nil, "no_id_token")
261
299
  return
@@ -269,13 +307,15 @@ public class AuthAdapter: NSObject {
269
307
 
270
308
  let accessToken = json["access_token"] as? String ?? ""
271
309
  let refreshToken = json["refresh_token"] as? String ?? ""
272
- let expiresIn = json["expires_in"] as? Double ?? 0
310
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
273
311
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
274
312
 
313
+ tokenStoreLock.lock()
275
314
  if !refreshToken.isEmpty {
276
315
  inMemoryMicrosoftRefreshToken = refreshToken
277
316
  }
278
317
  inMemoryMicrosoftScopes = scopes.isEmpty ? defaultMicrosoftScopes : scopes
318
+ tokenStoreLock.unlock()
279
319
 
280
320
  let resultData: [String: Any] = [
281
321
  "provider": "microsoft",
@@ -331,7 +371,9 @@ public class AuthAdapter: NSObject {
331
371
  }
332
372
 
333
373
  let serverAuthCode = result?.serverAuthCode ?? ""
374
+ tokenStoreLock.lock()
334
375
  inMemoryGoogleServerAuthCode = serverAuthCode.isEmpty ? nil : serverAuthCode
376
+ tokenStoreLock.unlock()
335
377
 
336
378
  let data: [String: Any] = [
337
379
  "provider": "google",
@@ -349,11 +391,14 @@ public class AuthAdapter: NSObject {
349
391
 
350
392
  static func mapError(_ error: Error) -> String {
351
393
  let nsError = error as NSError
394
+ if nsError.domain == NSURLErrorDomain {
395
+ return "network_error"
396
+ }
352
397
  // GIDSignIn error codes
353
398
  if nsError.domain == "com.google.GIDSignIn" {
354
399
  switch nsError.code {
355
400
  case -5: return "cancelled" // GIDSignInErrorCodeCanceled
356
- case -4: return "network_error" // GIDSignInErrorCodeNoCurrentUser (used for network issues)
401
+ case -4: return "not_signed_in" // GIDSignInErrorCodeNoCurrentUser
357
402
  default: break
358
403
  }
359
404
  }
@@ -388,7 +433,7 @@ public class AuthAdapter: NSObject {
388
433
  DispatchQueue.main.async {
389
434
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
390
435
  let rootVC = windowScene.windows.first?.rootViewController else {
391
- completion(nil, "No root view controller found")
436
+ completion(nil, "no_window")
392
437
  return
393
438
  }
394
439
  currentUser.addScopes(scopes, presenting: rootVC) { result, error in
@@ -397,11 +442,17 @@ public class AuthAdapter: NSObject {
397
442
  }
398
443
  return
399
444
  }
400
- guard inMemoryMicrosoftRefreshToken != nil else {
401
- completion(nil, "No user logged in")
445
+ tokenStoreLock.lock()
446
+ let hasRefreshToken = inMemoryMicrosoftRefreshToken != nil
447
+ let currentScopes = inMemoryMicrosoftScopes
448
+ tokenStoreLock.unlock()
449
+ guard hasRefreshToken else {
450
+ completion(nil, "not_signed_in")
402
451
  return
403
452
  }
404
- let mergedScopes = Array(Set(inMemoryMicrosoftScopes + scopes))
453
+ let mergedScopes = (currentScopes + scopes).reduce(into: [String]()) { acc, s in
454
+ if !acc.contains(s) { acc.append(s) }
455
+ }
405
456
  loginMicrosoft(scopes: mergedScopes, loginHint: nil, tenant: nil, prompt: nil, completion: completion)
406
457
  }
407
458
 
@@ -410,7 +461,7 @@ public class AuthAdapter: NSObject {
410
461
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
411
462
  currentUser.refreshTokensIfNeeded { user, error in
412
463
  if let error = error {
413
- completion(nil, error.localizedDescription)
464
+ completion(nil, mapError(error))
414
465
  return
415
466
  }
416
467
  guard let user = user else {
@@ -435,6 +486,9 @@ public class AuthAdapter: NSObject {
435
486
  if Bundle.main.object(forInfoDictionaryKey: "GIDClientID") != nil {
436
487
  GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
437
488
  if let user = user {
489
+ tokenStoreLock.lock()
490
+ let cachedServerAuthCode = inMemoryGoogleServerAuthCode
491
+ tokenStoreLock.unlock()
438
492
  let data: [String: Any] = [
439
493
  "provider": "google",
440
494
  "email": user.profile?.email ?? "",
@@ -442,7 +496,7 @@ public class AuthAdapter: NSObject {
442
496
  "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
443
497
  "idToken": user.idToken?.tokenString ?? "",
444
498
  "accessToken": user.accessToken.tokenString,
445
- "serverAuthCode": inMemoryGoogleServerAuthCode ?? "",
499
+ "serverAuthCode": cachedServerAuthCode ?? "",
446
500
  "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
447
501
  ]
448
502
  completion(data as NSDictionary)
@@ -456,7 +510,10 @@ public class AuthAdapter: NSObject {
456
510
  }
457
511
 
458
512
  private static func tryMicrosoftSilentRefresh(completion: @escaping (NSDictionary?) -> Void) {
459
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
513
+ tokenStoreLock.lock()
514
+ let refreshToken = inMemoryMicrosoftRefreshToken
515
+ tokenStoreLock.unlock()
516
+ guard let refreshToken = refreshToken else {
460
517
  completion(nil)
461
518
  return
462
519
  }
@@ -468,13 +525,16 @@ public class AuthAdapter: NSObject {
468
525
 
469
526
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
470
527
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
471
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
472
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
473
-
528
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
529
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
530
+ completion(nil)
531
+ return
532
+ }
533
+
474
534
  var request = URLRequest(url: tokenUrl)
475
535
  request.httpMethod = "POST"
476
536
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
477
-
537
+
478
538
  let bodyParams = [
479
539
  "client_id": clientId,
480
540
  "grant_type": "refresh_token",
@@ -488,23 +548,42 @@ public class AuthAdapter: NSObject {
488
548
 
489
549
  URLSession.shared.dataTask(with: request) { data, response, error in
490
550
  DispatchQueue.main.async {
551
+ if let error = error {
552
+ #if DEBUG
553
+ print("[NitroAuth] Microsoft silent refresh network error: \(error.localizedDescription)")
554
+ #endif
555
+ completion(nil)
556
+ return
557
+ }
558
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
559
+ #if DEBUG
560
+ print("[NitroAuth] Microsoft silent refresh HTTP \(httpResponse.statusCode)")
561
+ #endif
562
+ completion(nil)
563
+ return
564
+ }
491
565
  guard let data = data,
492
566
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
493
567
  let idToken = json["id_token"] as? String else {
568
+ #if DEBUG
569
+ print("[NitroAuth] Microsoft silent refresh: failed to parse token response")
570
+ #endif
494
571
  completion(nil)
495
572
  return
496
573
  }
497
-
574
+
498
575
  let claims = decodeJwt(idToken)
499
576
  let accessToken = json["access_token"] as? String ?? ""
500
577
  let newRefreshToken = json["refresh_token"] as? String ?? ""
501
- let expiresIn = json["expires_in"] as? Double ?? 0
578
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
502
579
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
503
-
580
+
581
+ tokenStoreLock.lock()
504
582
  if !newRefreshToken.isEmpty {
505
583
  inMemoryMicrosoftRefreshToken = newRefreshToken
506
584
  }
507
-
585
+ tokenStoreLock.unlock()
586
+
508
587
  let resultData: [String: Any] = [
509
588
  "provider": "microsoft",
510
589
  "email": claims["preferred_username"] ?? claims["email"] ?? "",
@@ -521,8 +600,11 @@ public class AuthAdapter: NSObject {
521
600
  }
522
601
 
523
602
  private static func tryMicrosoftRefreshForTokenRefresh(completion: @escaping (NSDictionary?, String?) -> Void) {
524
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
525
- completion(nil, "No user logged in")
603
+ tokenStoreLock.lock()
604
+ let refreshToken = inMemoryMicrosoftRefreshToken
605
+ tokenStoreLock.unlock()
606
+ guard let refreshToken = refreshToken else {
607
+ completion(nil, "not_signed_in")
526
608
  return
527
609
  }
528
610
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "MSALClientID") as? String, !clientId.isEmpty else {
@@ -531,8 +613,11 @@ public class AuthAdapter: NSObject {
531
613
  }
532
614
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
533
615
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
534
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
535
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
616
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
617
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
618
+ completion(nil, "configuration_error")
619
+ return
620
+ }
536
621
  var request = URLRequest(url: tokenUrl)
537
622
  request.httpMethod = "POST"
538
623
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
@@ -553,21 +638,31 @@ public class AuthAdapter: NSObject {
553
638
  }
554
639
  guard let data = data,
555
640
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
556
- completion(nil, "parse_error")
641
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
642
+ completion(nil, "network_error")
643
+ } else {
644
+ completion(nil, "parse_error")
645
+ }
557
646
  return
558
647
  }
559
648
  if let errorCode = json["error"] as? String {
560
649
  completion(nil, AuthAdapter.mapOAuthError(errorCode))
561
650
  return
562
651
  }
652
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
653
+ completion(nil, "network_error")
654
+ return
655
+ }
563
656
  let idToken = json["id_token"] as? String ?? ""
564
657
  let accessToken = json["access_token"] as? String ?? ""
565
658
  let newRefreshToken = json["refresh_token"] as? String ?? ""
566
- let expiresIn = json["expires_in"] as? Double ?? 0
659
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
567
660
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
661
+ tokenStoreLock.lock()
568
662
  if !newRefreshToken.isEmpty {
569
663
  inMemoryMicrosoftRefreshToken = newRefreshToken
570
664
  }
665
+ tokenStoreLock.unlock()
571
666
  let tokensData: [String: Any] = [
572
667
  "accessToken": accessToken,
573
668
  "idToken": idToken,
@@ -579,24 +674,28 @@ public class AuthAdapter: NSObject {
579
674
  }.resume()
580
675
  }
581
676
 
582
- private static func getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?) -> String {
583
- if tenant.hasPrefix("https://") {
584
- return tenant.hasSuffix("/") ? tenant : "\(tenant)/"
677
+ private static func getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?) -> String? {
678
+ let trimmedTenant = tenant.trimmingCharacters(in: .whitespacesAndNewlines)
679
+ guard !trimmedTenant.isEmpty else { return nil }
680
+
681
+ if trimmedTenant.hasPrefix("https://") {
682
+ guard URL(string: trimmedTenant) != nil else { return nil }
683
+ return trimmedTenant.hasSuffix("/") ? trimmedTenant : "\(trimmedTenant)/"
585
684
  }
586
-
587
- if let domain = b2cDomain {
588
- return "https://\(domain)/tfp/\(tenant)/"
589
- } else {
590
- return "https://login.microsoftonline.com/\(tenant)/"
685
+ if let domain = b2cDomain?.trimmingCharacters(in: .whitespacesAndNewlines), !domain.isEmpty {
686
+ return "https://\(domain)/tfp/\(trimmedTenant)/"
591
687
  }
688
+ return "https://login.microsoftonline.com/\(trimmedTenant)/"
592
689
  }
593
690
 
594
691
  @objc
595
692
  public static func logout() {
596
693
  GIDSignIn.sharedInstance.signOut()
694
+ tokenStoreLock.lock()
597
695
  inMemoryMicrosoftRefreshToken = nil
598
696
  inMemoryMicrosoftScopes = defaultMicrosoftScopes
599
697
  inMemoryGoogleServerAuthCode = nil
698
+ tokenStoreLock.unlock()
600
699
  }
601
700
  }
602
701
 
@@ -625,14 +724,28 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
625
724
  "underlyingError": ""
626
725
  ]
627
726
  completion(data as NSDictionary, nil)
727
+ } else {
728
+ completion(nil, "unknown")
628
729
  }
629
730
  }
630
-
731
+
631
732
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
632
733
  completion(nil, AuthAdapter.mapError(error))
633
734
  }
634
735
  }
635
736
 
737
+ class AppleSignInContextProvider: NSObject, ASAuthorizationControllerPresentationContextProviding {
738
+ let anchor: ASPresentationAnchor
739
+
740
+ init(anchor: ASPresentationAnchor) {
741
+ self.anchor = anchor
742
+ }
743
+
744
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
745
+ return anchor
746
+ }
747
+ }
748
+
636
749
  class WebAuthContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
637
750
  let anchor: ASPresentationAnchor
638
751
 
@@ -16,8 +16,8 @@
16
16
 
17
17
  namespace margelo::nitro::NitroAuth {
18
18
 
19
- inline std::string nsToStd(NSString* _Nullable ns) {
20
- if (ns == nil) return "";
19
+ inline std::optional<std::string> nsToStd(NSString* _Nullable ns) {
20
+ if (ns == nil) return std::nullopt;
21
21
  return std::string([ns UTF8String]);
22
22
  }
23
23
 
@@ -109,7 +109,14 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
109
109
  }
110
110
 
111
111
  AuthUser user;
112
- user.provider = AuthProvider::GOOGLE;
112
+ NSString *providerStr = [data objectForKey:@"provider"];
113
+ if ([providerStr isEqualToString:@"microsoft"]) {
114
+ user.provider = AuthProvider::MICROSOFT;
115
+ } else if ([providerStr isEqualToString:@"apple"]) {
116
+ user.provider = AuthProvider::APPLE;
117
+ } else {
118
+ user.provider = AuthProvider::GOOGLE;
119
+ }
113
120
  user.email = nsToStd([data objectForKey:@"email"]);
114
121
  user.name = nsToStd([data objectForKey:@"name"]);
115
122
  user.photo = nsToStd([data objectForKey:@"photo"]);