react-native-nitro-auth 0.5.5 → 0.5.7

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 (96) hide show
  1. package/README.md +30 -19
  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 +261 -68
  5. package/android/src/main/java/com/auth/AuthAdapter.kt +250 -157
  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 +208 -66
  11. package/ios/PlatformAuth+iOS.mm +30 -4
  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 +25 -19
  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 +34 -10
  25. package/lib/commonjs/use-auth.js.map +1 -1
  26. package/lib/commonjs/utils/auth-error.js +1 -1
  27. package/lib/commonjs/utils/auth-error.js.map +1 -1
  28. package/lib/commonjs/utils/logger.js +1 -0
  29. package/lib/commonjs/utils/logger.js.map +1 -1
  30. package/lib/module/Auth.web.js +50 -10
  31. package/lib/module/Auth.web.js.map +1 -1
  32. package/lib/module/global.d.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/index.web.js +2 -1
  35. package/lib/module/index.web.js.map +1 -1
  36. package/lib/module/service.js +25 -19
  37. package/lib/module/service.js.map +1 -1
  38. package/lib/module/service.web.js +65 -13
  39. package/lib/module/service.web.js.map +1 -1
  40. package/lib/module/ui/social-button.js +19 -14
  41. package/lib/module/ui/social-button.js.map +1 -1
  42. package/lib/module/ui/social-button.web.js +16 -10
  43. package/lib/module/ui/social-button.web.js.map +1 -1
  44. package/lib/module/use-auth.js +34 -10
  45. package/lib/module/use-auth.js.map +1 -1
  46. package/lib/module/utils/auth-error.js +1 -1
  47. package/lib/module/utils/auth-error.js.map +1 -1
  48. package/lib/module/utils/logger.js +1 -0
  49. package/lib/module/utils/logger.js.map +1 -1
  50. package/lib/typescript/commonjs/Auth.nitro.d.ts +2 -2
  51. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/Auth.web.d.ts +5 -1
  53. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/index.d.ts +1 -1
  55. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/index.web.d.ts +2 -1
  57. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/service.web.d.ts +2 -18
  60. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
  66. package/lib/typescript/module/Auth.nitro.d.ts +2 -2
  67. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  68. package/lib/typescript/module/Auth.web.d.ts +5 -1
  69. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  70. package/lib/typescript/module/index.d.ts +1 -1
  71. package/lib/typescript/module/index.d.ts.map +1 -1
  72. package/lib/typescript/module/index.web.d.ts +2 -1
  73. package/lib/typescript/module/index.web.d.ts.map +1 -1
  74. package/lib/typescript/module/service.d.ts.map +1 -1
  75. package/lib/typescript/module/service.web.d.ts +2 -18
  76. package/lib/typescript/module/service.web.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  78. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  79. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  80. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -1
  81. package/lib/typescript/module/utils/logger.d.ts.map +1 -1
  82. package/nitro.json +4 -1
  83. package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -0
  84. package/package.json +3 -4
  85. package/src/Auth.nitro.ts +3 -1
  86. package/src/Auth.web.ts +77 -11
  87. package/src/global.d.ts +0 -11
  88. package/src/index.ts +5 -1
  89. package/src/index.web.ts +6 -1
  90. package/src/service.ts +26 -19
  91. package/src/service.web.ts +84 -15
  92. package/src/ui/social-button.tsx +21 -9
  93. package/src/ui/social-button.web.tsx +17 -4
  94. package/src/use-auth.ts +65 -9
  95. package/src/utils/auth-error.ts +2 -0
  96. 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")
@@ -23,12 +25,11 @@ public class AuthAdapter: NSObject {
23
25
  let serverClientId = Bundle.main.object(forInfoDictionaryKey: "GIDServerClientID") as? String
24
26
 
25
27
  DispatchQueue.main.async {
26
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
27
- let rootVC = windowScene.windows.first?.rootViewController else {
28
- completion(nil, "No root view controller found")
28
+ guard let rootVC = presentingViewController() else {
29
+ completion(nil, "no_window")
29
30
  return
30
31
  }
31
-
32
+
32
33
  let config = GIDConfiguration(clientID: clientId, serverClientID: serverClientId)
33
34
  GIDSignIn.sharedInstance.configuration = config
34
35
 
@@ -58,7 +59,17 @@ public class AuthAdapter: NSObject {
58
59
  let delegate = AppleSignInDelegate(completion: completion)
59
60
  controller.delegate = delegate
60
61
  objc_setAssociatedObject(controller, &delegateHandle, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
61
- controller.performRequests()
62
+
63
+ DispatchQueue.main.async {
64
+ guard let window = activeWindow() else {
65
+ completion(nil, "no_window")
66
+ return
67
+ }
68
+ let contextProvider = AppleSignInContextProvider(anchor: window)
69
+ controller.presentationContextProvider = contextProvider
70
+ objc_setAssociatedObject(controller, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
71
+ controller.performRequests()
72
+ }
62
73
  } else if provider == "microsoft" {
63
74
  loginMicrosoft(scopes: scopes, loginHint: loginHint, tenant: tenant, prompt: prompt, completion: completion)
64
75
  } else {
@@ -78,15 +89,27 @@ public class AuthAdapter: NSObject {
78
89
  let effectiveScopes = scopes.isEmpty ? ["openid", "email", "profile", "offline_access", "User.Read"] : scopes
79
90
  let effectivePrompt = prompt ?? "select_account"
80
91
 
81
- let codeVerifier = generateCodeVerifier()
82
- let codeChallenge = generateCodeChallenge(codeVerifier)
92
+ guard let codeVerifier = generateCodeVerifier() else {
93
+ completion(nil, "configuration_error")
94
+ return
95
+ }
96
+ guard let codeChallenge = generateCodeChallenge(codeVerifier) else {
97
+ completion(nil, "configuration_error")
98
+ return
99
+ }
83
100
  let state = UUID().uuidString
84
101
  let nonce = UUID().uuidString
85
102
 
86
103
  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")!
104
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: effectiveTenant, b2cDomain: b2cDomain) else {
105
+ completion(nil, "configuration_error")
106
+ return
107
+ }
108
+
109
+ guard var urlComponents = URLComponents(string: "\(authBaseUrl)oauth2/v2.0/authorize") else {
110
+ completion(nil, "configuration_error")
111
+ return
112
+ }
90
113
  urlComponents.queryItems = [
91
114
  URLQueryItem(name: "client_id", value: clientId),
92
115
  URLQueryItem(name: "redirect_uri", value: redirectUri),
@@ -166,14 +189,12 @@ public class AuthAdapter: NSObject {
166
189
  completion: completion
167
190
  )
168
191
  }
169
-
170
- 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")
192
+
193
+ guard let window = activeWindow() else {
194
+ completion(nil, "no_window")
173
195
  return
174
196
  }
175
-
176
- let contextProvider = WebAuthContextProvider(anchor: rootVC.view.window!)
197
+ let contextProvider = WebAuthContextProvider(anchor: window)
177
198
  session.presentationContextProvider = contextProvider
178
199
  objc_setAssociatedObject(session, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
179
200
  session.prefersEphemeralWebBrowserSession = false
@@ -181,17 +202,19 @@ public class AuthAdapter: NSObject {
181
202
  }
182
203
  }
183
204
 
184
- private static func generateCodeVerifier() -> String {
205
+ private static func generateCodeVerifier() -> String? {
185
206
  var bytes = [UInt8](repeating: 0, count: 32)
186
- _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
207
+ guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
208
+ return nil
209
+ }
187
210
  return Data(bytes).base64EncodedString()
188
211
  .replacingOccurrences(of: "+", with: "-")
189
212
  .replacingOccurrences(of: "/", with: "_")
190
213
  .replacingOccurrences(of: "=", with: "")
191
214
  }
192
215
 
193
- private static func generateCodeChallenge(_ verifier: String) -> String {
194
- guard let data = verifier.data(using: .ascii) else { return "" }
216
+ private static func generateCodeChallenge(_ verifier: String) -> String? {
217
+ guard let data = verifier.data(using: .ascii) else { return nil }
195
218
  var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
196
219
  data.withUnsafeBytes {
197
220
  _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
@@ -213,13 +236,16 @@ public class AuthAdapter: NSObject {
213
236
  scopes: [String],
214
237
  completion: @escaping (NSDictionary?, String?) -> Void
215
238
  ) {
216
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
217
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
218
-
239
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
240
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
241
+ DispatchQueue.main.async { completion(nil, "configuration_error") }
242
+ return
243
+ }
244
+
219
245
  var request = URLRequest(url: tokenUrl)
220
246
  request.httpMethod = "POST"
221
247
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
222
-
248
+
223
249
  let bodyParams = [
224
250
  "client_id": clientId,
225
251
  "code": code,
@@ -236,18 +262,17 @@ public class AuthAdapter: NSObject {
236
262
  URLSession.shared.dataTask(with: request) { data, response, error in
237
263
  DispatchQueue.main.async {
238
264
  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
- }
265
+ completion(nil, "network_error")
245
266
  return
246
267
  }
247
268
 
248
269
  guard let data = data,
249
270
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
250
- completion(nil, "parse_error")
271
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
272
+ completion(nil, "network_error")
273
+ } else {
274
+ completion(nil, "parse_error")
275
+ }
251
276
  return
252
277
  }
253
278
 
@@ -256,6 +281,11 @@ public class AuthAdapter: NSObject {
256
281
  return
257
282
  }
258
283
 
284
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
285
+ completion(nil, "network_error")
286
+ return
287
+ }
288
+
259
289
  guard let idToken = json["id_token"] as? String else {
260
290
  completion(nil, "no_id_token")
261
291
  return
@@ -269,13 +299,15 @@ public class AuthAdapter: NSObject {
269
299
 
270
300
  let accessToken = json["access_token"] as? String ?? ""
271
301
  let refreshToken = json["refresh_token"] as? String ?? ""
272
- let expiresIn = json["expires_in"] as? Double ?? 0
302
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
273
303
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
274
304
 
305
+ tokenStoreLock.lock()
275
306
  if !refreshToken.isEmpty {
276
307
  inMemoryMicrosoftRefreshToken = refreshToken
277
308
  }
278
309
  inMemoryMicrosoftScopes = scopes.isEmpty ? defaultMicrosoftScopes : scopes
310
+ tokenStoreLock.unlock()
279
311
 
280
312
  let resultData: [String: Any] = [
281
313
  "provider": "microsoft",
@@ -285,6 +317,7 @@ public class AuthAdapter: NSObject {
285
317
  "idToken": idToken,
286
318
  "accessToken": accessToken,
287
319
  "serverAuthCode": "",
320
+ "scopes": scopes,
288
321
  "expirationTime": expirationTime,
289
322
  "underlyingError": ""
290
323
  ]
@@ -331,7 +364,9 @@ public class AuthAdapter: NSObject {
331
364
  }
332
365
 
333
366
  let serverAuthCode = result?.serverAuthCode ?? ""
367
+ tokenStoreLock.lock()
334
368
  inMemoryGoogleServerAuthCode = serverAuthCode.isEmpty ? nil : serverAuthCode
369
+ tokenStoreLock.unlock()
335
370
 
336
371
  let data: [String: Any] = [
337
372
  "provider": "google",
@@ -349,11 +384,14 @@ public class AuthAdapter: NSObject {
349
384
 
350
385
  static func mapError(_ error: Error) -> String {
351
386
  let nsError = error as NSError
387
+ if nsError.domain == NSURLErrorDomain {
388
+ return "network_error"
389
+ }
352
390
  // GIDSignIn error codes
353
391
  if nsError.domain == "com.google.GIDSignIn" {
354
392
  switch nsError.code {
355
393
  case -5: return "cancelled" // GIDSignInErrorCodeCanceled
356
- case -4: return "network_error" // GIDSignInErrorCodeNoCurrentUser (used for network issues)
394
+ case -4: return "not_signed_in" // GIDSignInErrorCodeNoCurrentUser
357
395
  default: break
358
396
  }
359
397
  }
@@ -386,9 +424,8 @@ public class AuthAdapter: NSObject {
386
424
  public static func addScopes(scopes: [String], completion: @escaping (NSDictionary?, String?) -> Void) {
387
425
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
388
426
  DispatchQueue.main.async {
389
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
390
- let rootVC = windowScene.windows.first?.rootViewController else {
391
- completion(nil, "No root view controller found")
427
+ guard let rootVC = presentingViewController() else {
428
+ completion(nil, "no_window")
392
429
  return
393
430
  }
394
431
  currentUser.addScopes(scopes, presenting: rootVC) { result, error in
@@ -397,11 +434,17 @@ public class AuthAdapter: NSObject {
397
434
  }
398
435
  return
399
436
  }
400
- guard inMemoryMicrosoftRefreshToken != nil else {
401
- completion(nil, "No user logged in")
437
+ tokenStoreLock.lock()
438
+ let hasRefreshToken = inMemoryMicrosoftRefreshToken != nil
439
+ let currentScopes = inMemoryMicrosoftScopes
440
+ tokenStoreLock.unlock()
441
+ guard hasRefreshToken else {
442
+ completion(nil, "not_signed_in")
402
443
  return
403
444
  }
404
- let mergedScopes = Array(Set(inMemoryMicrosoftScopes + scopes))
445
+ let mergedScopes = (currentScopes + scopes).reduce(into: [String]()) { acc, s in
446
+ if !acc.contains(s) { acc.append(s) }
447
+ }
405
448
  loginMicrosoft(scopes: mergedScopes, loginHint: nil, tenant: nil, prompt: nil, completion: completion)
406
449
  }
407
450
 
@@ -410,7 +453,7 @@ public class AuthAdapter: NSObject {
410
453
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
411
454
  currentUser.refreshTokensIfNeeded { user, error in
412
455
  if let error = error {
413
- completion(nil, error.localizedDescription)
456
+ completion(nil, mapError(error))
414
457
  return
415
458
  }
416
459
  guard let user = user else {
@@ -435,6 +478,9 @@ public class AuthAdapter: NSObject {
435
478
  if Bundle.main.object(forInfoDictionaryKey: "GIDClientID") != nil {
436
479
  GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
437
480
  if let user = user {
481
+ tokenStoreLock.lock()
482
+ let cachedServerAuthCode = inMemoryGoogleServerAuthCode
483
+ tokenStoreLock.unlock()
438
484
  let data: [String: Any] = [
439
485
  "provider": "google",
440
486
  "email": user.profile?.email ?? "",
@@ -442,7 +488,7 @@ public class AuthAdapter: NSObject {
442
488
  "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
443
489
  "idToken": user.idToken?.tokenString ?? "",
444
490
  "accessToken": user.accessToken.tokenString,
445
- "serverAuthCode": inMemoryGoogleServerAuthCode ?? "",
491
+ "serverAuthCode": cachedServerAuthCode ?? "",
446
492
  "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
447
493
  ]
448
494
  completion(data as NSDictionary)
@@ -456,7 +502,11 @@ public class AuthAdapter: NSObject {
456
502
  }
457
503
 
458
504
  private static func tryMicrosoftSilentRefresh(completion: @escaping (NSDictionary?) -> Void) {
459
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
505
+ tokenStoreLock.lock()
506
+ let refreshToken = inMemoryMicrosoftRefreshToken
507
+ let currentScopes = inMemoryMicrosoftScopes
508
+ tokenStoreLock.unlock()
509
+ guard let refreshToken = refreshToken else {
460
510
  completion(nil)
461
511
  return
462
512
  }
@@ -468,13 +518,16 @@ public class AuthAdapter: NSObject {
468
518
 
469
519
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
470
520
  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
-
521
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
522
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
523
+ completion(nil)
524
+ return
525
+ }
526
+
474
527
  var request = URLRequest(url: tokenUrl)
475
528
  request.httpMethod = "POST"
476
529
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
477
-
530
+
478
531
  let bodyParams = [
479
532
  "client_id": clientId,
480
533
  "grant_type": "refresh_token",
@@ -488,23 +541,42 @@ public class AuthAdapter: NSObject {
488
541
 
489
542
  URLSession.shared.dataTask(with: request) { data, response, error in
490
543
  DispatchQueue.main.async {
544
+ if let error = error {
545
+ #if DEBUG
546
+ print("[NitroAuth] Microsoft silent refresh network error: \(error.localizedDescription)")
547
+ #endif
548
+ completion(nil)
549
+ return
550
+ }
551
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
552
+ #if DEBUG
553
+ print("[NitroAuth] Microsoft silent refresh HTTP \(httpResponse.statusCode)")
554
+ #endif
555
+ completion(nil)
556
+ return
557
+ }
491
558
  guard let data = data,
492
559
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
493
560
  let idToken = json["id_token"] as? String else {
561
+ #if DEBUG
562
+ print("[NitroAuth] Microsoft silent refresh: failed to parse token response")
563
+ #endif
494
564
  completion(nil)
495
565
  return
496
566
  }
497
-
567
+
498
568
  let claims = decodeJwt(idToken)
499
569
  let accessToken = json["access_token"] as? String ?? ""
500
570
  let newRefreshToken = json["refresh_token"] as? String ?? ""
501
- let expiresIn = json["expires_in"] as? Double ?? 0
571
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
502
572
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
503
-
573
+
574
+ tokenStoreLock.lock()
504
575
  if !newRefreshToken.isEmpty {
505
576
  inMemoryMicrosoftRefreshToken = newRefreshToken
506
577
  }
507
-
578
+ tokenStoreLock.unlock()
579
+
508
580
  let resultData: [String: Any] = [
509
581
  "provider": "microsoft",
510
582
  "email": claims["preferred_username"] ?? claims["email"] ?? "",
@@ -513,6 +585,7 @@ public class AuthAdapter: NSObject {
513
585
  "idToken": idToken,
514
586
  "accessToken": accessToken,
515
587
  "serverAuthCode": "",
588
+ "scopes": currentScopes,
516
589
  "expirationTime": expirationTime
517
590
  ]
518
591
  completion(resultData as NSDictionary)
@@ -521,8 +594,11 @@ public class AuthAdapter: NSObject {
521
594
  }
522
595
 
523
596
  private static func tryMicrosoftRefreshForTokenRefresh(completion: @escaping (NSDictionary?, String?) -> Void) {
524
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
525
- completion(nil, "No user logged in")
597
+ tokenStoreLock.lock()
598
+ let refreshToken = inMemoryMicrosoftRefreshToken
599
+ tokenStoreLock.unlock()
600
+ guard let refreshToken = refreshToken else {
601
+ completion(nil, "not_signed_in")
526
602
  return
527
603
  }
528
604
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "MSALClientID") as? String, !clientId.isEmpty else {
@@ -531,8 +607,11 @@ public class AuthAdapter: NSObject {
531
607
  }
532
608
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
533
609
  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")!
610
+ guard let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain),
611
+ let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token") else {
612
+ completion(nil, "configuration_error")
613
+ return
614
+ }
536
615
  var request = URLRequest(url: tokenUrl)
537
616
  request.httpMethod = "POST"
538
617
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
@@ -553,21 +632,31 @@ public class AuthAdapter: NSObject {
553
632
  }
554
633
  guard let data = data,
555
634
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
556
- completion(nil, "parse_error")
635
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
636
+ completion(nil, "network_error")
637
+ } else {
638
+ completion(nil, "parse_error")
639
+ }
557
640
  return
558
641
  }
559
642
  if let errorCode = json["error"] as? String {
560
643
  completion(nil, AuthAdapter.mapOAuthError(errorCode))
561
644
  return
562
645
  }
646
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
647
+ completion(nil, "network_error")
648
+ return
649
+ }
563
650
  let idToken = json["id_token"] as? String ?? ""
564
651
  let accessToken = json["access_token"] as? String ?? ""
565
652
  let newRefreshToken = json["refresh_token"] as? String ?? ""
566
- let expiresIn = json["expires_in"] as? Double ?? 0
653
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
567
654
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
655
+ tokenStoreLock.lock()
568
656
  if !newRefreshToken.isEmpty {
569
657
  inMemoryMicrosoftRefreshToken = newRefreshToken
570
658
  }
659
+ tokenStoreLock.unlock()
571
660
  let tokensData: [String: Any] = [
572
661
  "accessToken": accessToken,
573
662
  "idToken": idToken,
@@ -579,24 +668,63 @@ public class AuthAdapter: NSObject {
579
668
  }.resume()
580
669
  }
581
670
 
582
- private static func getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?) -> String {
583
- if tenant.hasPrefix("https://") {
584
- return tenant.hasSuffix("/") ? tenant : "\(tenant)/"
671
+ private static func getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?) -> String? {
672
+ let trimmedTenant = tenant.trimmingCharacters(in: .whitespacesAndNewlines)
673
+ guard !trimmedTenant.isEmpty else { return nil }
674
+
675
+ if trimmedTenant.hasPrefix("https://") {
676
+ guard URL(string: trimmedTenant) != nil else { return nil }
677
+ return trimmedTenant.hasSuffix("/") ? trimmedTenant : "\(trimmedTenant)/"
585
678
  }
586
-
587
- if let domain = b2cDomain {
588
- return "https://\(domain)/tfp/\(tenant)/"
589
- } else {
590
- return "https://login.microsoftonline.com/\(tenant)/"
679
+ if let domain = b2cDomain?.trimmingCharacters(in: .whitespacesAndNewlines), !domain.isEmpty {
680
+ return "https://\(domain)/tfp/\(trimmedTenant)/"
591
681
  }
682
+ return "https://login.microsoftonline.com/\(trimmedTenant)/"
592
683
  }
593
684
 
594
685
  @objc
595
686
  public static func logout() {
596
687
  GIDSignIn.sharedInstance.signOut()
688
+ tokenStoreLock.lock()
597
689
  inMemoryMicrosoftRefreshToken = nil
598
690
  inMemoryMicrosoftScopes = defaultMicrosoftScopes
599
691
  inMemoryGoogleServerAuthCode = nil
692
+ tokenStoreLock.unlock()
693
+ }
694
+
695
+ private static func activeWindow() -> UIWindow? {
696
+ let windowScenes = UIApplication.shared.connectedScenes
697
+ .compactMap { $0 as? UIWindowScene }
698
+ .filter {
699
+ $0.activationState == .foregroundActive ||
700
+ $0.activationState == .foregroundInactive
701
+ }
702
+
703
+ for scene in windowScenes {
704
+ if let keyWindow = scene.windows.first(where: { $0.isKeyWindow }) {
705
+ return keyWindow
706
+ }
707
+ }
708
+
709
+ return windowScenes.lazy.compactMap { $0.windows.first }.first
710
+ }
711
+
712
+ private static func presentingViewController() -> UIViewController? {
713
+ guard let rootViewController = activeWindow()?.rootViewController else {
714
+ return nil
715
+ }
716
+
717
+ var current = rootViewController
718
+ while let presented = current.presentedViewController {
719
+ current = presented
720
+ }
721
+ if let navigationController = current as? UINavigationController {
722
+ return navigationController.visibleViewController ?? navigationController
723
+ }
724
+ if let tabBarController = current as? UITabBarController {
725
+ return tabBarController.selectedViewController ?? tabBarController
726
+ }
727
+ return current
600
728
  }
601
729
  }
602
730
 
@@ -625,14 +753,28 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
625
753
  "underlyingError": ""
626
754
  ]
627
755
  completion(data as NSDictionary, nil)
756
+ } else {
757
+ completion(nil, "unknown")
628
758
  }
629
759
  }
630
-
760
+
631
761
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
632
762
  completion(nil, AuthAdapter.mapError(error))
633
763
  }
634
764
  }
635
765
 
766
+ class AppleSignInContextProvider: NSObject, ASAuthorizationControllerPresentationContextProviding {
767
+ let anchor: ASPresentationAnchor
768
+
769
+ init(anchor: ASPresentationAnchor) {
770
+ self.anchor = anchor
771
+ }
772
+
773
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
774
+ return anchor
775
+ }
776
+ }
777
+
636
778
  class WebAuthContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
637
779
  let anchor: ASPresentationAnchor
638
780
 
@@ -16,9 +16,25 @@
16
16
 
17
17
  namespace margelo::nitro::NitroAuth {
18
18
 
19
- inline std::string nsToStd(NSString* _Nullable ns) {
20
- if (ns == nil) return "";
21
- return std::string([ns UTF8String]);
19
+ inline std::optional<std::string> nsToStd(NSString* _Nullable ns) {
20
+ if (ns == nil) return std::nullopt;
21
+ std::string value([ns UTF8String]);
22
+ if (value.empty()) return std::nullopt;
23
+ return value;
24
+ }
25
+
26
+ inline std::optional<std::vector<std::string>> nsArrayToStd(NSArray<NSString*>* _Nullable nsArray) {
27
+ if (nsArray == nil || nsArray.count == 0) return std::nullopt;
28
+
29
+ std::vector<std::string> values;
30
+ values.reserve(nsArray.count);
31
+ for (NSString* value in nsArray) {
32
+ if (value.length == 0) continue;
33
+ values.emplace_back([value UTF8String]);
34
+ }
35
+
36
+ if (values.empty()) return std::nullopt;
37
+ return values;
22
38
  }
23
39
 
24
40
  std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
@@ -85,6 +101,7 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
85
101
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
86
102
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
87
103
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
104
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
88
105
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
89
106
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
90
107
 
@@ -109,13 +126,21 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::requestScopes(const std::vector
109
126
  }
110
127
 
111
128
  AuthUser user;
112
- user.provider = AuthProvider::GOOGLE;
129
+ NSString *providerStr = [data objectForKey:@"provider"];
130
+ if ([providerStr isEqualToString:@"microsoft"]) {
131
+ user.provider = AuthProvider::MICROSOFT;
132
+ } else if ([providerStr isEqualToString:@"apple"]) {
133
+ user.provider = AuthProvider::APPLE;
134
+ } else {
135
+ user.provider = AuthProvider::GOOGLE;
136
+ }
113
137
  user.email = nsToStd([data objectForKey:@"email"]);
114
138
  user.name = nsToStd([data objectForKey:@"name"]);
115
139
  user.photo = nsToStd([data objectForKey:@"photo"]);
116
140
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
117
141
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
118
142
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
143
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
119
144
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
120
145
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
121
146
  promise->resolve(user);
@@ -161,6 +186,7 @@ std::shared_ptr<Promise<std::optional<AuthUser>>> PlatformAuth::silentRestore()
161
186
  user.idToken = nsToStd([data objectForKey:@"idToken"]);
162
187
  if ([data objectForKey:@"accessToken"]) user.accessToken = nsToStd([data objectForKey:@"accessToken"]);
163
188
  if ([data objectForKey:@"serverAuthCode"]) user.serverAuthCode = nsToStd([data objectForKey:@"serverAuthCode"]);
189
+ if ([data objectForKey:@"scopes"]) user.scopes = nsArrayToStd([data objectForKey:@"scopes"]);
164
190
  if ([data objectForKey:@"expirationTime"]) user.expirationTime = [[data objectForKey:@"expirationTime"] doubleValue];
165
191
  if ([data objectForKey:@"underlyingError"]) user.underlyingError = nsToStd([data objectForKey:@"underlyingError"]);
166
192
  promise->resolve(user);