Skip to content

Commit 6439f11

Browse files
authored
Expose TOKEN_EXPIRED error upon mfa unenroll. (#6973)
* Expose TOKEN_EXPIRED error upon mfa unenroll. This can be thrown if the MFA option that was most recently enrolled into, was unenrolled. The user will be logged out to prove the posession of the other second factor. This error can be handled by reauthenticating the user. This change also updates the demo app to store the lastUser in case mfa unenroll logs out the user. From here, the lastUser can be reauthenticated. * Changeset
1 parent 4604b3c commit 6439f11

File tree

6 files changed

+75
-28
lines changed

6 files changed

+75
-28
lines changed

.changeset/chilled-boats-report.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/auth': patch
3+
---
4+
5+
Expose TOKEN_EXPIRED error when mfa unenroll logs out the user.

packages/auth/demo/public/index.html

+4
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@
252252
id="sign-in-with-email-and-password">
253253
Sign In with Email and Password
254254
</button>
255+
<button class="btn btn-block btn-primary"
256+
id="reauth-with-email-and-password">
257+
Reauthenticate with Email and Password
258+
</button>
255259
</form>
256260
<form class="form form-bordered no-submit">
257261
<input type="text" id="user-custom-token" class="form-control"

packages/auth/demo/src/index.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ function showMultiFactorStatus(activeUser) {
247247
const label = info && (info.displayName || info.uid);
248248
if (label) {
249249
$('#enrolled-factors-drop-down').removeClass('open');
250+
// Set the last user, in case the current user is logged out.
251+
// This can happen if the MFA option being unenrolled is the one that was most recently enrolled into.
252+
// See - https://github.com/firebase/firebase-js-sdk/issues/3233
253+
setLastUser(activeUser);
250254
mfaUser.unenroll(info).then(() => {
251255
refreshUserData();
252256
alertSuccess('Multi-factor successfully unenrolled.');
@@ -278,6 +282,9 @@ function onAuthError(error) {
278282
handleMultiFactorSignIn(getMultiFactorResolver(auth, error));
279283
} else {
280284
alertError('Error: ' + error.code);
285+
if (error.code === 'auth/user-token-expired') {
286+
alertError('Token expired, please reauthenticate.');
287+
}
281288
}
282289
}
283290

@@ -403,13 +410,41 @@ function onLinkWithEmailLink() {
403410
* Re-authenticate a user with email link credential.
404411
*/
405412
function onReauthenticateWithEmailLink() {
413+
if (!activeUser()) {
414+
alertError(
415+
'No user logged in. Select the "Last User" tab to reauth the previous user.'
416+
);
417+
return;
418+
}
406419
const email = $('#link-with-email-link-email').val();
407420
const link = $('#link-with-email-link-link').val() || undefined;
408421
const credential = EmailAuthProvider.credentialWithLink(email, link);
422+
// This will not set auth.currentUser to lastUser if the lastUser is reauthenticated.
409423
reauthenticateWithCredential(activeUser(), credential).then(result => {
410424
logAdditionalUserInfo(result);
411425
refreshUserData();
412-
alertSuccess('User reauthenticated!');
426+
alertSuccess('User reauthenticated with email link!');
427+
}, onAuthError);
428+
}
429+
430+
/**
431+
* Re-authenticate a user with email and password.
432+
*/
433+
function onReauthenticateWithEmailAndPassword() {
434+
if (!activeUser()) {
435+
alertError(
436+
'No user logged in. Select the "Last User" tab to reauth the previous user.'
437+
);
438+
return;
439+
}
440+
const email = $('#signin-email').val();
441+
const password = $('#signin-password').val();
442+
const credential = EmailAuthProvider.credential(email, password);
443+
// This will not set auth.currentUser to lastUser if the lastUser is reauthenticated.
444+
reauthenticateWithCredential(activeUser(), credential).then(result => {
445+
logAdditionalUserInfo(result);
446+
refreshUserData();
447+
alertSuccess('User reauthenticated with email/password!');
413448
}, onAuthError);
414449
}
415450

@@ -1264,7 +1299,9 @@ function signInWithPopupRedirect(provider) {
12641299
break;
12651300
case 'reauthenticate':
12661301
if (!activeUser()) {
1267-
alertError('No user logged in.');
1302+
alertError(
1303+
'No user logged in. Select the "Last User" tab to reauth the previous user.'
1304+
);
12681305
return;
12691306
}
12701307
inst = activeUser();
@@ -1860,6 +1897,9 @@ function initApp() {
18601897
// Actions listeners.
18611898
$('#sign-up-with-email-and-password').click(onSignUp);
18621899
$('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword);
1900+
$('#reauth-with-email-and-password').click(
1901+
onReauthenticateWithEmailAndPassword
1902+
);
18631903
$('.sign-in-with-custom-token').click(onSignInWithCustomToken);
18641904
$('#sign-in-anonymously').click(onSignInAnonymously);
18651905
$('#sign-in-with-generic-idp-credential').click(

packages/auth/src/core/strategies/credential.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export async function linkWithCredential(
9696
*
9797
* @remarks
9898
* Use before operations such as {@link updatePassword} that require tokens from recent sign-in
99-
* attempts. This method can be used to recover from a `CREDENTIAL_TOO_OLD_LOGIN_AGAIN` error.
99+
* attempts. This method can be used to recover from a `CREDENTIAL_TOO_OLD_LOGIN_AGAIN` error
100+
* or a `TOKEN_EXPIRED` error.
100101
*
101102
* @param user - The user.
102103
* @param credential - The auth credential.

packages/auth/src/mfa/mfa_user.test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,10 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => {
235235
);
236236
});
237237

238-
it('should swallow the error', async () => {
239-
await mfaUser.unenroll(mfaInfo);
238+
it('should throw TOKEN_EXPIRED error', async () => {
239+
await expect(mfaUser.unenroll(mfaInfo)).to.be.rejectedWith(
240+
'auth/user-token-expired'
241+
);
240242
});
241243
});
242244
});

packages/auth/src/mfa/mfa_user.ts

+18-23
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ import {
2323
} from '../model/public_types';
2424

2525
import { withdrawMfa } from '../api/account_management/mfa';
26-
import { AuthErrorCode } from '../core/errors';
2726
import { _logoutIfInvalidated } from '../core/user/invalidation';
2827
import { UserInternal } from '../model/user';
2928
import { MultiFactorAssertionImpl } from './mfa_assertion';
3029
import { MultiFactorInfoImpl } from './mfa_info';
3130
import { MultiFactorSessionImpl } from './mfa_session';
32-
import { FirebaseError, getModularInstance } from '@firebase/util';
31+
import { getModularInstance } from '@firebase/util';
3332

3433
export class MultiFactorUserImpl implements MultiFactorUser {
3534
enrolledFactors: MultiFactorInfo[] = [];
@@ -78,30 +77,26 @@ export class MultiFactorUserImpl implements MultiFactorUser {
7877
const mfaEnrollmentId =
7978
typeof infoOrUid === 'string' ? infoOrUid : infoOrUid.uid;
8079
const idToken = await this.user.getIdToken();
81-
const idTokenResponse = await _logoutIfInvalidated(
82-
this.user,
83-
withdrawMfa(this.user.auth, {
84-
idToken,
85-
mfaEnrollmentId
86-
})
87-
);
88-
// Remove the second factor from the user's list.
89-
this.enrolledFactors = this.enrolledFactors.filter(
90-
({ uid }) => uid !== mfaEnrollmentId
91-
);
92-
// Depending on whether the backend decided to revoke the user's session,
93-
// the tokenResponse may be empty. If the tokens were not updated (and they
94-
// are now invalid), reloading the user will discover this and invalidate
95-
// the user's state accordingly.
96-
await this.user._updateTokensIfNecessary(idTokenResponse);
9780
try {
81+
const idTokenResponse = await _logoutIfInvalidated(
82+
this.user,
83+
withdrawMfa(this.user.auth, {
84+
idToken,
85+
mfaEnrollmentId
86+
})
87+
);
88+
// Remove the second factor from the user's list.
89+
this.enrolledFactors = this.enrolledFactors.filter(
90+
({ uid }) => uid !== mfaEnrollmentId
91+
);
92+
// Depending on whether the backend decided to revoke the user's session,
93+
// the tokenResponse may be empty. If the tokens were not updated (and they
94+
// are now invalid), reloading the user will discover this and invalidate
95+
// the user's state accordingly.
96+
await this.user._updateTokensIfNecessary(idTokenResponse);
9897
await this.user.reload();
9998
} catch (e) {
100-
if (
101-
(e as FirebaseError)?.code !== `auth/${AuthErrorCode.TOKEN_EXPIRED}`
102-
) {
103-
throw e;
104-
}
99+
throw e;
105100
}
106101
}
107102
}

0 commit comments

Comments
 (0)