Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recaptcha Enterprise integration with phone auth flows #13192

Open
wants to merge 46 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7665630
fix phone auth login flow to swift methods and inject recaptcha fields
pragatimodi Jun 15, 2024
d93c078
add recaptcha enterprise dependencies and fix flows to use swift methods
pragatimodi Jun 18, 2024
150d805
undo explicit package invocation
pragatimodi Jun 20, 2024
7fda3a4
Barebones e2e flow for phone auth login
pragatimodi Jun 27, 2024
2fbc001
add logic for verify rce
pragatimodi Jul 2, 2024
bc71e05
Merge branch 'release-11.0' into inject-fields
pragatimodi Jul 2, 2024
5477d9d
lint formatting
pragatimodi Jul 2, 2024
2e2d67f
lint formatting
pragatimodi Jul 2, 2024
e139f33
Merge branch 'inject-fields' into phone-auth-login
pragatimodi Jul 2, 2024
4551a26
lint fixes
pragatimodi Jul 2, 2024
6159e24
whitespace
pragatimodi Jul 2, 2024
abaa930
remove comments
pragatimodi Jul 8, 2024
442fbb3
Barebones e2e flow for phone auth login
pragatimodi Jun 27, 2024
8df9d02
add logic for verify rce
pragatimodi Jul 2, 2024
3370b9d
lint fixes
pragatimodi Jul 2, 2024
40f4f74
temp changes
pragatimodi Aug 6, 2024
108c555
Merge branch 'main' into phone-auth-login
pragatimodi Aug 20, 2024
4999595
Version 11.2.0 (#13522)
paulb777 Aug 20, 2024
e9cfad6
add recaptcha enterprise dependencies and fix flows to use swift methods
pragatimodi Jun 18, 2024
3855e45
undo explicit package invocation
pragatimodi Jun 20, 2024
8cf57db
lint formatting
pragatimodi Jul 2, 2024
facfa9a
use action enum
pragatimodi Aug 20, 2024
90e866e
revert pod changes
pragatimodi Aug 20, 2024
f9bcce4
undo pod changes
pragatimodi Aug 20, 2024
4c9df96
lint fixes
pragatimodi Aug 21, 2024
30ed91a
recaptcha auth flow clean
pragatimodi Aug 26, 2024
9b75a9a
modify unit tests
pragatimodi Aug 27, 2024
01dbe09
Revert setup changes
pragatimodi Aug 27, 2024
8967140
lint changes
pragatimodi Aug 27, 2024
5f230c3
decouple recaptcha enabled unit tests
pragatimodi Aug 27, 2024
e82baef
restore oauthprovidertests
pragatimodi Aug 27, 2024
99167bc
line
pragatimodi Aug 27, 2024
9c332e2
fix CI failures
pragatimodi Sep 3, 2024
db43d99
modifying objc methods to use swift implementations
pragatimodi Sep 3, 2024
b9479fa
remove ios only enablement enum
pragatimodi Sep 3, 2024
a979479
silence expectations related swift warning
pragatimodi Sep 4, 2024
268f74d
fix
pragatimodi Sep 4, 2024
40a18e4
fix recaptcha config unit tests
pragatimodi Sep 4, 2024
fdd4a27
add recaptcha config unit tests
pragatimodi Sep 4, 2024
bfaaf1d
address PR feedback
pragatimodi Sep 4, 2024
35f5b2f
add expectations to callbacks fns
pragatimodi Sep 4, 2024
d32b320
add
pragatimodi Sep 4, 2024
bcfa862
lint
pragatimodi Sep 4, 2024
3717629
change force refresh value
pragatimodi Sep 4, 2024
88ac589
remove async implementations
pragatimodi Sep 5, 2024
0a4b9bc
add comments
pragatimodi Sep 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2289,7 +2289,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 {
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
try await recaptchaVerifier.injectRecaptchaFields(request: request,
provider: AuthRecaptchaProvider.password,
action: action)
Expand Down
195 changes: 172 additions & 23 deletions FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,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,29 +194,63 @@ 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:
paulb777 marked this conversation as resolved.
Show resolved Hide resolved
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
)
}
}

/// 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.
/// - 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
/// Initiates the verification flow by sending a verification code with reCAPTCHA protection to
/// the provided phone number.
/// - Parameters:
/// - phoneNumber: The phone number to which the verification code should be sent.
/// - retryOnInvalidAppCredential: A boolean indicating whether to retry the flow if an
/// AuthErrorCodeInvalidAppCredential error occurs.
/// - uiDelegate: An optional delegate for handling UI events during the verification process.
/// - recaptchaVerifier: An instance of `AuthRecaptchaVerifier` to inject reCAPTCHA fields
/// into the request.
/// - Returns: A string containing the verification ID if the request is successful; otherwise,
/// handles the error and returns nil.
private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a api reference here? what does cl stand for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cl stands for client I think, deriving the name from an existing implementation of verifyClAndSendVerificationCode

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, might still worth adding a comment here.

retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
-> String? {
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: codeIdentity,
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 {
Expand All @@ -228,23 +262,136 @@ import Foundation
}
}

/// 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.
/// Initiates the verification flow by sending a verification code to the provided phone number
/// using a silent push notification.
/// - Parameters:
/// - phoneNumber: The phone number to which the verification code should be sent.
/// - retryOnInvalidAppCredential: A boolean indicating whether to retry the flow if an
/// AuthErrorCodeInvalidAppCredential error occurs.
/// - uiDelegate: An optional delegate for handling UI events during the verification process.
/// - Returns: A string containing the verification ID if the request is successful; otherwise,
/// handles the error and returns nil.
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
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 AuthBackend.call(with: request)
return response.verificationID
} catch {
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate
)
}
}

/// Initiates the verification flow by sending a verification code with reCAPTCHA protection,
/// optionally considering a multi-factor session.
/// - Parameters:
/// - phoneNumber: The phone number to which the verification code should be sent.
/// - retryOnInvalidAppCredential: A boolean indicating whether to retry the flow if an
/// AuthErrorCodeInvalidAppCredential error occurs.
/// - session: An optional `MultiFactorSession` instance to include in the verification flow
/// for multi-factor authentication.
/// - uiDelegate: An optional delegate for handling UI events during the verification process.
/// - recaptchaVerifier: An instance of `AuthRecaptchaVerifier` to inject reCAPTCHA fields
/// into the request.
/// - Returns: A string containing the verification ID or session info if the request is
/// successful; otherwise, handles the error and returns nil.
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
)
}
}

/// Initiates the verification flow by sending a verification code, optionally considering a
/// multi-factor session using silent push notification.
/// - Parameters:
/// - phoneNumber: The phone number to which the verification code should be sent.
/// - retryOnInvalidAppCredential: A boolean indicating whether to retry the flow if an
/// AuthErrorCodeInvalidAppCredential error occurs.
/// - session: An optional `MultiFactorSession` instance to include in the verification flow
/// for multi-factor authentication.
/// - uiDelegate: An optional delegate for handling UI events during the verification process.
/// - Returns: A string containing the verification ID or session info if the request is
/// successful; otherwise, handles the error and returns nil.
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?) 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
}
Expand Down Expand Up @@ -477,8 +624,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 @@ -497,6 +645,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,6 +29,15 @@ 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"

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
}
}
Loading