-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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 40 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 |
---|---|---|
|
@@ -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) | ||
} | ||
} | ||
} | ||
|
@@ -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") | ||
} | ||
} | ||
|
||
|
@@ -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. | ||
|
@@ -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 | ||
|
@@ -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 { | ||
|
@@ -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.
Outdated
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, | ||
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 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. | ||
|
@@ -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 | ||
) | ||
} | ||
} | ||
|
||
|
@@ -244,7 +365,6 @@ import Foundation | |
codeIdentity: CodeIdentity.empty, | ||
requestConfiguration: auth.requestConfiguration | ||
) | ||
|
||
let response = try await AuthBackend.call(with: request) | ||
return response.verificationID | ||
} | ||
|
@@ -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() | ||
|
@@ -497,6 +618,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.