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 40 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
234 changes: 178 additions & 56 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,
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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,
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved
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,10 +197,61 @@ 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: true)
pragatimodi marked this conversation as resolved.
Show resolved Hide resolved

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

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 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.
Expand All @@ -215,16 +269,83 @@ import Foundation
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)
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 All @@ -244,7 +365,6 @@ import Foundation
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth.requestConfiguration
)

let response = try await AuthBackend.call(with: request)
return response.verificationID
}
Expand Down Expand Up @@ -477,8 +597,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 +618,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
Loading