Skip to content

Commit

Permalink
Adding rCE support for phone auth flows. (#14047)
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoshouzi-gh committed Nov 13, 2024
1 parent cf30f5a commit 839bbb2
Show file tree
Hide file tree
Showing 9 changed files with 608 additions and 221 deletions.
2 changes: 1 addition & 1 deletion FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2293,7 +2293,7 @@ extension Auth: AuthInterop {
action: AuthRecaptchaAction) async throws -> T
.Response {
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self)
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) {
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) != .off {
try await recaptchaVerifier.injectRecaptchaFields(request: request,
provider: AuthRecaptchaProvider.password,
action: action)
Expand Down
243 changes: 183 additions & 60 deletions FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,19 @@ import Foundation
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession? = nil,
completion: ((_: String?, _: Error?) -> Void)?) {
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
urlTypes: auth.mainBundleUrlTypes) else {
fatalError(
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
)
}
kAuthGlobalWorkQueue.async {
Task {
do {
let verificationID = try await self.internalVerify(
phoneNumber: phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession
)
Auth.wrapMainAsync(callback: completion, withParam: verificationID, error: nil)
} catch {
Auth.wrapMainAsync(callback: completion, withParam: nil, error: error)
Task {
do {
let verificationID = try await verifyPhoneNumber(
phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession
)
await MainActor.run {
completion?(verificationID, nil)
}
} catch {
await MainActor.run {
completion?(nil, error)
}
}
}
Expand All @@ -107,16 +103,19 @@ import Foundation
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession? = nil) async throws
-> String {
return try await withCheckedThrowingContinuation { continuation in
self.verifyPhoneNumber(phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession) { result, error in
if let error {
continuation.resume(throwing: error)
} else if let result {
continuation.resume(returning: result)
}
}
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
urlTypes: auth.mainBundleUrlTypes) else {
fatalError(
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
)
}

if let verificationID = try await internalVerify(phoneNumber: phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession) {
return verificationID
} else {
throw AuthErrorUtils.invalidVerificationIDError(message: "Invalid verification ID")
}
}

Expand All @@ -133,11 +132,22 @@ import Foundation
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession?,
completion: ((_: String?, _: Error?) -> Void)?) {
multiFactorSession?.multiFactorInfo = multiFactorInfo
verifyPhoneNumber(multiFactorInfo.phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession,
completion: completion)
Task {
do {
let verificationID = try await verifyPhoneNumber(
with: multiFactorInfo,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession
)
await MainActor.run {
completion?(verificationID, nil)
}
} catch {
await MainActor.run {
completion?(nil, error)
}
}
}
}

/// Verify ownership of the second factor phone number by the current user.
Expand All @@ -152,17 +162,10 @@ import Foundation
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession?) async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
self.verifyPhoneNumber(with: multiFactorInfo,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession) { result, error in
if let error {
continuation.resume(throwing: error)
} else if let result {
continuation.resume(returning: result)
}
}
}
multiFactorSession?.multiFactorInfo = multiFactorInfo
return try await verifyPhoneNumber(multiFactorInfo.phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession)
}

/// Creates an `AuthCredential` for the phone number provider identified by the
Expand All @@ -185,7 +188,7 @@ import Foundation
uiDelegate: AuthUIDelegate?,
multiFactorSession: MultiFactorSession? = nil) async throws
-> String? {
guard phoneNumber.count > 0 else {
guard !phoneNumber.isEmpty else {
throw AuthErrorUtils.missingPhoneNumberError(message: nil)
}
guard let manager = auth.notificationManager else {
Expand All @@ -194,37 +197,155 @@ import Foundation
guard await manager.checkNotificationForwarding() else {
throw AuthErrorUtils.notificationNotForwardedError()
}
return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate)

let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: false)

switch recaptchaVerifier.enablementStatus(forProvider: .phone) {
case .off:
return try await verifyClAndSendVerificationCode(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate
)
case .audit:
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
case .enforce:
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: false,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
}
}

func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
-> String? {
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth
.requestConfiguration)
do {
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .sendVerificationCode
)
let response = try await AuthBackend.call(with: request)
return response.verificationID
} catch {
return try await handleVerifyErrorWithRetry(error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate)
}
}

/// Starts the flow to verify the client via silent push notification.
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
/// - 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.
/// - Parameter callback: The callback to be invoked on the global work queue when the flow is
/// finished.
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?) async throws
func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?) async throws
-> String? {
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: codeIdentity,
requestConfiguration: auth
.requestConfiguration)

do {
let response = try await auth.backend.call(with: request)
let response = try await AuthBackend.call(with: request)
return response.verificationID
} catch {
return try await handleVerifyErrorWithRetry(error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate)
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate
)
}
}

/// Starts the flow to verify the client via silent push notification.
/// - 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 verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
-> String? {
if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
let request = SendVerificationCodeRequest(
phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth.requestConfiguration
)
let response = try await AuthBackend.call(with: request)
return response.verificationID
}
guard let session else {
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
}
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty)
do {
if let idToken = session.idToken {
let request = StartMFAEnrollmentRequest(idToken: idToken,
enrollmentInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .startMfaEnrollment
)
let response = try await AuthBackend.call(with: request)
return response.phoneSessionInfo?.sessionInfo
} else {
let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
MFAEnrollmentID: session.multiFactorInfo?.uid,
signInInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .startMfaSignin
)
let response = try await AuthBackend.call(with: request)
return response.responseInfo?.sessionInfo
}
} catch {
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: session,
uiDelegate: uiDelegate
)
}
}

Expand Down Expand Up @@ -474,8 +595,9 @@ import Foundation
private let auth: Auth
private let callbackScheme: String
private let usingClientIDScheme: Bool
private var recaptchaVerifier: AuthRecaptchaVerifier?

init(auth: Auth) {
init(auth: Auth, recaptchaVerifier: AuthRecaptchaVerifier? = nil) {
self.auth = auth
if let clientID = auth.app?.options.clientID {
let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed()
Expand All @@ -494,6 +616,7 @@ import Foundation
return
}
callbackScheme = ""
self.recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
}

private let kAuthTypeVerifyApp = "verifyApp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,20 @@ private let kSecretKey = "iosSecret"
/// The key for the reCAPTCHAToken parameter in the request.
private let kreCAPTCHATokenKey = "recaptchaToken"

/// The key for the "clientType" value in the request.
private let kClientType = "clientType"

/// The key for the "captchaResponse" value in the request.
private let kCaptchaResponseKey = "captchaResponse"

/// The key for the "recaptchaVersion" value in the request.
private let kRecaptchaVersion = "recaptchaVersion"

/// The key for the tenant id value in the request.
private let kTenantIDKey = "tenantId"

/// A verification code can be an appCredential or a reCaptcha Token
enum CodeIdentity {
enum CodeIdentity: Equatable {
case credential(AuthAppCredential)
case recaptcha(String)
case empty
Expand All @@ -50,6 +59,12 @@ class SendVerificationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
/// verification code.
let codeIdentity: CodeIdentity

/// Response to the captcha.
var captchaResponse: String?

/// The reCAPTCHA version.
var recaptchaVersion: String?

init(phoneNumber: String, codeIdentity: CodeIdentity,
requestConfiguration: AuthRequestConfiguration) {
self.phoneNumber = phoneNumber
Expand All @@ -71,10 +86,21 @@ class SendVerificationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
postBody[kreCAPTCHATokenKey] = reCAPTCHAToken
case .empty: break
}

if let captchaResponse {
postBody[kCaptchaResponseKey] = captchaResponse
}
if let recaptchaVersion {
postBody[kRecaptchaVersion] = recaptchaVersion
}
if let tenantID {
postBody[kTenantIDKey] = tenantID
}
postBody[kClientType] = clientType
return postBody
}

func injectRecaptchaFields(recaptchaResponse: String?, recaptchaVersion: String) {
captchaResponse = recaptchaResponse
self.recaptchaVersion = recaptchaVersion
}
}
4 changes: 4 additions & 0 deletions FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ class AuthErrorUtils {
error(code: .missingAndroidPackageName, message: message)
}

static func invalidRecaptchaTokenError() -> Error {
error(code: .invalidRecaptchaToken)
}

static func unauthorizedDomainError(message: String?) -> Error {
error(code: .unauthorizedDomain, message: message)
}
Expand Down
Loading

0 comments on commit 839bbb2

Please sign in to comment.