react-native-nitro-auth 0.5.4 → 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 (100) hide show
  1. package/README.md +82 -47
  2. package/android/proguard-rules.pro +7 -1
  3. package/android/src/main/AndroidManifest.xml +12 -0
  4. package/android/src/main/cpp/JniOnLoad.cpp +3 -1
  5. package/android/src/main/cpp/PlatformAuth+Android.cpp +271 -78
  6. package/android/src/main/java/com/auth/AuthAdapter.kt +293 -238
  7. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +9 -5
  8. package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
  9. package/cpp/HybridAuth.cpp +79 -64
  10. package/cpp/HybridAuth.hpp +9 -7
  11. package/cpp/JSONSerializer.hpp +3 -0
  12. package/ios/AuthAdapter.swift +226 -79
  13. package/ios/PlatformAuth+iOS.mm +10 -3
  14. package/lib/commonjs/Auth.web.js +50 -10
  15. package/lib/commonjs/Auth.web.js.map +1 -1
  16. package/lib/commonjs/index.js +23 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/index.web.js +30 -12
  19. package/lib/commonjs/index.web.js.map +1 -1
  20. package/lib/commonjs/service.js +36 -9
  21. package/lib/commonjs/service.js.map +1 -1
  22. package/lib/commonjs/service.web.js +65 -13
  23. package/lib/commonjs/service.web.js.map +1 -1
  24. package/lib/commonjs/ui/social-button.js +19 -14
  25. package/lib/commonjs/ui/social-button.js.map +1 -1
  26. package/lib/commonjs/ui/social-button.web.js +16 -10
  27. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  28. package/lib/commonjs/use-auth.js +22 -25
  29. package/lib/commonjs/use-auth.js.map +1 -1
  30. package/lib/commonjs/utils/auth-error.js +37 -0
  31. package/lib/commonjs/utils/auth-error.js.map +1 -0
  32. package/lib/commonjs/utils/logger.js +1 -0
  33. package/lib/commonjs/utils/logger.js.map +1 -1
  34. package/lib/module/Auth.web.js +50 -10
  35. package/lib/module/Auth.web.js.map +1 -1
  36. package/lib/module/global.d.js.map +1 -1
  37. package/lib/module/index.js +1 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/index.web.js +2 -1
  40. package/lib/module/index.web.js.map +1 -1
  41. package/lib/module/service.js +36 -9
  42. package/lib/module/service.js.map +1 -1
  43. package/lib/module/service.web.js +65 -13
  44. package/lib/module/service.web.js.map +1 -1
  45. package/lib/module/ui/social-button.js +19 -14
  46. package/lib/module/ui/social-button.js.map +1 -1
  47. package/lib/module/ui/social-button.web.js +16 -10
  48. package/lib/module/ui/social-button.web.js.map +1 -1
  49. package/lib/module/use-auth.js +22 -25
  50. package/lib/module/use-auth.js.map +1 -1
  51. package/lib/module/utils/auth-error.js +30 -0
  52. package/lib/module/utils/auth-error.js.map +1 -0
  53. package/lib/module/utils/logger.js +1 -0
  54. package/lib/module/utils/logger.js.map +1 -1
  55. package/lib/typescript/commonjs/Auth.web.d.ts +5 -1
  56. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/index.d.ts +1 -0
  58. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/index.web.d.ts +2 -1
  60. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/service.web.d.ts +2 -18
  63. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/use-auth.d.ts +2 -1
  67. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
  69. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
  71. package/lib/typescript/module/Auth.web.d.ts +5 -1
  72. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  73. package/lib/typescript/module/index.d.ts +1 -0
  74. package/lib/typescript/module/index.d.ts.map +1 -1
  75. package/lib/typescript/module/index.web.d.ts +2 -1
  76. package/lib/typescript/module/index.web.d.ts.map +1 -1
  77. package/lib/typescript/module/service.d.ts.map +1 -1
  78. package/lib/typescript/module/service.web.d.ts +2 -18
  79. package/lib/typescript/module/service.web.d.ts.map +1 -1
  80. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  82. package/lib/typescript/module/use-auth.d.ts +2 -1
  83. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  84. package/lib/typescript/module/utils/auth-error.d.ts +16 -0
  85. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
  86. package/lib/typescript/module/utils/logger.d.ts.map +1 -1
  87. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
  88. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
  89. package/package.json +8 -10
  90. package/src/Auth.web.ts +77 -11
  91. package/src/global.d.ts +0 -11
  92. package/src/index.ts +5 -0
  93. package/src/index.web.ts +6 -1
  94. package/src/service.ts +37 -9
  95. package/src/service.web.ts +84 -15
  96. package/src/ui/social-button.tsx +21 -9
  97. package/src/ui/social-button.web.tsx +17 -4
  98. package/src/use-auth.ts +29 -67
  99. package/src/utils/auth-error.ts +49 -0
  100. 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),
@@ -114,37 +139,43 @@ public class AuthAdapter: NSObject {
114
139
  DispatchQueue.main.async {
115
140
  let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
116
141
  if let error = error {
117
- if (error as NSError).code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
142
+ let nsError = error as NSError
143
+ if nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
118
144
  completion(nil, "cancelled")
145
+ } else if nsError.domain.lowercased().contains("network") || nsError.code == NSURLErrorNotConnectedToInternet {
146
+ completion(nil, "network_error")
119
147
  } else {
120
- completion(nil, error.localizedDescription)
148
+ completion(nil, "unknown")
121
149
  }
122
150
  return
123
151
  }
124
-
152
+
125
153
  guard let callbackURL = callbackURL,
126
154
  let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false) else {
127
- completion(nil, "No response from Microsoft")
155
+ completion(nil, "unknown")
128
156
  return
129
157
  }
130
-
158
+
131
159
  var params: [String: String] = [:]
132
160
  for item in components.queryItems ?? [] {
133
161
  params[item.name] = item.value
134
162
  }
135
-
163
+
136
164
  if let errorCode = params["error"] {
137
- completion(nil, params["error_description"] ?? errorCode)
165
+ // OAuth error codes are already structured (e.g. "access_denied").
166
+ // Map well-known ones; fall back to "unknown".
167
+ let mapped = mapOAuthError(errorCode)
168
+ completion(nil, mapped)
138
169
  return
139
170
  }
140
-
171
+
141
172
  guard let returnedState = params["state"], returnedState == state else {
142
- completion(nil, "State mismatch - possible CSRF attack")
173
+ completion(nil, "invalid_state")
143
174
  return
144
175
  }
145
-
176
+
146
177
  guard let code = params["code"] else {
147
- completion(nil, "No authorization code in response")
178
+ completion(nil, "unknown")
148
179
  return
149
180
  }
150
181
 
@@ -162,12 +193,16 @@ public class AuthAdapter: NSObject {
162
193
  }
163
194
 
164
195
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
165
- let rootVC = windowScene.windows.first?.rootViewController else {
166
- 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")
167
199
  return
168
200
  }
169
-
170
- 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)
171
206
  session.presentationContextProvider = contextProvider
172
207
  objc_setAssociatedObject(session, &contextProviderHandle, contextProvider, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
173
208
  session.prefersEphemeralWebBrowserSession = false
@@ -175,17 +210,19 @@ public class AuthAdapter: NSObject {
175
210
  }
176
211
  }
177
212
 
178
- private static func generateCodeVerifier() -> String {
213
+ private static func generateCodeVerifier() -> String? {
179
214
  var bytes = [UInt8](repeating: 0, count: 32)
180
- _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
215
+ guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
216
+ return nil
217
+ }
181
218
  return Data(bytes).base64EncodedString()
182
219
  .replacingOccurrences(of: "+", with: "-")
183
220
  .replacingOccurrences(of: "/", with: "_")
184
221
  .replacingOccurrences(of: "=", with: "")
185
222
  }
186
223
 
187
- private static func generateCodeChallenge(_ verifier: String) -> String {
188
- 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 }
189
226
  var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
190
227
  data.withUnsafeBytes {
191
228
  _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
@@ -207,13 +244,16 @@ public class AuthAdapter: NSObject {
207
244
  scopes: [String],
208
245
  completion: @escaping (NSDictionary?, String?) -> Void
209
246
  ) {
210
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
211
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
212
-
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
+
213
253
  var request = URLRequest(url: tokenUrl)
214
254
  request.httpMethod = "POST"
215
255
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
216
-
256
+
217
257
  let bodyParams = [
218
258
  "client_id": clientId,
219
259
  "code": code,
@@ -230,42 +270,52 @@ public class AuthAdapter: NSObject {
230
270
  URLSession.shared.dataTask(with: request) { data, response, error in
231
271
  DispatchQueue.main.async {
232
272
  if let error = error {
233
- completion(nil, error.localizedDescription)
273
+ completion(nil, "network_error")
234
274
  return
235
275
  }
236
-
276
+
237
277
  guard let data = data,
238
278
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
239
- completion(nil, "Failed to parse token response")
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
+ }
240
284
  return
241
285
  }
242
-
286
+
243
287
  if let errorCode = json["error"] as? String {
244
- let desc = json["error_description"] as? String ?? errorCode
245
- completion(nil, desc)
288
+ completion(nil, mapOAuthError(errorCode))
246
289
  return
247
290
  }
248
-
291
+
292
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
293
+ completion(nil, "network_error")
294
+ return
295
+ }
296
+
249
297
  guard let idToken = json["id_token"] as? String else {
250
- completion(nil, "No id_token in token response")
298
+ completion(nil, "no_id_token")
251
299
  return
252
300
  }
253
-
301
+
254
302
  let claims = decodeJwt(idToken)
255
303
  guard claims["nonce"] == expectedNonce else {
256
- completion(nil, "Nonce mismatch - token may be replayed")
304
+ completion(nil, "invalid_nonce")
257
305
  return
258
306
  }
259
307
 
260
308
  let accessToken = json["access_token"] as? String ?? ""
261
309
  let refreshToken = json["refresh_token"] as? String ?? ""
262
- let expiresIn = json["expires_in"] as? Double ?? 0
310
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
263
311
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
264
312
 
313
+ tokenStoreLock.lock()
265
314
  if !refreshToken.isEmpty {
266
315
  inMemoryMicrosoftRefreshToken = refreshToken
267
316
  }
268
317
  inMemoryMicrosoftScopes = scopes.isEmpty ? defaultMicrosoftScopes : scopes
318
+ tokenStoreLock.unlock()
269
319
 
270
320
  let resultData: [String: Any] = [
271
321
  "provider": "microsoft",
@@ -311,7 +361,7 @@ public class AuthAdapter: NSObject {
311
361
 
312
362
  private static func handleGoogleResult(_ result: GIDSignInResult?, error: Error?, completion: @escaping (NSDictionary?, String?) -> Void) {
313
363
  if let error = error {
314
- completion(nil, error.localizedDescription)
364
+ completion(nil, mapError(error))
315
365
  return
316
366
  }
317
367
 
@@ -321,7 +371,9 @@ public class AuthAdapter: NSObject {
321
371
  }
322
372
 
323
373
  let serverAuthCode = result?.serverAuthCode ?? ""
374
+ tokenStoreLock.lock()
324
375
  inMemoryGoogleServerAuthCode = serverAuthCode.isEmpty ? nil : serverAuthCode
376
+ tokenStoreLock.unlock()
325
377
 
326
378
  let data: [String: Any] = [
327
379
  "provider": "google",
@@ -339,22 +391,49 @@ public class AuthAdapter: NSObject {
339
391
 
340
392
  static func mapError(_ error: Error) -> String {
341
393
  let nsError = error as NSError
394
+ if nsError.domain == NSURLErrorDomain {
395
+ return "network_error"
396
+ }
397
+ // GIDSignIn error codes
342
398
  if nsError.domain == "com.google.GIDSignIn" {
343
- if nsError.code == -5 { return "cancelled" }
399
+ switch nsError.code {
400
+ case -5: return "cancelled" // GIDSignInErrorCodeCanceled
401
+ case -4: return "not_signed_in" // GIDSignInErrorCodeNoCurrentUser
402
+ default: break
403
+ }
404
+ }
405
+ // ASAuthorizationError codes (Apple Sign-In / ASWebAuthenticationSession)
406
+ if nsError.domain == ASAuthorizationError.errorDomain {
407
+ switch nsError.code {
408
+ case ASAuthorizationError.canceled.rawValue: return "cancelled"
409
+ case ASAuthorizationError.invalidResponse.rawValue: return "configuration_error"
410
+ default: return "unknown"
411
+ }
344
412
  }
345
413
  let msg = error.localizedDescription.lowercased()
346
414
  if msg.contains("cancel") { return "cancelled" }
347
- if msg.contains("network") { return "network_error" }
415
+ if msg.contains("network") || msg.contains("internet") || msg.contains("offline") { return "network_error" }
348
416
  return "unknown"
349
417
  }
350
418
 
419
+ /// Maps OAuth 2.0 error codes (returned in query params or JSON) to AuthErrorCode values.
420
+ private static func mapOAuthError(_ oauthCode: String) -> String {
421
+ switch oauthCode {
422
+ case "access_denied": return "cancelled"
423
+ case "invalid_client", "unauthorized_client", "invalid_scope": return "configuration_error"
424
+ case "invalid_grant", "invalid_request": return "token_error"
425
+ case "temporarily_unavailable", "server_error": return "network_error"
426
+ default: return "unknown"
427
+ }
428
+ }
429
+
351
430
  @objc
352
431
  public static func addScopes(scopes: [String], completion: @escaping (NSDictionary?, String?) -> Void) {
353
432
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
354
433
  DispatchQueue.main.async {
355
434
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
356
435
  let rootVC = windowScene.windows.first?.rootViewController else {
357
- completion(nil, "No root view controller found")
436
+ completion(nil, "no_window")
358
437
  return
359
438
  }
360
439
  currentUser.addScopes(scopes, presenting: rootVC) { result, error in
@@ -363,11 +442,17 @@ public class AuthAdapter: NSObject {
363
442
  }
364
443
  return
365
444
  }
366
- guard inMemoryMicrosoftRefreshToken != nil else {
367
- 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")
368
451
  return
369
452
  }
370
- 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
+ }
371
456
  loginMicrosoft(scopes: mergedScopes, loginHint: nil, tenant: nil, prompt: nil, completion: completion)
372
457
  }
373
458
 
@@ -376,7 +461,7 @@ public class AuthAdapter: NSObject {
376
461
  if let currentUser = GIDSignIn.sharedInstance.currentUser {
377
462
  currentUser.refreshTokensIfNeeded { user, error in
378
463
  if let error = error {
379
- completion(nil, error.localizedDescription)
464
+ completion(nil, mapError(error))
380
465
  return
381
466
  }
382
467
  guard let user = user else {
@@ -401,6 +486,9 @@ public class AuthAdapter: NSObject {
401
486
  if Bundle.main.object(forInfoDictionaryKey: "GIDClientID") != nil {
402
487
  GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
403
488
  if let user = user {
489
+ tokenStoreLock.lock()
490
+ let cachedServerAuthCode = inMemoryGoogleServerAuthCode
491
+ tokenStoreLock.unlock()
404
492
  let data: [String: Any] = [
405
493
  "provider": "google",
406
494
  "email": user.profile?.email ?? "",
@@ -408,7 +496,7 @@ public class AuthAdapter: NSObject {
408
496
  "photo": user.profile?.imageURL(withDimension: 300)?.absoluteString ?? "",
409
497
  "idToken": user.idToken?.tokenString ?? "",
410
498
  "accessToken": user.accessToken.tokenString,
411
- "serverAuthCode": inMemoryGoogleServerAuthCode ?? "",
499
+ "serverAuthCode": cachedServerAuthCode ?? "",
412
500
  "expirationTime": (user.accessToken.expirationDate?.timeIntervalSince1970 ?? 0) * 1000
413
501
  ]
414
502
  completion(data as NSDictionary)
@@ -422,7 +510,10 @@ public class AuthAdapter: NSObject {
422
510
  }
423
511
 
424
512
  private static func tryMicrosoftSilentRefresh(completion: @escaping (NSDictionary?) -> Void) {
425
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
513
+ tokenStoreLock.lock()
514
+ let refreshToken = inMemoryMicrosoftRefreshToken
515
+ tokenStoreLock.unlock()
516
+ guard let refreshToken = refreshToken else {
426
517
  completion(nil)
427
518
  return
428
519
  }
@@ -434,13 +525,16 @@ public class AuthAdapter: NSObject {
434
525
 
435
526
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
436
527
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
437
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
438
- let tokenUrl = URL(string: "\(authBaseUrl)oauth2/v2.0/token")!
439
-
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
+
440
534
  var request = URLRequest(url: tokenUrl)
441
535
  request.httpMethod = "POST"
442
536
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
443
-
537
+
444
538
  let bodyParams = [
445
539
  "client_id": clientId,
446
540
  "grant_type": "refresh_token",
@@ -454,23 +548,42 @@ public class AuthAdapter: NSObject {
454
548
 
455
549
  URLSession.shared.dataTask(with: request) { data, response, error in
456
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
+ }
457
565
  guard let data = data,
458
566
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
459
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
460
571
  completion(nil)
461
572
  return
462
573
  }
463
-
574
+
464
575
  let claims = decodeJwt(idToken)
465
576
  let accessToken = json["access_token"] as? String ?? ""
466
577
  let newRefreshToken = json["refresh_token"] as? String ?? ""
467
- let expiresIn = json["expires_in"] as? Double ?? 0
578
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
468
579
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
469
-
580
+
581
+ tokenStoreLock.lock()
470
582
  if !newRefreshToken.isEmpty {
471
583
  inMemoryMicrosoftRefreshToken = newRefreshToken
472
584
  }
473
-
585
+ tokenStoreLock.unlock()
586
+
474
587
  let resultData: [String: Any] = [
475
588
  "provider": "microsoft",
476
589
  "email": claims["preferred_username"] ?? claims["email"] ?? "",
@@ -487,8 +600,11 @@ public class AuthAdapter: NSObject {
487
600
  }
488
601
 
489
602
  private static func tryMicrosoftRefreshForTokenRefresh(completion: @escaping (NSDictionary?, String?) -> Void) {
490
- guard let refreshToken = inMemoryMicrosoftRefreshToken else {
491
- 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")
492
608
  return
493
609
  }
494
610
  guard let clientId = Bundle.main.object(forInfoDictionaryKey: "MSALClientID") as? String, !clientId.isEmpty else {
@@ -497,8 +613,11 @@ public class AuthAdapter: NSObject {
497
613
  }
498
614
  let tenant = Bundle.main.object(forInfoDictionaryKey: "MSALTenant") as? String ?? "common"
499
615
  let b2cDomain = Bundle.main.object(forInfoDictionaryKey: "MSALB2cDomain") as? String
500
- let authBaseUrl = getMicrosoftAuthBaseUrl(tenant: tenant, b2cDomain: b2cDomain)
501
- 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
+ }
502
621
  var request = URLRequest(url: tokenUrl)
503
622
  request.httpMethod = "POST"
504
623
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
@@ -514,26 +633,36 @@ public class AuthAdapter: NSObject {
514
633
  URLSession.shared.dataTask(with: request) { data, response, error in
515
634
  DispatchQueue.main.async {
516
635
  if let error = error {
517
- completion(nil, error.localizedDescription)
636
+ completion(nil, "network_error")
518
637
  return
519
638
  }
520
639
  guard let data = data,
521
640
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
522
- completion(nil, "Token refresh failed")
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
+ }
523
646
  return
524
647
  }
525
648
  if let errorCode = json["error"] as? String {
526
- completion(nil, (json["error_description"] as? String) ?? errorCode)
649
+ completion(nil, AuthAdapter.mapOAuthError(errorCode))
650
+ return
651
+ }
652
+ if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
653
+ completion(nil, "network_error")
527
654
  return
528
655
  }
529
656
  let idToken = json["id_token"] as? String ?? ""
530
657
  let accessToken = json["access_token"] as? String ?? ""
531
658
  let newRefreshToken = json["refresh_token"] as? String ?? ""
532
- let expiresIn = json["expires_in"] as? Double ?? 0
659
+ let expiresIn = (json["expires_in"] as? Double).flatMap { $0 > 0 ? $0 : nil } ?? 3600.0
533
660
  let expirationTime = Date().timeIntervalSince1970 * 1000 + expiresIn * 1000
661
+ tokenStoreLock.lock()
534
662
  if !newRefreshToken.isEmpty {
535
663
  inMemoryMicrosoftRefreshToken = newRefreshToken
536
664
  }
665
+ tokenStoreLock.unlock()
537
666
  let tokensData: [String: Any] = [
538
667
  "accessToken": accessToken,
539
668
  "idToken": idToken,
@@ -545,24 +674,28 @@ public class AuthAdapter: NSObject {
545
674
  }.resume()
546
675
  }
547
676
 
548
- private static func getMicrosoftAuthBaseUrl(tenant: String, b2cDomain: String?) -> String {
549
- if tenant.hasPrefix("https://") {
550
- 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)/"
551
684
  }
552
-
553
- if let domain = b2cDomain {
554
- return "https://\(domain)/tfp/\(tenant)/"
555
- } else {
556
- return "https://login.microsoftonline.com/\(tenant)/"
685
+ if let domain = b2cDomain?.trimmingCharacters(in: .whitespacesAndNewlines), !domain.isEmpty {
686
+ return "https://\(domain)/tfp/\(trimmedTenant)/"
557
687
  }
688
+ return "https://login.microsoftonline.com/\(trimmedTenant)/"
558
689
  }
559
690
 
560
691
  @objc
561
692
  public static func logout() {
562
693
  GIDSignIn.sharedInstance.signOut()
694
+ tokenStoreLock.lock()
563
695
  inMemoryMicrosoftRefreshToken = nil
564
696
  inMemoryMicrosoftScopes = defaultMicrosoftScopes
565
697
  inMemoryGoogleServerAuthCode = nil
698
+ tokenStoreLock.unlock()
566
699
  }
567
700
  }
568
701
 
@@ -591,11 +724,25 @@ class AppleSignInDelegate: NSObject, ASAuthorizationControllerDelegate {
591
724
  "underlyingError": ""
592
725
  ]
593
726
  completion(data as NSDictionary, nil)
727
+ } else {
728
+ completion(nil, "unknown")
594
729
  }
595
730
  }
596
-
731
+
597
732
  func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
598
- completion(nil, error.localizedDescription)
733
+ completion(nil, AuthAdapter.mapError(error))
734
+ }
735
+ }
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
599
746
  }
600
747
  }
601
748
 
@@ -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"]);