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.
- package/README.md +82 -47
- package/android/proguard-rules.pro +7 -1
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/cpp/JniOnLoad.cpp +3 -1
- package/android/src/main/cpp/PlatformAuth+Android.cpp +271 -78
- package/android/src/main/java/com/auth/AuthAdapter.kt +293 -238
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +9 -5
- package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
- package/cpp/HybridAuth.cpp +79 -64
- package/cpp/HybridAuth.hpp +9 -7
- package/cpp/JSONSerializer.hpp +3 -0
- package/ios/AuthAdapter.swift +226 -79
- package/ios/PlatformAuth+iOS.mm +10 -3
- package/lib/commonjs/Auth.web.js +50 -10
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +23 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +30 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/service.js +36 -9
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +65 -13
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +19 -14
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +16 -10
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +22 -25
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/auth-error.js +37 -0
- package/lib/commonjs/utils/auth-error.js.map +1 -0
- package/lib/commonjs/utils/logger.js +1 -0
- package/lib/commonjs/utils/logger.js.map +1 -1
- package/lib/module/Auth.web.js +50 -10
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/global.d.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/service.js +36 -9
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +65 -13
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +19 -14
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +16 -10
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js +22 -25
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/auth-error.js +30 -0
- package/lib/module/utils/auth-error.js.map +1 -0
- package/lib/module/utils/logger.js +1 -0
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +5 -1
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +2 -18
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +2 -1
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
- package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +5 -1
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +2 -18
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +2 -1
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/auth-error.d.ts +16 -0
- package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
- package/package.json +8 -10
- package/src/Auth.web.ts +77 -11
- package/src/global.d.ts +0 -11
- package/src/index.ts +5 -0
- package/src/index.web.ts +6 -1
- package/src/service.ts +37 -9
- package/src/service.web.ts +84 -15
- package/src/ui/social-button.tsx +21 -9
- package/src/ui/social-button.web.tsx +17 -4
- package/src/use-auth.ts +29 -67
- package/src/utils/auth-error.ts +49 -0
- package/src/utils/logger.ts +1 -0
package/ios/AuthAdapter.swift
CHANGED
|
@@ -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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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, "
|
|
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
|
-
|
|
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, "
|
|
173
|
+
completion(nil, "invalid_state")
|
|
143
174
|
return
|
|
144
175
|
}
|
|
145
|
-
|
|
176
|
+
|
|
146
177
|
guard let code = params["code"] else {
|
|
147
|
-
completion(nil, "
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
367
|
-
|
|
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 =
|
|
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
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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, (
|
|
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
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/ios/PlatformAuth+iOS.mm
CHANGED
|
@@ -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
|
-
|
|
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"]);
|