Skip to content

Commit d103cff

Browse files
committed
implement MFA
1 parent 30b5e48 commit d103cff

19 files changed

+552
-43
lines changed

packages/firebase_ui_auth/example/lib/main.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ class FirebaseAuthUIExample extends StatelessWidget {
116116
Navigator.pushReplacementNamed(context, '/profile');
117117
}
118118
}),
119+
AuthStateChangeAction<MFARequired>((context, state) async {
120+
final nav = Navigator.of(context);
121+
122+
await startMFAVerification(
123+
resolver: state.resolver,
124+
context: context,
125+
);
126+
127+
nav.pushReplacementNamed('/profile');
128+
}),
119129
EmailLinkSignInAction((context) {
120130
Navigator.pushReplacementNamed(context, '/email-link-sign-in');
121131
}),
@@ -232,6 +242,7 @@ class FirebaseAuthUIExample extends StatelessWidget {
232242
}),
233243
],
234244
actionCodeSettings: actionCodeSettings,
245+
showMFATile: true,
235246
);
236247
},
237248
},

packages/firebase_ui_auth/lib/firebase_ui_auth.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export 'src/auth_state.dart'
1515
SignedIn,
1616
SigningIn,
1717
AuthFailed,
18-
DifferentSignInMethodsFound;
18+
DifferentSignInMethodsFound,
19+
MFARequired;
1920

2021
export 'src/providers/auth_provider.dart';
2122
export 'src/providers/email_auth_provider.dart';
@@ -92,6 +93,8 @@ export 'src/styling/theme.dart' show FirebaseUITheme;
9293
export 'src/styling/style.dart' show FirebaseUIStyle;
9394
export 'src/widgets/internal/universal_button.dart' show ButtonVariant;
9495

96+
export 'src/mfa.dart' show startMFAVerification;
97+
9598
import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider;
9699
import 'package:firebase_core/firebase_core.dart';
97100
import 'package:flutter/widgets.dart';

packages/firebase_ui_auth/lib/src/actions.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,17 @@ class FirebaseUIActions extends InheritedWidget {
101101

102102
/// A [Widget] to wrap with [FirebaseUIActions].
103103
required Widget child,
104+
105+
/// A list of [FirebaseUIAction]s to provide to the [child].
106+
List<FirebaseUIAction> actions = const [],
104107
}) {
105108
final w = maybeOf(from);
106109

107110
if (w != null) {
108-
return FirebaseUIActions(actions: w.actions, child: child);
111+
return FirebaseUIActions(
112+
actions: [...w.actions, ...actions],
113+
child: child,
114+
);
109115
}
110116

111117
return child;

packages/firebase_ui_auth/lib/src/auth_controller.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ enum AuthAction {
1313

1414
/// Links a provided credential with currently signed in user account
1515
link,
16+
17+
/// Disables automatic credential handling.
18+
/// It's up to the user to decide what to do with the obtained credential.
19+
none,
1620
}
1721

1822
/// An abstract class that should be implemented by auth controllers of

packages/firebase_ui_auth/lib/src/auth_flow.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart';
66
import 'package:firebase_auth/firebase_auth.dart';
77
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
88

9+
import 'auth_state.dart';
10+
911
/// An exception that is being thrown when user cancels the authentication
1012
/// process.
1113
class AuthCancelledException implements Exception {
@@ -106,7 +108,7 @@ class AuthFlow<T extends AuthProvider> extends ValueNotifier<AuthState>
106108
}
107109

108110
@override
109-
void onBeforeCredentialLinked(AuthCredential credential) {
111+
void onCredentialReceived(AuthCredential credential) {
110112
value = CredentialReceived(credential);
111113
}
112114

@@ -164,4 +166,9 @@ class AuthFlow<T extends AuthProvider> extends ValueNotifier<AuthState>
164166
void onCanceled() {
165167
value = initialState;
166168
}
169+
170+
@override
171+
void onMFARequired(MultiFactorResolver resolver) {
172+
value = MFARequired(resolver);
173+
}
167174
}

packages/firebase_ui_auth/lib/src/auth_state.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
22
import 'package:flutter/widgets.dart';
3-
import 'package:firebase_auth/firebase_auth.dart' show AuthCredential, User;
3+
import 'package:firebase_auth/firebase_auth.dart'
4+
show AuthCredential, MultiFactorResolver, User;
45

56
/// An abstract class for all auth states.
67
/// [AuthState] transitions could be captured with an [AuthStateChangeAction]:
@@ -171,6 +172,16 @@ class FetchingProvidersForEmail extends AuthState {
171172
const FetchingProvidersForEmail();
172173
}
173174

175+
/// {@template ui.auth.auth_state.mfa_required}
176+
/// An [AuthState] that indicates that multi-factor authentication is required.
177+
/// {@endtemplate}
178+
class MFARequired extends AuthState {
179+
/// A multi-factor resolver that should be used to complete MFA.
180+
final MultiFactorResolver resolver;
181+
182+
const MFARequired(this.resolver);
183+
}
184+
174185
class AuthStateProvider extends InheritedWidget {
175186
final AuthState state;
176187

packages/firebase_ui_auth/lib/src/flows/phone_auth_flow.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ class AutoresolutionFailedException implements Exception {
7373
abstract class PhoneAuthController extends AuthController {
7474
/// Initializes the flow with a phone number. This method should be called
7575
/// after user submits a phone number.
76-
void acceptPhoneNumber(String phoneNumber);
76+
void acceptPhoneNumber(
77+
String phoneNumber, [
78+
fba.MultiFactorSession? multiFactorSession,
79+
]);
7780

7881
/// Triggers an SMS code verification.
7982
void verifySMSCode(
@@ -113,15 +116,23 @@ class PhoneAuthFlow extends AuthFlow<PhoneAuthProvider>
113116
);
114117

115118
@override
116-
void acceptPhoneNumber(String phoneNumber) {
117-
provider.sendVerificationCode(phoneNumber, action);
119+
void acceptPhoneNumber(
120+
String phoneNumber, [
121+
fba.MultiFactorSession? multiFactorSession,
122+
]) {
123+
provider.sendVerificationCode(
124+
phoneNumber: phoneNumber,
125+
action: action,
126+
multiFactorSession: multiFactorSession,
127+
);
118128
}
119129

120130
@override
121131
void verifySMSCode(
122132
String code, {
123133
String? verificationId,
124134
fba.ConfirmationResult? confirmationResult,
135+
fba.MultiFactorSession? multiFactorSession,
125136
}) {
126137
provider.verifySMSCode(
127138
action: action,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'dart:async';
2+
3+
import 'package:firebase_auth/firebase_auth.dart' hide PhoneAuthProvider;
4+
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
5+
import 'package:firebase_ui_auth/src/widgets/internal/universal_page_route.dart';
6+
import 'package:flutter/scheduler.dart';
7+
import 'package:flutter/widgets.dart';
8+
9+
Future<UserCredential> startMFAVerification({
10+
required BuildContext context,
11+
required MultiFactorResolver resolver,
12+
}) async {
13+
if (resolver.hints.first is PhoneMultiFactorInfo) {
14+
return startPhoneMFAVerification(
15+
context: context,
16+
resolver: resolver,
17+
);
18+
} else {
19+
throw Exception('Unsupported MFA type');
20+
}
21+
}
22+
23+
Future<UserCredential> startPhoneMFAVerification({
24+
required BuildContext context,
25+
required MultiFactorResolver resolver,
26+
FirebaseAuth? auth,
27+
}) async {
28+
final session = resolver.session;
29+
final hint = resolver.hints.first;
30+
final completer = Completer<UserCredential>();
31+
final navigator = Navigator.of(context);
32+
33+
final provider = PhoneAuthProvider();
34+
provider.auth = auth ?? FirebaseAuth.instance;
35+
36+
final flow = PhoneAuthFlow(
37+
auth: auth ?? FirebaseAuth.instance,
38+
action: AuthAction.none,
39+
provider: PhoneAuthProvider(),
40+
);
41+
42+
provider.authListener = flow;
43+
44+
final flowKey = Object();
45+
46+
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
47+
provider.sendVerificationCode(
48+
hint: hint as PhoneMultiFactorInfo,
49+
multiFactorSession: session,
50+
action: AuthAction.none,
51+
);
52+
});
53+
54+
navigator.push(
55+
createPageRoute(
56+
context: context,
57+
builder: (context) {
58+
return AuthFlowBuilder<PhoneAuthController>(
59+
flow: flow,
60+
flowKey: flowKey,
61+
child: SMSCodeInputScreen(
62+
flowKey: flowKey,
63+
action: AuthAction.none,
64+
auth: auth,
65+
actions: [
66+
AuthStateChangeAction<CredentialReceived>((context, inner) {
67+
final cred = inner.credential as PhoneAuthCredential;
68+
final assertion = PhoneMultiFactorGenerator.getAssertion(cred);
69+
try {
70+
final cred = resolver.resolveSignIn(assertion);
71+
completer.complete(cred);
72+
} catch (e) {
73+
completer.completeError(e);
74+
}
75+
}),
76+
],
77+
),
78+
);
79+
},
80+
),
81+
);
82+
83+
final cred = await completer.future;
84+
85+
navigator.pop();
86+
return cred;
87+
}

packages/firebase_ui_auth/lib/src/navigation/phone_verification.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth;
1+
import 'package:firebase_auth/firebase_auth.dart'
2+
show
3+
FirebaseAuth,
4+
MultiFactorInfo,
5+
MultiFactorSession,
6+
PhoneMultiFactorInfo;
27
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
38
import 'package:flutter/material.dart';
49

@@ -13,15 +18,27 @@ Future<void> startPhoneVerification({
1318

1419
/// {@macro ui.auth.auth_controller.auth}
1520
FirebaseAuth? auth,
21+
22+
/// {@macro ui.auth.providers.phone_auth_provider.mfa_session}
23+
MultiFactorSession? multiFactorSession,
24+
25+
/// {@macro ui.auth.providers.phone_auth_provider.mfa_hint}
26+
PhoneMultiFactorInfo? hint,
27+
28+
/// Additional actions to pass down to the [PhoneInputScreen].
29+
List<FirebaseUIAction> actions = const [],
1630
}) async {
1731
await Navigator.of(context).push(
1832
createPageRoute(
1933
context: context,
2034
builder: (_) => FirebaseUIActions.inherit(
2135
from: context,
36+
actions: actions,
2237
child: PhoneInputScreen(
2338
auth: auth,
2439
action: action,
40+
multiFactorSession: multiFactorSession,
41+
mfaHint: hint,
2542
),
2643
),
2744
),

packages/firebase_ui_auth/lib/src/providers/auth_provider.dart

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ void defaultOnAuthError(AuthProvider provider, Object error) {
1212
throw error;
1313
}
1414

15+
if (error is FirebaseAuthMultiFactorException) {
16+
provider.authListener.onMFARequired(error.resolver);
17+
return;
18+
}
19+
1520
if (error.code == 'account-exists-with-different-credential') {
1621
final email = error.email;
1722
if (email == null) {
@@ -49,7 +54,7 @@ abstract class AuthListener {
4954

5055
/// Called before an attempt to link the credential with currently signed in
5156
/// user account.
52-
void onBeforeCredentialLinked(AuthCredential credential);
57+
void onCredentialReceived(AuthCredential credential);
5358

5459
/// Called if the credential was successfully linked with the user account.
5560
void onCredentialLinked(AuthCredential credential);
@@ -66,6 +71,9 @@ abstract class AuthListener {
6671

6772
/// Called when the user cancells the sign in process.
6873
void onCanceled();
74+
75+
/// Called when the user has to complete MFA.
76+
void onMFARequired(MultiFactorResolver resolver);
6977
}
7078

7179
/// {@template ui.auth.auth_provider}
@@ -109,7 +117,7 @@ abstract class AuthProvider<T extends AuthListener, K extends AuthCredential> {
109117
/// Links a provided [AuthCredential] with the currently signed in user
110118
/// account.
111119
void linkWithCredential(AuthCredential credential) {
112-
authListener.onBeforeCredentialLinked(credential);
120+
authListener.onCredentialReceived(credential);
113121
try {
114122
final user = auth.currentUser!;
115123
user
@@ -155,10 +163,18 @@ abstract class AuthProvider<T extends AuthListener, K extends AuthCredential> {
155163
/// hooks are called if action is [AuthAction.signUp].
156164
/// {@endtemplate}
157165
void onCredentialReceived(K credential, AuthAction action) {
158-
if (action == AuthAction.link) {
159-
linkWithCredential(credential);
160-
} else {
161-
signInWithCredential(credential);
166+
switch (action) {
167+
case AuthAction.link:
168+
linkWithCredential(credential);
169+
break;
170+
case AuthAction.signIn:
171+
signInWithCredential(credential);
172+
break;
173+
case AuthAction.none:
174+
authListener.onCredentialReceived(credential);
175+
break;
176+
default:
177+
throw Exception('$runtimeType should handle $action');
162178
}
163179
}
164180
}

0 commit comments

Comments
 (0)