From 4720f42d205ede694554765ea550adf99d66047e Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 20 Nov 2024 15:18:58 -0800 Subject: [PATCH 1/3] fix Audit mode fallback failure --- .../AuthProvider/PhoneAuthProvider.swift | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index b975d62f3a3..ebb99fe90f5 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -22,6 +22,10 @@ import Foundation @objc(FIRPhoneAuthProvider) open class PhoneAuthProvider: NSObject { /// A string constant identifying the phone identity provider. @objc public static let id = "phone" + @objc private static let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" + @objc private static let kClientType = "CLIENT_TYPE_IOS" + @objc private static let kFakeCaptchaResponse = "NO_RECAPTCHA" + #if os(iOS) /// Returns an instance of `PhoneAuthProvider` for the default `Auth` object. @objc(provider) open class func provider() -> PhoneAuthProvider { @@ -250,7 +254,8 @@ import Foundation phoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, multiFactorSession: nil, - uiDelegate: uiDelegate) + uiDelegate: uiDelegate, + auditFallback: true) } } @@ -262,13 +267,18 @@ import Foundation /// finished. func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, - uiDelegate: AuthUIDelegate?) async throws + uiDelegate: AuthUIDelegate?, + auditFallback: Bool = false) async throws -> String? { let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate) let request = SendVerificationCodeRequest(phoneNumber: phoneNumber, codeIdentity: codeIdentity, requestConfiguration: auth .requestConfiguration) + if auditFallback { + request.injectRecaptchaFields(recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion) + } + //TODO inject fake_token when .audit do { let response = try await auth.backend.call(with: request) return response.verificationID @@ -278,12 +288,13 @@ import Foundation phoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, multiFactorSession: nil, - uiDelegate: uiDelegate + uiDelegate: uiDelegate, + auditFallback: auditFallback ) } } - /// Starts the flow to verify the client via silent push notification. + /// Starts the flow to verify the client via silent push notification. This is used in both .Audit and .Enforce mode /// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an /// AuthErrorCodeInvalidAppCredential error is returned from the backend. /// - Parameter phoneNumber: The phone number to be verified. @@ -339,24 +350,28 @@ import Foundation return response.responseInfo?.sessionInfo } } catch { + // For Audit fallback only after rCE check failed return try await handleVerifyErrorWithRetry( error: error, phoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, multiFactorSession: session, - uiDelegate: uiDelegate + uiDelegate: uiDelegate, + auditFallback: true ) } } /// Starts the flow to verify the client via silent push notification. + /// This method is called in Audit fallback flow with "NO_RECAPTCHA" fake token and Off flow /// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an /// AuthErrorCodeInvalidAppCredential error is returned from the backend. /// - Parameter phoneNumber: The phone number to be verified. private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, multiFactorSession session: MultiFactorSession?, - uiDelegate: AuthUIDelegate?) async throws + uiDelegate: AuthUIDelegate?, + auditFallback: Bool = false) async throws -> String? { if let settings = auth.settings, settings.isAppVerificationDisabledForTesting { @@ -370,20 +385,27 @@ import Foundation return response.verificationID } guard let session else { + // Phone MFA flow return try await verifyClAndSendVerificationCode( toPhoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, - uiDelegate: uiDelegate + uiDelegate: uiDelegate, + auditFallback: auditFallback ) } + // MFA flows let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate) let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber, codeIdentity: codeIdentity) + if auditFallback { + startMFARequestInfo.injectRecaptchaFields(recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion, clientType: PhoneAuthProvider.kClientType) + } do { if let idToken = session.idToken { let request = StartMFAEnrollmentRequest(idToken: idToken, enrollmentInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) + // TODO if mode is audit, inject recaptcha field with no_recaptcha let response = try await auth.backend.call(with: request) return response.phoneSessionInfo?.sessionInfo } else { @@ -401,23 +423,27 @@ import Foundation phoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, multiFactorSession: session, - uiDelegate: uiDelegate + uiDelegate: uiDelegate, + auditFallback: auditFallback ) } } + /// This method is only called when Audit failed on rCE on invalid-app-credential exception private func handleVerifyErrorWithRetry(error: Error, phoneNumber: String, retryOnInvalidAppCredential: Bool, multiFactorSession session: MultiFactorSession?, - uiDelegate: AuthUIDelegate?) async throws -> String? { + uiDelegate: AuthUIDelegate?, + auditFallback: Bool = false) async throws -> String? { if (error as NSError).code == AuthErrorCode.invalidAppCredential.rawValue { if retryOnInvalidAppCredential { auth.appCredentialManager.clearCredential() return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber, retryOnInvalidAppCredential: false, multiFactorSession: session, - uiDelegate: uiDelegate) + uiDelegate: uiDelegate, + auditFallback: auditFallback) } throw AuthErrorUtils.unexpectedResponse(deserializedResponse: nil, underlyingError: error) } From 597151b9f2021069218773238b598f91f4c494d4 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 20 Nov 2024 15:32:10 -0800 Subject: [PATCH 2/3] check.sh --- .../Swift/AuthProvider/PhoneAuthProvider.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index ebb99fe90f5..74a82683628 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -276,9 +276,12 @@ import Foundation requestConfiguration: auth .requestConfiguration) if auditFallback { - request.injectRecaptchaFields(recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion) + request.injectRecaptchaFields( + recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, + recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion + ) } - //TODO inject fake_token when .audit + // TODO: inject fake_token when .audit do { let response = try await auth.backend.call(with: request) return response.verificationID @@ -294,7 +297,8 @@ import Foundation } } - /// Starts the flow to verify the client via silent push notification. This is used in both .Audit and .Enforce mode + /// Starts the flow to verify the client via silent push notification. This is used in both + /// .Audit and .Enforce mode /// - Parameter retryOnInvalidAppCredential: Whether of not the flow should be retried if an /// AuthErrorCodeInvalidAppCredential error is returned from the backend. /// - Parameter phoneNumber: The phone number to be verified. @@ -398,14 +402,18 @@ import Foundation let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber, codeIdentity: codeIdentity) if auditFallback { - startMFARequestInfo.injectRecaptchaFields(recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion, clientType: PhoneAuthProvider.kClientType) + startMFARequestInfo.injectRecaptchaFields( + recaptchaResponse: PhoneAuthProvider.kFakeCaptchaResponse, + recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion, + clientType: PhoneAuthProvider.kClientType + ) } do { if let idToken = session.idToken { let request = StartMFAEnrollmentRequest(idToken: idToken, enrollmentInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - // TODO if mode is audit, inject recaptcha field with no_recaptcha + // TODO: if mode is audit, inject recaptcha field with no_recaptcha let response = try await auth.backend.call(with: request) return response.phoneSessionInfo?.sessionInfo } else { From f69b1393bde970ddec9f24c619c64f6faafed216 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:53:59 -0800 Subject: [PATCH 3/3] Remove TODO comments --- FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index 74a82683628..1953368b76d 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -281,7 +281,6 @@ import Foundation recaptchaVersion: PhoneAuthProvider.kRecaptchaVersion ) } - // TODO: inject fake_token when .audit do { let response = try await auth.backend.call(with: request) return response.verificationID @@ -413,7 +412,6 @@ import Foundation let request = StartMFAEnrollmentRequest(idToken: idToken, enrollmentInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - // TODO: if mode is audit, inject recaptcha field with no_recaptcha let response = try await auth.backend.call(with: request) return response.phoneSessionInfo?.sessionInfo } else {