-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Recaptcha Enterprise integration with phone auth flows #13192
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
base: main
Are you sure you want to change the base?
Changes from all commits
7665630
d93c078
150d805
7fda3a4
2fbc001
bc71e05
5477d9d
2e2d67f
e139f33
4551a26
6159e24
abaa930
442fbb3
8df9d02
3370b9d
40f4f74
108c555
4999595
e9cfad6
3855e45
8cf57db
facfa9a
90e866e
f9bcce4
4c9df96
30ed91a
9b75a9a
01dbe09
8967140
5f230c3
e82baef
99167bc
9c332e2
db43d99
b9479fa
a979479
268f74d
40a18e4
fdd4a27
bfaaf1d
35f5b2f
d32b320
bcfa862
3717629
88ac589
0a4b9bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a api reference here? what does cl stand for? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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 | ||
} | ||
|
@@ -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() | ||
|
@@ -497,6 +645,7 @@ import Foundation | |
return | ||
} | ||
callbackScheme = "" | ||
self.recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth) | ||
} | ||
|
||
private let kAuthTypeVerifyApp = "verifyApp" | ||
|
Uh oh!
There was an error while loading. Please reload this page.