Skip to content

Commit 0145cda

Browse files
committed
First pass at adding MFA support to auth-next (#3292)
* First pass at adding MFA support to auth-next * Refactor IDP & reauth interfaces a bit to play nicer with MFA * PR feedback * More tests & interface cleanup * One last test * More PR Feedbakc
1 parent f2f8bb0 commit 0145cda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1913
-353
lines changed

packages-exp/auth-exp/demo/src/index.js

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ import {
4545
confirmPasswordReset,
4646
linkWithCredential,
4747
reauthenticateWithCredential,
48-
unlink
48+
unlink,
49+
getMultiFactorResolver,
50+
multiFactor,
51+
PhoneMultiFactorGenerator
4952
} from '@firebase/auth-exp';
5053

5154
import { config } from './config';
@@ -189,8 +192,8 @@ function addProviderIcon(providerId) {
189192
* @param {!firebase.User} activeUser The corresponding user.
190193
*/
191194
function showMultiFactorStatus(activeUser) {
192-
var enrolledFactors =
193-
(activeUser.multiFactor && activeUser.multiFactor.enrolledFactors) || [];
195+
mfaUser = multiFactor(activeUser);
196+
var enrolledFactors = (mfaUser && mfaUser.enrolledFactors) || [];
194197
var $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors');
195198
// Hide the drop down menu initially.
196199
$listGroup
@@ -221,7 +224,7 @@ function showMultiFactorStatus(activeUser) {
221224
var label = info && (info.displayName || info.uid);
222225
if (label) {
223226
$('#enrolled-factors-drop-down').removeClass('open');
224-
activeUser.multiFactor.unenroll(info).then(function() {
227+
mfaUser.unenroll(info).then(function() {
225228
refreshUserData();
226229
alertSuccess('Multi-factor successfully unenrolled.');
227230
}, onAuthError);
@@ -249,7 +252,7 @@ function onAuthError(error) {
249252
logAtLevel_(error, 'error');
250253
if (error.code == 'auth/multi-factor-auth-required') {
251254
// Handle second factor sign-in.
252-
handleMultiFactorSignIn(error.resolver);
255+
handleMultiFactorSignIn(getMultiFactorResolver(auth, error));
253256
} else {
254257
alertError('Error: ' + error.code);
255258
}
@@ -361,10 +364,7 @@ function onSignInWithEmailLink() {
361364
function onLinkWithEmailLink() {
362365
var email = $('#link-with-email-link-email').val();
363366
var link = $('#link-with-email-link-link').val() || undefined;
364-
var credential = firebase.auth.EmailAuthProvider.credentialWithLink(
365-
email,
366-
link
367-
);
367+
var credential = EmailAuthProvider.credentialWithLink(email, link);
368368
linkWithCredential(activeUser(), credential).then(
369369
onAuthUserCredentialSuccess,
370370
onAuthError
@@ -578,8 +578,8 @@ function onStartEnrollWithPhoneMultiFactor() {
578578
clearApplicationVerifier();
579579
// Initialize a reCAPTCHA application verifier.
580580
makeApplicationVerifier('enroll-mfa-verify-phone-number');
581-
activeUser()
582-
.multiFactor.getSession()
581+
multiFactor(activeUser())
582+
.getSession()
583583
.then(function(multiFactorSession) {
584584
var phoneInfoOptions = {
585585
'phoneNumber': phoneNumber,
@@ -604,23 +604,24 @@ function onStartEnrollWithPhoneMultiFactor() {
604604
* Confirms a phone number verification for MFA enrollment.
605605
*/
606606
function onFinalizeEnrollWithPhoneMultiFactor() {
607-
alertNotImplemented();
608-
// var verificationId = $('#enroll-mfa-phone-verification-id').val();
609-
// var verificationCode = $('#enroll-mfa-phone-verification-code').val();
610-
// if (!verificationId || !verificationCode || !activeUser()) {
611-
// return;
612-
// }
613-
// var credential = PhoneAuthProvider.credential(
614-
// verificationId, verificationCode);
615-
// var multiFactorAssertion =
616-
// firebase.auth.PhoneMultiFactorGenerator.assertion(credential);
617-
// var displayName = $('#enroll-mfa-phone-display-name').val() || undefined;
607+
var verificationId = $('#enroll-mfa-phone-verification-id').val();
608+
var verificationCode = $('#enroll-mfa-phone-verification-code').val();
609+
if (!verificationId || !verificationCode || !activeUser()) {
610+
return;
611+
}
612+
var credential = PhoneAuthProvider.credential(
613+
verificationId,
614+
verificationCode
615+
);
616+
var multiFactorAssertion = PhoneMultiFactorGenerator.assertion(credential);
617+
var displayName = $('#enroll-mfa-phone-display-name').val() || undefined;
618618

619-
// activeUser().multiFactor.enroll(multiFactorAssertion, displayName)
620-
// .then(function() {
621-
// refreshUserData();
622-
// alertSuccess('Phone number enrolled!');
623-
// }, onAuthError);
619+
multiFactor(activeUser())
620+
.enroll(multiFactorAssertion, displayName)
621+
.then(function() {
622+
refreshUserData();
623+
alertSuccess('Phone number enrolled!');
624+
}, onAuthError);
624625
}
625626

626627
/**
@@ -830,7 +831,7 @@ function onLinkWithEmailAndPassword() {
830831
var password = $('#link-password').val();
831832
linkWithCredential(
832833
activeUser(),
833-
firebase.auth.EmailAuthProvider.credential(email, password)
834+
EmailAuthProvider.credential(email, password)
834835
).then(onAuthUserCredentialSuccess, onAuthError);
835836
}
836837

@@ -966,7 +967,7 @@ function onSignOut() {
966967

967968
/**
968969
* Handles multi-factor sign-in completion.
969-
* @param {!firebase.auth.MultiFactorResolver} resolver The multi-factor error
970+
* @param {!MultiFactorResolver} resolver The multi-factor error
970971
* resolver.
971972
*/
972973
function handleMultiFactorSignIn(resolver) {
@@ -1002,7 +1003,7 @@ function handleMultiFactorSignIn(resolver) {
10021003
* Displays the list of multi-factors in the provided list group.
10031004
* @param {!jQuery<!HTMLElement>} $listGroup The list group where the enrolled
10041005
* factors will be displayed.
1005-
* @param {!Array<!firebase.auth.MultiFactorInfo>} multiFactorInfo The list of
1006+
* @param {!Array<!MultiFactorInfo>} multiFactorInfo The list of
10061007
* multi-factors to display.
10071008
* @param {?function(!jQuery.Event)} onClick The click handler when a second
10081009
* factor is clicked.
@@ -1119,20 +1120,20 @@ function onStartSignInWithPhoneMultiFactor(event) {
11191120
* @param {!jQuery.Event} event The jQuery event object.
11201121
*/
11211122
function onFinalizeSignInWithPhoneMultiFactor(event) {
1122-
alertNotImplemented();
1123-
// event.preventDefault();
1124-
// var verificationId = $('#multi-factor-sign-in-verification-id').val();
1125-
// var code = $('#multi-factor-sign-in-verification-code').val();
1126-
// if (!code || !verificationId || !multiFactorErrorResolver) {
1127-
// return;
1128-
// }
1129-
// var cred = PhoneAuthProvider.credential(verificationId, code);
1130-
// var assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
1131-
// multiFactorErrorResolver.resolveSignIn(assertion)
1132-
// .then(function(userCredential) {
1133-
// onAuthUserCredentialSuccess(userCredential);
1134-
// $('#multiFactorModal').modal('hide');
1135-
// }, onAuthError);
1123+
event.preventDefault();
1124+
var verificationId = $('#multi-factor-sign-in-verification-id').val();
1125+
var code = $('#multi-factor-sign-in-verification-code').val();
1126+
if (!code || !verificationId || !multiFactorErrorResolver) {
1127+
return;
1128+
}
1129+
var cred = PhoneAuthProvider.credential(verificationId, code);
1130+
var assertion = PhoneMultiFactorGenerator.assertion(cred);
1131+
multiFactorErrorResolver
1132+
.resolveSignIn(assertion)
1133+
.then(function(userCredential) {
1134+
onAuthUserCredentialSuccess(userCredential);
1135+
$('#multiFactorModal').modal('hide');
1136+
}, onAuthError);
11361137
}
11371138

11381139
/**

packages-exp/auth-exp/src/api/account_management/account.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { Endpoint, HttpMethod, _performApiRequest } from '../';
1919
import { Auth } from '../../model/auth';
20-
import { APIMFAInfo } from '../../model/id_token';
20+
import { MfaEnrollment } from './mfa';
2121

2222
export interface DeleteAccountRequest {
2323
idToken: string;
@@ -75,7 +75,7 @@ export interface APIUserInfo {
7575
tenantId?: string;
7676
passwordHash?: string;
7777
providerUserInfo?: ProviderUserInfo[];
78-
mfaInfo?: APIMFAInfo[];
78+
mfaInfo?: MfaEnrollment[];
7979
}
8080

8181
export interface GetAccountInfoRequest {

packages-exp/auth-exp/src/api/account_management/mfa.test.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import { testAuth } from '../../../test/mock_auth';
2626
import * as mockFetch from '../../../test/mock_fetch';
2727
import { Auth } from '../../model/auth';
2828
import { ServerError } from '../errors';
29-
import { enrollPhoneMfa, startEnrollPhoneMfa, withdrawMfa } from './mfa';
29+
import {
30+
finalizeEnrollPhoneMfa,
31+
startEnrollPhoneMfa,
32+
withdrawMfa
33+
} from './mfa';
3034

3135
use(chaiAsPromised);
3236

@@ -57,7 +61,10 @@ describe('api/account_management/startEnrollPhoneMfa', () => {
5761

5862
const response = await startEnrollPhoneMfa(auth, request);
5963
expect(response.phoneSessionInfo.sessionInfo).to.eq('session-info');
60-
expect(mock.calls[0].request).to.eql(request);
64+
expect(mock.calls[0].request).to.eql({
65+
tenantId: null,
66+
...request
67+
});
6168
expect(mock.calls[0].method).to.eq('POST');
6269
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
6370
'application/json'
@@ -88,12 +95,16 @@ describe('api/account_management/startEnrollPhoneMfa', () => {
8895
FirebaseError,
8996
"Firebase: This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key. (auth/invalid-user-token)."
9097
);
91-
expect(mock.calls[0].request).to.eql(request);
98+
expect(mock.calls[0].request).to.eql({
99+
tenantId: null,
100+
...request
101+
});
92102
});
93103
});
94104

95-
describe('api/account_management/enrollPhoneMfa', () => {
105+
describe('api/account_management/finalizeEnrollPhoneMfa', () => {
96106
const request = {
107+
idToken: 'id-token',
97108
phoneVerificationInfo: {
98109
temporaryProof: 'temporary-proof',
99110
phoneNumber: 'phone-number',
@@ -113,14 +124,17 @@ describe('api/account_management/enrollPhoneMfa', () => {
113124

114125
it('should POST to the correct endpoint', async () => {
115126
const mock = mockEndpoint(Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, {
116-
displayName: 'my-name',
117-
idToken: 'id-token'
127+
idToken: 'id-token',
128+
refreshToken: 'refresh-token'
118129
});
119130

120-
const response = await enrollPhoneMfa(auth, request);
121-
expect(response.displayName).to.eq('my-name');
131+
const response = await finalizeEnrollPhoneMfa(auth, request);
122132
expect(response.idToken).to.eq('id-token');
123-
expect(mock.calls[0].request).to.eql(request);
133+
expect(response.refreshToken).to.eq('refresh-token');
134+
expect(mock.calls[0].request).to.eql({
135+
tenantId: null,
136+
...request
137+
});
124138
expect(mock.calls[0].method).to.eq('POST');
125139
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
126140
'application/json'
@@ -147,11 +161,14 @@ describe('api/account_management/enrollPhoneMfa', () => {
147161
400
148162
);
149163

150-
await expect(enrollPhoneMfa(auth, request)).to.be.rejectedWith(
164+
await expect(finalizeEnrollPhoneMfa(auth, request)).to.be.rejectedWith(
151165
FirebaseError,
152166
'Firebase: The verification ID used to create the phone auth credential is invalid. (auth/invalid-verification-id).'
153167
);
154-
expect(mock.calls[0].request).to.eql(request);
168+
expect(mock.calls[0].request).to.eql({
169+
tenantId: null,
170+
...request
171+
});
155172
});
156173
});
157174

@@ -172,14 +189,17 @@ describe('api/account_management/withdrawMfa', () => {
172189

173190
it('should POST to the correct endpoint', async () => {
174191
const mock = mockEndpoint(Endpoint.WITHDRAW_MFA, {
175-
displayName: 'my-name',
176-
idToken: 'id-token'
192+
idToken: 'id-token',
193+
refreshToken: 'refresh-token'
177194
});
178195

179196
const response = await withdrawMfa(auth, request);
180-
expect(response.displayName).to.eq('my-name');
181197
expect(response.idToken).to.eq('id-token');
182-
expect(mock.calls[0].request).to.eql(request);
198+
expect(response.refreshToken).to.eq('refresh-token');
199+
expect(mock.calls[0].request).to.eql({
200+
tenantId: null,
201+
...request
202+
});
183203
expect(mock.calls[0].method).to.eq('POST');
184204
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
185205
'application/json'
@@ -210,6 +230,9 @@ describe('api/account_management/withdrawMfa', () => {
210230
FirebaseError,
211231
"Firebase: This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key. (auth/invalid-user-token)."
212232
);
213-
expect(mock.calls[0].request).to.eql(request);
233+
expect(mock.calls[0].request).to.eql({
234+
tenantId: null,
235+
...request
236+
});
214237
});
215238
});

0 commit comments

Comments
 (0)