Skip to content

Commit 839bbb2

Browse files
committed
Adding rCE support for phone auth flows. (#14047)
1 parent cf30f5a commit 839bbb2

File tree

9 files changed

+608
-221
lines changed

9 files changed

+608
-221
lines changed

FirebaseAuth/Sources/Swift/Auth/Auth.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2293,7 +2293,7 @@ extension Auth: AuthInterop {
22932293
action: AuthRecaptchaAction) async throws -> T
22942294
.Response {
22952295
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self)
2296-
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) {
2296+
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) != .off {
22972297
try await recaptchaVerifier.injectRecaptchaFields(request: request,
22982298
provider: AuthRecaptchaProvider.password,
22992299
action: action)

FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift

Lines changed: 183 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -72,23 +72,19 @@ import Foundation
7272
uiDelegate: AuthUIDelegate? = nil,
7373
multiFactorSession: MultiFactorSession? = nil,
7474
completion: ((_: String?, _: Error?) -> Void)?) {
75-
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
76-
urlTypes: auth.mainBundleUrlTypes) else {
77-
fatalError(
78-
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
79-
)
80-
}
81-
kAuthGlobalWorkQueue.async {
82-
Task {
83-
do {
84-
let verificationID = try await self.internalVerify(
85-
phoneNumber: phoneNumber,
86-
uiDelegate: uiDelegate,
87-
multiFactorSession: multiFactorSession
88-
)
89-
Auth.wrapMainAsync(callback: completion, withParam: verificationID, error: nil)
90-
} catch {
91-
Auth.wrapMainAsync(callback: completion, withParam: nil, error: error)
75+
Task {
76+
do {
77+
let verificationID = try await verifyPhoneNumber(
78+
phoneNumber,
79+
uiDelegate: uiDelegate,
80+
multiFactorSession: multiFactorSession
81+
)
82+
await MainActor.run {
83+
completion?(verificationID, nil)
84+
}
85+
} catch {
86+
await MainActor.run {
87+
completion?(nil, error)
9288
}
9389
}
9490
}
@@ -107,16 +103,19 @@ import Foundation
107103
uiDelegate: AuthUIDelegate? = nil,
108104
multiFactorSession: MultiFactorSession? = nil) async throws
109105
-> String {
110-
return try await withCheckedThrowingContinuation { continuation in
111-
self.verifyPhoneNumber(phoneNumber,
112-
uiDelegate: uiDelegate,
113-
multiFactorSession: multiFactorSession) { result, error in
114-
if let error {
115-
continuation.resume(throwing: error)
116-
} else if let result {
117-
continuation.resume(returning: result)
118-
}
119-
}
106+
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
107+
urlTypes: auth.mainBundleUrlTypes) else {
108+
fatalError(
109+
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
110+
)
111+
}
112+
113+
if let verificationID = try await internalVerify(phoneNumber: phoneNumber,
114+
uiDelegate: uiDelegate,
115+
multiFactorSession: multiFactorSession) {
116+
return verificationID
117+
} else {
118+
throw AuthErrorUtils.invalidVerificationIDError(message: "Invalid verification ID")
120119
}
121120
}
122121

@@ -133,11 +132,22 @@ import Foundation
133132
uiDelegate: AuthUIDelegate? = nil,
134133
multiFactorSession: MultiFactorSession?,
135134
completion: ((_: String?, _: Error?) -> Void)?) {
136-
multiFactorSession?.multiFactorInfo = multiFactorInfo
137-
verifyPhoneNumber(multiFactorInfo.phoneNumber,
138-
uiDelegate: uiDelegate,
139-
multiFactorSession: multiFactorSession,
140-
completion: completion)
135+
Task {
136+
do {
137+
let verificationID = try await verifyPhoneNumber(
138+
with: multiFactorInfo,
139+
uiDelegate: uiDelegate,
140+
multiFactorSession: multiFactorSession
141+
)
142+
await MainActor.run {
143+
completion?(verificationID, nil)
144+
}
145+
} catch {
146+
await MainActor.run {
147+
completion?(nil, error)
148+
}
149+
}
150+
}
141151
}
142152

143153
/// Verify ownership of the second factor phone number by the current user.
@@ -152,17 +162,10 @@ import Foundation
152162
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
153163
uiDelegate: AuthUIDelegate? = nil,
154164
multiFactorSession: MultiFactorSession?) async throws -> String {
155-
return try await withCheckedThrowingContinuation { continuation in
156-
self.verifyPhoneNumber(with: multiFactorInfo,
157-
uiDelegate: uiDelegate,
158-
multiFactorSession: multiFactorSession) { result, error in
159-
if let error {
160-
continuation.resume(throwing: error)
161-
} else if let result {
162-
continuation.resume(returning: result)
163-
}
164-
}
165-
}
165+
multiFactorSession?.multiFactorInfo = multiFactorInfo
166+
return try await verifyPhoneNumber(multiFactorInfo.phoneNumber,
167+
uiDelegate: uiDelegate,
168+
multiFactorSession: multiFactorSession)
166169
}
167170

168171
/// Creates an `AuthCredential` for the phone number provider identified by the
@@ -185,7 +188,7 @@ import Foundation
185188
uiDelegate: AuthUIDelegate?,
186189
multiFactorSession: MultiFactorSession? = nil) async throws
187190
-> String? {
188-
guard phoneNumber.count > 0 else {
191+
guard !phoneNumber.isEmpty else {
189192
throw AuthErrorUtils.missingPhoneNumberError(message: nil)
190193
}
191194
guard let manager = auth.notificationManager else {
@@ -194,37 +197,155 @@ import Foundation
194197
guard await manager.checkNotificationForwarding() else {
195198
throw AuthErrorUtils.notificationNotForwardedError()
196199
}
197-
return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
198-
retryOnInvalidAppCredential: true,
199-
multiFactorSession: multiFactorSession,
200-
uiDelegate: uiDelegate)
200+
201+
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
202+
try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: false)
203+
204+
switch recaptchaVerifier.enablementStatus(forProvider: .phone) {
205+
case .off:
206+
return try await verifyClAndSendVerificationCode(
207+
toPhoneNumber: phoneNumber,
208+
retryOnInvalidAppCredential: true,
209+
multiFactorSession: multiFactorSession,
210+
uiDelegate: uiDelegate
211+
)
212+
case .audit:
213+
return try await verifyClAndSendVerificationCodeWithRecaptcha(
214+
toPhoneNumber: phoneNumber,
215+
retryOnInvalidAppCredential: true,
216+
multiFactorSession: multiFactorSession,
217+
uiDelegate: uiDelegate,
218+
recaptchaVerifier: recaptchaVerifier
219+
)
220+
case .enforce:
221+
return try await verifyClAndSendVerificationCodeWithRecaptcha(
222+
toPhoneNumber: phoneNumber,
223+
retryOnInvalidAppCredential: false,
224+
multiFactorSession: multiFactorSession,
225+
uiDelegate: uiDelegate,
226+
recaptchaVerifier: recaptchaVerifier
227+
)
228+
}
229+
}
230+
231+
func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
232+
retryOnInvalidAppCredential: Bool,
233+
uiDelegate: AuthUIDelegate?,
234+
recaptchaVerifier: AuthRecaptchaVerifier) async throws
235+
-> String? {
236+
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
237+
codeIdentity: CodeIdentity.empty,
238+
requestConfiguration: auth
239+
.requestConfiguration)
240+
do {
241+
try await recaptchaVerifier.injectRecaptchaFields(
242+
request: request,
243+
provider: .phone,
244+
action: .sendVerificationCode
245+
)
246+
let response = try await AuthBackend.call(with: request)
247+
return response.verificationID
248+
} catch {
249+
return try await handleVerifyErrorWithRetry(error: error,
250+
phoneNumber: phoneNumber,
251+
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
252+
multiFactorSession: nil,
253+
uiDelegate: uiDelegate)
254+
}
201255
}
202256

203257
/// Starts the flow to verify the client via silent push notification.
204-
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
258+
/// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an
205259
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
206260
/// - Parameter phoneNumber: The phone number to be verified.
207261
/// - Parameter callback: The callback to be invoked on the global work queue when the flow is
208262
/// finished.
209-
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
210-
retryOnInvalidAppCredential: Bool,
211-
uiDelegate: AuthUIDelegate?) async throws
263+
func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
264+
retryOnInvalidAppCredential: Bool,
265+
uiDelegate: AuthUIDelegate?) async throws
212266
-> String? {
213267
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
214268
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
215269
codeIdentity: codeIdentity,
216270
requestConfiguration: auth
217271
.requestConfiguration)
218-
219272
do {
220-
let response = try await auth.backend.call(with: request)
273+
let response = try await AuthBackend.call(with: request)
221274
return response.verificationID
222275
} catch {
223-
return try await handleVerifyErrorWithRetry(error: error,
224-
phoneNumber: phoneNumber,
225-
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
226-
multiFactorSession: nil,
227-
uiDelegate: uiDelegate)
276+
return try await handleVerifyErrorWithRetry(
277+
error: error,
278+
phoneNumber: phoneNumber,
279+
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
280+
multiFactorSession: nil,
281+
uiDelegate: uiDelegate
282+
)
283+
}
284+
}
285+
286+
/// Starts the flow to verify the client via silent push notification.
287+
/// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an
288+
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
289+
/// - Parameter phoneNumber: The phone number to be verified.
290+
private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
291+
retryOnInvalidAppCredential: Bool,
292+
multiFactorSession session: MultiFactorSession?,
293+
uiDelegate: AuthUIDelegate?,
294+
recaptchaVerifier: AuthRecaptchaVerifier) async throws
295+
-> String? {
296+
if let settings = auth.settings,
297+
settings.isAppVerificationDisabledForTesting {
298+
let request = SendVerificationCodeRequest(
299+
phoneNumber: phoneNumber,
300+
codeIdentity: CodeIdentity.empty,
301+
requestConfiguration: auth.requestConfiguration
302+
)
303+
let response = try await AuthBackend.call(with: request)
304+
return response.verificationID
305+
}
306+
guard let session else {
307+
return try await verifyClAndSendVerificationCodeWithRecaptcha(
308+
toPhoneNumber: phoneNumber,
309+
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
310+
uiDelegate: uiDelegate,
311+
recaptchaVerifier: recaptchaVerifier
312+
)
313+
}
314+
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
315+
codeIdentity: CodeIdentity.empty)
316+
do {
317+
if let idToken = session.idToken {
318+
let request = StartMFAEnrollmentRequest(idToken: idToken,
319+
enrollmentInfo: startMFARequestInfo,
320+
requestConfiguration: auth.requestConfiguration)
321+
try await recaptchaVerifier.injectRecaptchaFields(
322+
request: request,
323+
provider: .phone,
324+
action: .startMfaEnrollment
325+
)
326+
let response = try await AuthBackend.call(with: request)
327+
return response.phoneSessionInfo?.sessionInfo
328+
} else {
329+
let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
330+
MFAEnrollmentID: session.multiFactorInfo?.uid,
331+
signInInfo: startMFARequestInfo,
332+
requestConfiguration: auth.requestConfiguration)
333+
try await recaptchaVerifier.injectRecaptchaFields(
334+
request: request,
335+
provider: .phone,
336+
action: .startMfaSignin
337+
)
338+
let response = try await AuthBackend.call(with: request)
339+
return response.responseInfo?.sessionInfo
340+
}
341+
} catch {
342+
return try await handleVerifyErrorWithRetry(
343+
error: error,
344+
phoneNumber: phoneNumber,
345+
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
346+
multiFactorSession: session,
347+
uiDelegate: uiDelegate
348+
)
228349
}
229350
}
230351

@@ -474,8 +595,9 @@ import Foundation
474595
private let auth: Auth
475596
private let callbackScheme: String
476597
private let usingClientIDScheme: Bool
598+
private var recaptchaVerifier: AuthRecaptchaVerifier?
477599

478-
init(auth: Auth) {
600+
init(auth: Auth, recaptchaVerifier: AuthRecaptchaVerifier? = nil) {
479601
self.auth = auth
480602
if let clientID = auth.app?.options.clientID {
481603
let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed()
@@ -494,6 +616,7 @@ import Foundation
494616
return
495617
}
496618
callbackScheme = ""
619+
self.recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
497620
}
498621

499622
private let kAuthTypeVerifyApp = "verifyApp"

FirebaseAuth/Sources/Swift/Backend/RPC/SendVerificationTokenRequest.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ private let kSecretKey = "iosSecret"
2929
/// The key for the reCAPTCHAToken parameter in the request.
3030
private let kreCAPTCHATokenKey = "recaptchaToken"
3131

32+
/// The key for the "clientType" value in the request.
33+
private let kClientType = "clientType"
34+
35+
/// The key for the "captchaResponse" value in the request.
36+
private let kCaptchaResponseKey = "captchaResponse"
37+
38+
/// The key for the "recaptchaVersion" value in the request.
39+
private let kRecaptchaVersion = "recaptchaVersion"
40+
3241
/// The key for the tenant id value in the request.
3342
private let kTenantIDKey = "tenantId"
3443

3544
/// A verification code can be an appCredential or a reCaptcha Token
36-
enum CodeIdentity {
45+
enum CodeIdentity: Equatable {
3746
case credential(AuthAppCredential)
3847
case recaptcha(String)
3948
case empty
@@ -50,6 +59,12 @@ class SendVerificationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
5059
/// verification code.
5160
let codeIdentity: CodeIdentity
5261

62+
/// Response to the captcha.
63+
var captchaResponse: String?
64+
65+
/// The reCAPTCHA version.
66+
var recaptchaVersion: String?
67+
5368
init(phoneNumber: String, codeIdentity: CodeIdentity,
5469
requestConfiguration: AuthRequestConfiguration) {
5570
self.phoneNumber = phoneNumber
@@ -71,10 +86,21 @@ class SendVerificationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
7186
postBody[kreCAPTCHATokenKey] = reCAPTCHAToken
7287
case .empty: break
7388
}
74-
89+
if let captchaResponse {
90+
postBody[kCaptchaResponseKey] = captchaResponse
91+
}
92+
if let recaptchaVersion {
93+
postBody[kRecaptchaVersion] = recaptchaVersion
94+
}
7595
if let tenantID {
7696
postBody[kTenantIDKey] = tenantID
7797
}
98+
postBody[kClientType] = clientType
7899
return postBody
79100
}
101+
102+
func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) {
103+
captchaResponse = recaptchaResponse
104+
self.recaptchaVersion = recaptchaVersion
105+
}
80106
}

FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ class AuthErrorUtils {
203203
error(code: .missingAndroidPackageName, message: message)
204204
}
205205

206+
static func invalidRecaptchaTokenError() -> Error {
207+
error(code: .invalidRecaptchaToken)
208+
}
209+
206210
static func unauthorizedDomainError(message: String?) -> Error {
207211
error(code: .unauthorizedDomain, message: message)
208212
}

0 commit comments

Comments
 (0)