react-native-oauth-essentials 1.0.12 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +384 -362
- package/android/src/main/java/com/oauthessentials/CredentialError.kt +4 -1
- package/android/src/main/java/com/oauthessentials/CredentialFactory.kt +24 -2
- package/android/src/main/java/com/oauthessentials/CustomTabsActivity.kt +14 -1
- package/android/src/main/java/com/oauthessentials/OauthEssentialsModule.kt +445 -187
- package/app.plugin.js +1 -0
- package/ios/AppleIdRetriever.swift +25 -26
- package/ios/CredentialError.swift +3 -0
- package/ios/CredentialFactory.swift +38 -20
- package/ios/CredentialsType.swift +1 -0
- package/ios/GoogleIdRetriever.swift +132 -56
- package/ios/HybridRetriever.swift +12 -18
- package/ios/OauthEssentials.h +0 -3
- package/ios/OauthEssentials.mm +45 -2
- package/ios/OauthEssentialsModule.swift +266 -57
- package/ios/PasskeyRetriever.swift +31 -49
- package/ios/PasswordRetriever.swift +10 -30
- package/ios/RCTPromiseSettler.swift +125 -0
- package/jest/index.js +149 -0
- package/lib/module/NativeOauthEssentials.js +10 -1
- package/lib/module/NativeOauthEssentials.js.map +1 -1
- package/lib/module/errors.js +38 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +432 -58
- package/lib/module/index.js.map +1 -1
- package/lib/module/oauth.js +97 -0
- package/lib/module/oauth.js.map +1 -0
- package/lib/module/platform.js +108 -0
- package/lib/module/platform.js.map +1 -0
- package/lib/module/types.js +46 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/NativeOauthEssentials.d.ts +70 -16
- package/lib/typescript/src/NativeOauthEssentials.d.ts.map +1 -1
- package/lib/typescript/src/errors.d.ts +18 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +235 -40
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/oauth.d.ts +32 -0
- package/lib/typescript/src/oauth.d.ts.map +1 -0
- package/lib/typescript/src/platform.d.ts +39 -0
- package/lib/typescript/src/platform.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +190 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/plugin/withOauthEssentials.js +291 -0
- package/src/NativeOauthEssentials.ts +91 -17
- package/src/errors.ts +49 -0
- package/src/index.tsx +587 -80
- package/src/oauth.ts +140 -0
- package/src/platform.ts +121 -0
- package/src/types.ts +200 -1
|
@@ -2,35 +2,57 @@ import Foundation
|
|
|
2
2
|
import React
|
|
3
3
|
import GoogleSignIn
|
|
4
4
|
import AuthenticationServices
|
|
5
|
+
import CryptoKit
|
|
5
6
|
import Security
|
|
6
7
|
import UIKit
|
|
7
8
|
|
|
9
|
+
/// TurboModule methods are called on the module queue. Every method hops to the main thread, so
|
|
10
|
+
/// the retrievers, the simultaneous call lock and all UIKit/AuthenticationServices calls run on main.
|
|
8
11
|
public class OauthEssentialsModule: NSObject {
|
|
9
12
|
|
|
10
13
|
public var onCredentialSuccess: ((NSDictionary) -> Void)?
|
|
11
14
|
public var onCredentialFailure: ((NSDictionary) -> Void)?
|
|
15
|
+
public var onAppleIdCredentialRevoked: ((NSDictionary) -> Void)?
|
|
12
16
|
|
|
13
17
|
private var _password_retriever: PasswordRetriever? = nil
|
|
14
18
|
private var _google_id_retriever: GoogleIdRetriever? = nil
|
|
15
19
|
private var _apple_id_retriever: AppleIdRetriever? = nil
|
|
16
20
|
private var _hybrid_retriever: HybridRetriever? = nil
|
|
17
21
|
private var _passkey_retriever: RCTPromiseSettler? = nil
|
|
22
|
+
private var _auth_session_retriever: AuthSessionRetriever? = nil
|
|
23
|
+
private var revokedObserver: NSObjectProtocol?
|
|
18
24
|
|
|
19
25
|
@objc
|
|
20
26
|
public init(
|
|
21
27
|
onCredentialSuccess: @escaping ((NSDictionary) -> Void),
|
|
22
|
-
onCredentialFailure: @escaping ((NSDictionary) -> Void)
|
|
28
|
+
onCredentialFailure: @escaping ((NSDictionary) -> Void),
|
|
29
|
+
onAppleIdCredentialRevoked: @escaping ((NSDictionary) -> Void)
|
|
23
30
|
) {
|
|
24
31
|
super.init()
|
|
25
32
|
|
|
26
33
|
self.onCredentialSuccess = onCredentialSuccess
|
|
27
34
|
self.onCredentialFailure = onCredentialFailure
|
|
35
|
+
self.onAppleIdCredentialRevoked = onAppleIdCredentialRevoked
|
|
36
|
+
|
|
37
|
+
self.revokedObserver = NotificationCenter.default.addObserver(
|
|
38
|
+
forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
|
|
39
|
+
object: nil,
|
|
40
|
+
queue: .main
|
|
41
|
+
) { [weak self] _ in
|
|
42
|
+
self?.onAppleIdCredentialRevoked?(["type": "REVOKED"])
|
|
43
|
+
}
|
|
28
44
|
}
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
if
|
|
32
|
-
|
|
46
|
+
deinit {
|
|
47
|
+
if let revokedObserver = revokedObserver {
|
|
48
|
+
NotificationCenter.default.removeObserver(revokedObserver)
|
|
33
49
|
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Returns true when the previous call of the same method already settled its promise.
|
|
53
|
+
private func ensurePreviousPromiseSettled(settler: RCTPromiseSettler?, reject: RCTPromiseRejectBlock) -> Bool {
|
|
54
|
+
guard let settler = settler else { return true }
|
|
55
|
+
if settler.settled() { return true }
|
|
34
56
|
|
|
35
57
|
reject(
|
|
36
58
|
CredentialError.simultaneousCallError.rawValue,
|
|
@@ -65,7 +87,8 @@ public class OauthEssentialsModule: NSObject {
|
|
|
65
87
|
"GOOGLE_PLAY_SERVICES_SUPPORTED": false, // Not applicable on iOS, it does not have Google Play Services
|
|
66
88
|
"APPLE_ID_SUPPORTED": appleIdSupported,
|
|
67
89
|
"HYBRID_SUPPORTED": passwordSupported || appleIdSupported,
|
|
68
|
-
"PASSKEYS_SUPPORTED": passkeySupported
|
|
90
|
+
"PASSKEYS_SUPPORTED": passkeySupported,
|
|
91
|
+
"OAUTH_REDIRECT_URI": "\((Bundle.main.bundleIdentifier ?? "app").lowercased())://oauth/callback"
|
|
69
92
|
]
|
|
70
93
|
}
|
|
71
94
|
|
|
@@ -76,11 +99,40 @@ public class OauthEssentialsModule: NSObject {
|
|
|
76
99
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
77
100
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
78
101
|
) {
|
|
79
|
-
|
|
80
|
-
self.
|
|
102
|
+
DispatchQueue.main.async {
|
|
103
|
+
if self.ensurePreviousPromiseSettled(settler: self._google_id_retriever, reject: reject) {
|
|
104
|
+
self._google_id_retriever = GoogleIdRetriever(clientId: clientId, options: options, silent: false, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@objc
|
|
110
|
+
public func googleSilentSignIn(
|
|
111
|
+
_ clientId: String,
|
|
112
|
+
options: NSDictionary,
|
|
113
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
114
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
115
|
+
) {
|
|
116
|
+
DispatchQueue.main.async {
|
|
117
|
+
if self.ensurePreviousPromiseSettled(settler: self._google_id_retriever, reject: reject) {
|
|
118
|
+
self._google_id_retriever = GoogleIdRetriever(clientId: clientId, options: options, silent: true, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
119
|
+
}
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
|
|
123
|
+
@objc
|
|
124
|
+
public func signOut(
|
|
125
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
126
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
127
|
+
) {
|
|
128
|
+
DispatchQueue.main.async {
|
|
129
|
+
GIDSignIn.sharedInstance.signOut()
|
|
130
|
+
resolve(true)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// iOS has no API to save a password without an associated domain: let the system offer it
|
|
135
|
+
/// through the text fields `textContentType` instead.
|
|
84
136
|
@objc
|
|
85
137
|
public func passwordSignIn(
|
|
86
138
|
_ username: String,
|
|
@@ -88,9 +140,7 @@ public class OauthEssentialsModule: NSObject {
|
|
|
88
140
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
89
141
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
90
142
|
) {
|
|
91
|
-
|
|
92
|
-
self._apple_id_retriever = AppleIdRetriever(resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
93
|
-
}
|
|
143
|
+
resolve(false)
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
@objc
|
|
@@ -98,9 +148,11 @@ public class OauthEssentialsModule: NSObject {
|
|
|
98
148
|
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
99
149
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
100
150
|
) {
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
DispatchQueue.main.async {
|
|
152
|
+
if self.ensurePreviousPromiseSettled(settler: self._password_retriever, reject: reject) {
|
|
153
|
+
self._password_retriever = PasswordRetriever(resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
103
154
|
}
|
|
155
|
+
}
|
|
104
156
|
}
|
|
105
157
|
|
|
106
158
|
@objc
|
|
@@ -109,8 +161,10 @@ public class OauthEssentialsModule: NSObject {
|
|
|
109
161
|
options: NSDictionary,
|
|
110
162
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
111
163
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
112
|
-
|
|
113
|
-
self.
|
|
164
|
+
DispatchQueue.main.async {
|
|
165
|
+
if self.ensurePreviousPromiseSettled(settler: self._hybrid_retriever, reject: reject) {
|
|
166
|
+
self._hybrid_retriever = HybridRetriever(clientId: clientId, options: options, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
167
|
+
}
|
|
114
168
|
}
|
|
115
169
|
}
|
|
116
170
|
|
|
@@ -119,6 +173,24 @@ public class OauthEssentialsModule: NSObject {
|
|
|
119
173
|
_ options: NSDictionary,
|
|
120
174
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
121
175
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
176
|
+
) {
|
|
177
|
+
performPassKey(options: options, registration: true, resolve: resolve, reject: reject)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@objc(getPassKey:resolver:rejecter:)
|
|
181
|
+
public func getPassKey(
|
|
182
|
+
_ options: NSDictionary,
|
|
183
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
184
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
185
|
+
) {
|
|
186
|
+
performPassKey(options: options, registration: false, resolve: resolve, reject: reject)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func performPassKey(
|
|
190
|
+
options: NSDictionary,
|
|
191
|
+
registration: Bool,
|
|
192
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
193
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
122
194
|
) {
|
|
123
195
|
guard #available(iOS 16.0, *) else {
|
|
124
196
|
reject(
|
|
@@ -129,69 +201,122 @@ public class OauthEssentialsModule: NSObject {
|
|
|
129
201
|
return
|
|
130
202
|
}
|
|
131
203
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
nil
|
|
138
|
-
)
|
|
139
|
-
return
|
|
204
|
+
DispatchQueue.main.async {
|
|
205
|
+
// A pending AutoFill request must not block an explicit passkey request.
|
|
206
|
+
if let pending = self._passkey_retriever as? PasskeyRetriever, pending.autoFill, !pending.settled() {
|
|
207
|
+
pending.cancel()
|
|
208
|
+
pending.succeed(CredentialFactory.fromCancelled())
|
|
140
209
|
}
|
|
141
210
|
|
|
142
|
-
self._passkey_retriever
|
|
143
|
-
requestJson
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
211
|
+
if self.ensurePreviousPromiseSettled(settler: self._passkey_retriever, reject: reject) {
|
|
212
|
+
guard let requestJson = options["requestJson"] as? String else {
|
|
213
|
+
reject(
|
|
214
|
+
CredentialError.invalidResult.rawValue,
|
|
215
|
+
"\"requestJson\" field is required",
|
|
216
|
+
nil
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
self._passkey_retriever = PasskeyRetriever(
|
|
222
|
+
requestJson: requestJson,
|
|
223
|
+
mode: registration ? .registration : .assertion,
|
|
224
|
+
autoFill: (options["mediation"] as? String) == "autofill",
|
|
225
|
+
resolve: resolve,
|
|
226
|
+
reject: reject,
|
|
227
|
+
emitEvent: self.emitEvent
|
|
228
|
+
)
|
|
229
|
+
}
|
|
149
230
|
}
|
|
150
231
|
}
|
|
151
232
|
|
|
152
|
-
@objc
|
|
153
|
-
public func
|
|
154
|
-
_
|
|
155
|
-
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
233
|
+
@objc
|
|
234
|
+
public func cancelPassKeyAutoFill(
|
|
235
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
156
236
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
157
237
|
) {
|
|
158
238
|
guard #available(iOS 16.0, *) else {
|
|
159
|
-
|
|
160
|
-
CredentialError.notSupportedError.rawValue,
|
|
161
|
-
"Passkeys require iOS 16.0 or later",
|
|
162
|
-
nil
|
|
163
|
-
)
|
|
239
|
+
resolve(false)
|
|
164
240
|
return
|
|
165
241
|
}
|
|
166
242
|
|
|
167
|
-
|
|
168
|
-
guard let
|
|
169
|
-
|
|
170
|
-
CredentialError.invalidResult.rawValue,
|
|
171
|
-
"\"requestJson\" field is required",
|
|
172
|
-
nil
|
|
173
|
-
)
|
|
243
|
+
DispatchQueue.main.async {
|
|
244
|
+
guard let pending = self._passkey_retriever as? PasskeyRetriever, pending.autoFill, !pending.settled() else {
|
|
245
|
+
resolve(false)
|
|
174
246
|
return
|
|
175
247
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
mode: .assertion,
|
|
180
|
-
resolve: resolve,
|
|
181
|
-
reject: reject,
|
|
182
|
-
emitEvent: emitEvent
|
|
183
|
-
)
|
|
248
|
+
pending.cancel()
|
|
249
|
+
pending.succeed(CredentialFactory.fromCancelled())
|
|
250
|
+
resolve(true)
|
|
184
251
|
}
|
|
185
252
|
}
|
|
186
253
|
|
|
187
|
-
@objc(appleSignIn:resolver:rejecter:)
|
|
254
|
+
@objc(appleSignIn:options:resolver:rejecter:)
|
|
188
255
|
public func appleSignIn(
|
|
189
|
-
_ webAndroidUrl: String
|
|
256
|
+
_ webAndroidUrl: String?,
|
|
257
|
+
options: NSDictionary,
|
|
190
258
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
191
259
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
192
260
|
) {
|
|
193
|
-
|
|
194
|
-
self.
|
|
261
|
+
DispatchQueue.main.async {
|
|
262
|
+
if self.ensurePreviousPromiseSettled(settler: self._apple_id_retriever, reject: reject) {
|
|
263
|
+
self._apple_id_retriever = AppleIdRetriever(options: options, resolve: resolve, reject: reject, emitEvent: self.emitEvent)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@objc(getAppleIdCredentialState:resolver:rejecter:)
|
|
269
|
+
public func getAppleIdCredentialState(
|
|
270
|
+
_ userId: String,
|
|
271
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
272
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
273
|
+
) {
|
|
274
|
+
ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userId) { state, error in
|
|
275
|
+
if let error = error {
|
|
276
|
+
reject(CredentialError.invalidResult.rawValue, error.localizedDescription, error)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
switch state {
|
|
280
|
+
case .authorized:
|
|
281
|
+
resolve("AUTHORIZED")
|
|
282
|
+
case .revoked:
|
|
283
|
+
resolve("REVOKED")
|
|
284
|
+
case .transferred:
|
|
285
|
+
resolve("TRANSFERRED")
|
|
286
|
+
default:
|
|
287
|
+
resolve("NOT_FOUND")
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@objc
|
|
293
|
+
public func generatePkce(
|
|
294
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
295
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
296
|
+
) {
|
|
297
|
+
guard let codeVerifier = Pkce.randomString(byteCount: 32),
|
|
298
|
+
let state = Pkce.randomString(byteCount: 16) else {
|
|
299
|
+
reject(CredentialError.invalidResult.rawValue, "Unable to generate random bytes", nil)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
resolve([
|
|
303
|
+
"codeVerifier": codeVerifier,
|
|
304
|
+
"codeChallenge": Pkce.challenge(for: codeVerifier),
|
|
305
|
+
"state": state
|
|
306
|
+
])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@objc(openAuthSession:options:resolver:rejecter:)
|
|
310
|
+
public func openAuthSession(
|
|
311
|
+
_ url: String,
|
|
312
|
+
options: NSDictionary,
|
|
313
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
314
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
315
|
+
) {
|
|
316
|
+
DispatchQueue.main.async {
|
|
317
|
+
if self.ensurePreviousPromiseSettled(settler: self._auth_session_retriever, reject: reject) {
|
|
318
|
+
self._auth_session_retriever = AuthSessionRetriever(url: url, options: options, resolve: resolve, reject: reject)
|
|
319
|
+
}
|
|
195
320
|
}
|
|
196
321
|
}
|
|
197
322
|
|
|
@@ -208,3 +333,87 @@ public class OauthEssentialsModule: NSObject {
|
|
|
208
333
|
}
|
|
209
334
|
}
|
|
210
335
|
}
|
|
336
|
+
|
|
337
|
+
enum Pkce {
|
|
338
|
+
static func randomString(byteCount: Int) -> String? {
|
|
339
|
+
var bytes = [UInt8](repeating: 0, count: byteCount)
|
|
340
|
+
guard SecRandomCopyBytes(kSecRandomDefault, byteCount, &bytes) == errSecSuccess else {
|
|
341
|
+
return nil
|
|
342
|
+
}
|
|
343
|
+
return Data(bytes).base64URLEncodedString()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
static func challenge(for verifier: String) -> String {
|
|
347
|
+
return Data(SHA256.hash(data: Data(verifier.utf8))).base64URLEncodedString()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// Opens an url in ASWebAuthenticationSession and resolves with the redirect url.
|
|
352
|
+
/// It does not emit events: oauthSignIn finishes the flow in JS.
|
|
353
|
+
public class AuthSessionRetriever: RCTPromiseSettler, ASWebAuthenticationPresentationContextProviding {
|
|
354
|
+
|
|
355
|
+
private var session: ASWebAuthenticationSession?
|
|
356
|
+
|
|
357
|
+
init(
|
|
358
|
+
url: String,
|
|
359
|
+
options: NSDictionary,
|
|
360
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
361
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
362
|
+
) {
|
|
363
|
+
super.init(resolve: resolve, reject: reject, emitEvent: nil)
|
|
364
|
+
|
|
365
|
+
guard let authUrl = URL(string: url),
|
|
366
|
+
let callbackScheme = options["callbackScheme"] as? String else {
|
|
367
|
+
fail(.invalidResult, "Invalid authorization url or redirect uri")
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
whenBrowserCanOpen("oauthSignIn") { _ in
|
|
371
|
+
let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { callbackURL, error in
|
|
372
|
+
self.session = nil
|
|
373
|
+
self.handle(callbackURL: callbackURL, error: error)
|
|
374
|
+
}
|
|
375
|
+
session.presentationContextProvider = self
|
|
376
|
+
session.prefersEphemeralWebBrowserSession = (options["prefersEphemeralSession"] as? Bool) ?? false
|
|
377
|
+
self.session = session
|
|
378
|
+
|
|
379
|
+
if !session.start() {
|
|
380
|
+
self.session = nil
|
|
381
|
+
self.fail(.browserUnavailable, "Unable to start the browser session")
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private func handle(callbackURL: URL?, error: Error?) {
|
|
387
|
+
if let error = error {
|
|
388
|
+
let nsError = error as NSError
|
|
389
|
+
if nsError.domain == ASWebAuthenticationSessionError.errorDomain {
|
|
390
|
+
switch nsError.code {
|
|
391
|
+
case ASWebAuthenticationSessionError.canceledLogin.rawValue:
|
|
392
|
+
succeed(CredentialFactory.fromCancelled())
|
|
393
|
+
return
|
|
394
|
+
case ASWebAuthenticationSessionError.presentationContextNotProvided.rawValue,
|
|
395
|
+
ASWebAuthenticationSessionError.presentationContextInvalid.rawValue:
|
|
396
|
+
fail(.browserUnavailable, error.localizedDescription, error)
|
|
397
|
+
return
|
|
398
|
+
default:
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
fail(.invalidResult, error.localizedDescription, error)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
guard let callbackURL = callbackURL else {
|
|
407
|
+
fail(.invalidResult, "No redirect url returned")
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
succeed([
|
|
411
|
+
"type": CredentialsType.redirect.rawValue,
|
|
412
|
+
"data": ["url": callbackURL.absoluteString]
|
|
413
|
+
])
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
417
|
+
return PresentationContext.anchor()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -20,15 +20,20 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
private let mode: Mode
|
|
23
|
+
/// AutoFill requests wait for the user to pick a passkey in the QuickType bar.
|
|
24
|
+
let autoFill: Bool
|
|
25
|
+
private var controller: ASAuthorizationController?
|
|
23
26
|
|
|
24
27
|
init(
|
|
25
28
|
requestJson: String,
|
|
26
29
|
mode: Mode,
|
|
30
|
+
autoFill: Bool,
|
|
27
31
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
28
32
|
reject: @escaping RCTPromiseRejectBlock,
|
|
29
33
|
emitEvent: ((String, NSDictionary) -> Void)?
|
|
30
34
|
) {
|
|
31
35
|
self.mode = mode
|
|
36
|
+
self.autoFill = autoFill && mode == .assertion
|
|
32
37
|
super.init(resolve: resolve, reject: reject, emitEvent: emitEvent)
|
|
33
38
|
|
|
34
39
|
do {
|
|
@@ -36,17 +41,22 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
36
41
|
let controller = ASAuthorizationController(authorizationRequests: [request])
|
|
37
42
|
controller.delegate = self
|
|
38
43
|
controller.presentationContextProvider = self
|
|
39
|
-
controller
|
|
44
|
+
self.controller = controller
|
|
45
|
+
if self.autoFill {
|
|
46
|
+
controller.performAutoFillAssistedRequests()
|
|
47
|
+
} else {
|
|
48
|
+
controller.performRequests(options: .preferImmediatelyAvailableCredentials)
|
|
49
|
+
}
|
|
40
50
|
} catch {
|
|
41
|
-
|
|
42
|
-
CredentialError.invalidResult.rawValue,
|
|
43
|
-
"Failed to parse requestJson: \(error.localizedDescription)",
|
|
44
|
-
error
|
|
45
|
-
)
|
|
46
|
-
cleanup()
|
|
51
|
+
fail(.invalidResult, "Failed to parse requestJson: \(error.localizedDescription)", error)
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/// Cancels the pending request, which resolves it as cancelled.
|
|
56
|
+
func cancel() {
|
|
57
|
+
controller?.cancel()
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
private func buildRequest(requestJson: String, mode: Mode) throws -> ASAuthorizationRequest {
|
|
51
61
|
guard let data = requestJson.data(using: .utf8),
|
|
52
62
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
@@ -65,7 +75,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
65
75
|
|
|
66
76
|
private func buildRegistrationRequest(json: [String: Any]) throws -> ASAuthorizationRequest {
|
|
67
77
|
guard let challengeStr = json["challenge"] as? String,
|
|
68
|
-
let challenge =
|
|
78
|
+
let challenge = Data(base64URLEncoded: challengeStr) else {
|
|
69
79
|
throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
|
|
70
80
|
NSLocalizedDescriptionKey: "Missing or invalid challenge"
|
|
71
81
|
])
|
|
@@ -80,7 +90,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
80
90
|
|
|
81
91
|
guard let user = json["user"] as? [String: Any],
|
|
82
92
|
let userIdStr = user["id"] as? String,
|
|
83
|
-
let userId =
|
|
93
|
+
let userId = Data(base64URLEncoded: userIdStr),
|
|
84
94
|
let userName = user["name"] as? String else {
|
|
85
95
|
throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
|
|
86
96
|
NSLocalizedDescriptionKey: "Missing or invalid user fields"
|
|
@@ -107,7 +117,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
107
117
|
if #available(iOS 17.4, *) {
|
|
108
118
|
request.excludedCredentials = excludeCredentials.compactMap { cred in
|
|
109
119
|
guard let idStr = cred["id"] as? String,
|
|
110
|
-
let idData =
|
|
120
|
+
let idData = Data(base64URLEncoded: idStr) else { return nil }
|
|
111
121
|
return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: idData)
|
|
112
122
|
}
|
|
113
123
|
} else {
|
|
@@ -120,7 +130,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
120
130
|
|
|
121
131
|
private func buildAssertionRequest(json: [String: Any]) throws -> ASAuthorizationRequest {
|
|
122
132
|
guard let challengeStr = json["challenge"] as? String,
|
|
123
|
-
let challenge =
|
|
133
|
+
let challenge = Data(base64URLEncoded: challengeStr) else {
|
|
124
134
|
throw NSError(domain: "PasskeyRetriever", code: -1, userInfo: [
|
|
125
135
|
NSLocalizedDescriptionKey: "Missing or invalid challenge"
|
|
126
136
|
])
|
|
@@ -134,7 +144,7 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
134
144
|
if let allowCredentials = json["allowCredentials"] as? [[String: Any]] {
|
|
135
145
|
request.allowedCredentials = allowCredentials.compactMap { cred in
|
|
136
146
|
guard let idStr = cred["id"] as? String,
|
|
137
|
-
let idData =
|
|
147
|
+
let idData = Data(base64URLEncoded: idStr) else { return nil }
|
|
138
148
|
return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: idData)
|
|
139
149
|
}
|
|
140
150
|
}
|
|
@@ -152,66 +162,38 @@ public class PasskeyRetriever: RCTPromiseSettler,
|
|
|
152
162
|
controller: ASAuthorizationController,
|
|
153
163
|
didCompleteWithAuthorization authorization: ASAuthorization
|
|
154
164
|
) {
|
|
165
|
+
self.controller = nil
|
|
155
166
|
switch mode {
|
|
156
167
|
case .registration:
|
|
157
168
|
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
self.emitEvent?("onCredentialSuccess", result as NSDictionary)
|
|
169
|
+
succeed(CredentialFactory.fromPasskeyRegistration(credential: credential))
|
|
170
|
+
return
|
|
161
171
|
}
|
|
162
172
|
case .assertion:
|
|
163
173
|
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
self.emitEvent?("onCredentialSuccess", result as NSDictionary)
|
|
174
|
+
succeed(CredentialFactory.fromPasskeyAssertion(credential: credential))
|
|
175
|
+
return
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
|
-
|
|
178
|
+
fail(.invalidResult, "Unexpected credential type")
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
public func authorizationController(
|
|
173
182
|
controller: ASAuthorizationController,
|
|
174
183
|
didCompleteWithError error: Error
|
|
175
184
|
) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
nsError.code == ASAuthorizationError.canceled.rawValue {
|
|
179
|
-
let result = CredentialFactory.fromCancelled()
|
|
180
|
-
self.resolve?(result)
|
|
181
|
-
self.emitEvent?("onCredentialSuccess", result as NSDictionary)
|
|
182
|
-
} else {
|
|
183
|
-
self.reject?(
|
|
184
|
-
CredentialError.invalidResult.rawValue,
|
|
185
|
-
error.localizedDescription,
|
|
186
|
-
error
|
|
187
|
-
)
|
|
188
|
-
self.emitEvent?("onCredentialFailure", [
|
|
189
|
-
"code": CredentialError.invalidResult.rawValue,
|
|
190
|
-
"message": error.localizedDescription
|
|
191
|
-
])
|
|
192
|
-
}
|
|
193
|
-
cleanup()
|
|
185
|
+
self.controller = nil
|
|
186
|
+
finish(authorizationError: error)
|
|
194
187
|
}
|
|
195
188
|
|
|
196
189
|
// MARK: - ASAuthorizationControllerPresentationContextProviding
|
|
197
190
|
|
|
198
191
|
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
|
199
|
-
return
|
|
192
|
+
return PresentationContext.anchor()
|
|
200
193
|
}
|
|
201
194
|
|
|
202
195
|
// MARK: - Helpers
|
|
203
196
|
|
|
204
|
-
private func base64URLDecode(_ string: String) -> Data? {
|
|
205
|
-
var base64 = string
|
|
206
|
-
.replacingOccurrences(of: "-", with: "+")
|
|
207
|
-
.replacingOccurrences(of: "_", with: "/")
|
|
208
|
-
let remainder = base64.count % 4
|
|
209
|
-
if remainder > 0 {
|
|
210
|
-
base64.append(String(repeating: "=", count: 4 - remainder))
|
|
211
|
-
}
|
|
212
|
-
return Data(base64Encoded: base64)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
197
|
private func mapUserVerification(_ value: String) -> ASAuthorizationPublicKeyCredentialUserVerificationPreference {
|
|
216
198
|
switch value {
|
|
217
199
|
case "required":
|
|
@@ -5,6 +5,8 @@ public class PasswordRetriever: RCTPromiseSettler,
|
|
|
5
5
|
ASAuthorizationControllerDelegate,
|
|
6
6
|
ASAuthorizationControllerPresentationContextProviding {
|
|
7
7
|
|
|
8
|
+
private var controller: ASAuthorizationController?
|
|
9
|
+
|
|
8
10
|
override init(
|
|
9
11
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
10
12
|
reject: @escaping RCTPromiseRejectBlock,
|
|
@@ -16,6 +18,7 @@ public class PasswordRetriever: RCTPromiseSettler,
|
|
|
16
18
|
let controller = ASAuthorizationController(authorizationRequests: [request])
|
|
17
19
|
controller.delegate = self
|
|
18
20
|
controller.presentationContextProvider = self
|
|
21
|
+
self.controller = controller
|
|
19
22
|
if #available(iOS 16.0, *) {
|
|
20
23
|
controller.performRequests(options: .preferImmediatelyAvailableCredentials)
|
|
21
24
|
} else {
|
|
@@ -27,12 +30,11 @@ public class PasswordRetriever: RCTPromiseSettler,
|
|
|
27
30
|
controller: ASAuthorizationController,
|
|
28
31
|
didCompleteWithAuthorization authorization: ASAuthorization
|
|
29
32
|
) {
|
|
33
|
+
self.controller = nil
|
|
30
34
|
if let credential = authorization.credential as? ASPasswordCredential {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
cleanup()
|
|
35
|
+
succeed(CredentialFactory.fromPassword(credential: credential))
|
|
36
|
+
} else {
|
|
37
|
+
fail(.invalidResult, "Unexpected credential type")
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -40,33 +42,11 @@ public class PasswordRetriever: RCTPromiseSettler,
|
|
|
40
42
|
controller: ASAuthorizationController,
|
|
41
43
|
didCompleteWithError error: Error
|
|
42
44
|
) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
nsError.code == ASAuthorizationError.canceled.rawValue {
|
|
46
|
-
|
|
47
|
-
let result = CredentialFactory.fromCancelled()
|
|
48
|
-
self.resolve?(result)
|
|
49
|
-
self.emitEvent?("onCredentialSuccess", result as NSDictionary)
|
|
50
|
-
} else {
|
|
51
|
-
self.reject?(
|
|
52
|
-
CredentialError.invalidResult.rawValue,
|
|
53
|
-
error.localizedDescription,
|
|
54
|
-
error
|
|
55
|
-
)
|
|
56
|
-
self.emitEvent?("onCredentialFailure", [
|
|
57
|
-
"code": CredentialError.invalidResult.rawValue,
|
|
58
|
-
"message": error.localizedDescription
|
|
59
|
-
])
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
self.cleanup()
|
|
45
|
+
self.controller = nil
|
|
46
|
+
finish(authorizationError: error)
|
|
63
47
|
}
|
|
64
48
|
|
|
65
49
|
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func getIsSettled() -> Bool {
|
|
70
|
-
return resolve == nil || reject == nil
|
|
50
|
+
return PresentationContext.anchor()
|
|
71
51
|
}
|
|
72
52
|
}
|