Skip to content

Commit c040512

Browse files
committed
Add MFA view controller and support MFA on google sign-in
1 parent 709a685 commit c040512

File tree

4 files changed

+198
-26
lines changed

4 files changed

+198
-26
lines changed

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/OtherAuthMethods.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum OtherAuthMethod: String {
1919
case Passwordless = "Email Link/Passwordless"
2020
case PhoneNumber = "Phone Auth"
2121
case Custom = "a Custom Auth System"
22+
case MfaLogin = "Multifactor Authentication"
2223

2324
var navigationTitle: String { "Sign in using \(rawValue)" }
2425

@@ -30,6 +31,8 @@ enum OtherAuthMethod: String {
3031
return "Enter Phone Number"
3132
case .Custom:
3233
return "Enter Custom Auth Token"
34+
case .MfaLogin:
35+
return "Choose a Second Factor to Continue"
3336
}
3437
}
3538

@@ -41,13 +44,17 @@ enum OtherAuthMethod: String {
4144
return "phone.circle"
4245
case .Custom:
4346
return "lock.shield"
47+
case .MfaLogin:
48+
return "phone.circle"
4449
}
4550
}
4651

4752
var textFieldInputText: String? {
4853
switch self {
4954
case .PhoneNumber:
5055
return "Example input for +1 (123)456-7890 would be 11234567890"
56+
case .MfaLogin:
57+
return "Enter the index of the selected factor to continue"
5158
default:
5259
return nil
5360
}
@@ -61,6 +68,8 @@ enum OtherAuthMethod: String {
6168
return "Send Verification Code"
6269
case .Custom:
6370
return "Login"
71+
case .MfaLogin:
72+
return "Send Verification Code"
6473
}
6574
}
6675

@@ -72,9 +81,17 @@ enum OtherAuthMethod: String {
7281
return phoneNumberInfoText
7382
case .Custom:
7483
return customAuthInfoText
84+
case .MfaLogin:
85+
return mfaLoginInfoText
7586
}
7687
}
7788

89+
private var mfaLoginInfoText: String {
90+
"""
91+
MFA placeholder
92+
"""
93+
}
94+
7895
private var passwordlessInfoText: String {
7996
"""
8097
Authenticate users with only their email, \

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -202,35 +202,31 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
202202
// [END_EXCLUDE]
203203
let config = GIDConfiguration(clientID: clientID)
204204
GIDSignIn.sharedInstance.configuration = config
205+
Task {
206+
do {
207+
let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: self)
208+
let user = result.user
209+
guard let idToken = user.idToken?.tokenString
210+
else {
211+
// [START_EXCLUDE]
212+
let error = NSError(
213+
domain: "GIDSignInError",
214+
code: -1,
215+
userInfo: [
216+
NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
217+
]
218+
)
219+
return displayError(error)
220+
// [END_EXCLUDE]
221+
}
222+
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
223+
accessToken: user.accessToken.tokenString)
224+
try await newSignIn(with: credential)
205225

206-
// Start the sign in flow!
207-
GIDSignIn.sharedInstance.signIn(withPresenting: self) { [unowned self] result, error in
208-
guard error == nil else {
209-
// [START_EXCLUDE]
210-
return displayError(error)
211-
// [END_EXCLUDE]
212-
}
213-
214-
guard let user = result?.user,
215-
let idToken = user.idToken?.tokenString
216-
else {
217-
// [START_EXCLUDE]
218-
let error = NSError(
219-
domain: "GIDSignInError",
220-
code: -1,
221-
userInfo: [
222-
NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
223-
]
224-
)
226+
} catch {
225227
return displayError(error)
226-
// [END_EXCLUDE]
227228
}
228229

229-
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
230-
accessToken: user.accessToken.tokenString)
231-
232-
// [START_EXCLUDE]
233-
signIn(with: credential)
234230
// [END_EXCLUDE]
235231
}
236232
// [END headless_google_auth]
@@ -252,6 +248,22 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
252248
// [END signin_google_credential]
253249
}
254250

251+
func newSignIn(with credential: AuthCredential) async throws {
252+
do {
253+
_ = try await AppManager.shared.auth().signIn(with: credential)
254+
transitionToUserViewController()
255+
} catch {
256+
let authError = error as NSError
257+
if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
258+
let resolver = authError
259+
.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
260+
performMfaLoginFlow(resolver: resolver)
261+
} else {
262+
return displayError(error)
263+
}
264+
}
265+
}
266+
255267
// For Sign in with Apple
256268
var currentNonce: String?
257269

@@ -358,6 +370,13 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
358370
navigationController?.present(navPhoneAuthController, animated: true)
359371
}
360372

373+
private func performMfaLoginFlow(resolver: MultiFactorResolver) {
374+
let mfaLoginController = MFALoginViewController(resolver: resolver)
375+
mfaLoginController.delegate = self
376+
let navMfaLoginController = UINavigationController(rootViewController: mfaLoginController)
377+
navigationController?.present(navMfaLoginController, animated: true)
378+
}
379+
361380
private func performAnonymousLoginFlow() {
362381
AppManager.shared.auth().signInAnonymously { result, error in
363382
guard error == nil else { return self.displayError(error) }
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseAuth
16+
import UIKit
17+
18+
class MFALoginViewController: OtherAuthViewController {
19+
var resolver: MultiFactorResolver
20+
21+
init(resolver: MultiFactorResolver) {
22+
self.resolver = resolver
23+
super.init(nibName: nil, bundle: nil)
24+
}
25+
26+
@available(*, unavailable)
27+
required init?(coder: NSCoder) {
28+
fatalError("init(coder:) has not been implemented")
29+
}
30+
31+
override func viewDidLoad() {
32+
super.viewDidLoad()
33+
configureUI(for: .MfaLogin)
34+
configureMfaSelections()
35+
}
36+
37+
override func buttonTapped() {
38+
guard let selectedFactorIndex = textField.text, !selectedFactorIndex.isEmpty else { return }
39+
phoneMfaAuthLogin(selectedFactorIndex: selectedFactorIndex)
40+
}
41+
42+
// MARK: - Firebase 🔥
43+
44+
// Display available factors
45+
// TODO: optimize and beautify this.
46+
private func configureMfaSelections() {
47+
var msg = "available factors: \n"
48+
for (index, mfaInfo) in resolver.hints.enumerated() {
49+
msg += "[" + String(index) + "] " + mfaInfo.displayName!
50+
msg += "\n"
51+
}
52+
textFieldInputLabel?.text = msg
53+
}
54+
55+
private func phoneMfaAuthLogin(selectedFactorIndex: String) {
56+
let multifactorInfo = resolver.hints[Int(selectedFactorIndex)!]
57+
// TODO: support TOTP in sample app
58+
if multifactorInfo.factorID == TOTPMultiFactorID {
59+
let error = NSError(
60+
domain: "SignInError",
61+
code: -1,
62+
userInfo: [
63+
NSLocalizedDescriptionKey: "TOTP MFA factor is not supported",
64+
]
65+
)
66+
displayError(error)
67+
return
68+
}
69+
70+
signIn(hint: multifactorInfo as! PhoneMultiFactorInfo)
71+
}
72+
73+
/// Start the 2nd factor signIn
74+
private func signIn(hint: PhoneMultiFactorInfo) {
75+
Task {
76+
do {
77+
let verificationId = try await PhoneAuthProvider.provider().verifyPhoneNumber(
78+
with: hint,
79+
uiDelegate: nil,
80+
multiFactorSession: resolver.session
81+
)
82+
let verificationCodeFromUser = try await getVerificationCode()
83+
let credential = PhoneAuthProvider.provider().credential(
84+
withVerificationID: verificationId,
85+
verificationCode: verificationCodeFromUser
86+
)
87+
let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
88+
resolver.resolveSignIn(with: assertion) { authResult, error in
89+
guard error == nil else { return self.displayError(error) }
90+
self.navigationController?.dismiss(animated: true, completion: {
91+
self.delegate?.loginDidOccur()
92+
})
93+
}
94+
}
95+
}
96+
}
97+
98+
/// Display the pop up window for end user to enter the one-time code
99+
private func presentVerificationCodeController(saveHandler: @escaping (String) -> Void) {
100+
let verificationCodeController = UIAlertController(
101+
title: "Verification Code",
102+
message: nil,
103+
preferredStyle: .alert
104+
)
105+
verificationCodeController.addTextField { textfield in
106+
textfield.placeholder = "Enter the code you received"
107+
textfield.textContentType = .oneTimeCode
108+
}
109+
110+
let onContinue: (UIAlertAction) -> Void = { _ in
111+
let text = verificationCodeController.textFields!.first!.text!
112+
saveHandler(text)
113+
}
114+
115+
verificationCodeController
116+
.addAction(UIAlertAction(title: "Continue", style: .default, handler: onContinue))
117+
verificationCodeController.addAction(UIAlertAction(title: "Cancel", style: .cancel))
118+
119+
present(verificationCodeController, animated: true, completion: nil)
120+
}
121+
122+
private func getVerificationCode() async throws -> String {
123+
return try await withCheckedThrowingContinuation { continuation in
124+
self.presentVerificationCodeController { code in
125+
if code != "" {
126+
continuation.resume(returning: code)
127+
} else {
128+
// Cancelled
129+
continuation.resume(throwing: NSError())
130+
}
131+
}
132+
}
133+
}
134+
}

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/OtherAuthViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class OtherAuthViewController: UIViewController {
2727
return textField
2828
}()
2929

30-
private var textFieldInputLabel: UILabel?
30+
var textFieldInputLabel: UILabel?
3131

3232
private lazy var button: UIButton = {
3333
let button = UIButton()
@@ -113,6 +113,8 @@ class OtherAuthViewController: UIViewController {
113113
let label = UILabel()
114114
label.font = .systemFont(ofSize: 12)
115115
label.textColor = .secondaryLabel
116+
// unlimited line breaks
117+
label.numberOfLines = 0
116118
label.text = text
117119
label.alpha = UIDevice.current.orientation.isLandscape ? 0 : 1
118120
label.translatesAutoresizingMaskIntoConstraints = false

0 commit comments

Comments
 (0)