From d943c2c8ca9013fa094592884f6db9f54d16cd90 Mon Sep 17 00:00:00 2001 From: Pavithra Ramesh Date: Mon, 23 Jan 2023 16:57:30 -0800 Subject: [PATCH 1/2] 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. --- packages/auth/demo/public/index.html | 4 ++ packages/auth/demo/src/index.js | 44 ++++++++++++++++++- .../auth/src/core/strategies/credential.ts | 3 +- packages/auth/src/mfa/mfa_user.test.ts | 6 ++- packages/auth/src/mfa/mfa_user.ts | 41 ++++++++--------- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/packages/auth/demo/public/index.html b/packages/auth/demo/public/index.html index aa32e144ed5..624b6e3c0e7 100644 --- a/packages/auth/demo/public/index.html +++ b/packages/auth/demo/public/index.html @@ -252,6 +252,10 @@ id="sign-in-with-email-and-password"> Sign In with Email and Password +
{ refreshUserData(); alertSuccess('Multi-factor successfully unenrolled.'); @@ -278,6 +282,9 @@ function onAuthError(error) { handleMultiFactorSignIn(getMultiFactorResolver(auth, error)); } else { alertError('Error: ' + error.code); + if (error.code === 'auth/user-token-expired') { + alertError('Token expired, please reauthenticate.'); + } } } @@ -403,13 +410,41 @@ function onLinkWithEmailLink() { * Re-authenticate a user with email link credential. */ function onReauthenticateWithEmailLink() { + if (!activeUser()) { + alertError( + 'No user logged in. Select the "Last User" tab to reauth the previous user.' + ); + return; + } const email = $('#link-with-email-link-email').val(); const link = $('#link-with-email-link-link').val() || undefined; const credential = EmailAuthProvider.credentialWithLink(email, link); + // This will not set auth.currentUser to lastUser if the lastUser is reauthenticated. reauthenticateWithCredential(activeUser(), credential).then(result => { logAdditionalUserInfo(result); refreshUserData(); - alertSuccess('User reauthenticated!'); + alertSuccess('User reauthenticated with email link!'); + }, onAuthError); +} + +/** + * Re-authenticate a user with email and password. + */ +function onReauthenticateWithEmailAndPassword() { + if (!activeUser()) { + alertError( + 'No user logged in. Select the "Last User" tab to reauth the previous user.' + ); + return; + } + const email = $('#signin-email').val(); + const password = $('#signin-password').val(); + const credential = EmailAuthProvider.credential(email, password); + // This will not set auth.currentUser to lastUser if the lastUser is reauthenticated. + reauthenticateWithCredential(activeUser(), credential).then(result => { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('User reauthenticated with email/password!'); }, onAuthError); } @@ -1264,7 +1299,9 @@ function signInWithPopupRedirect(provider) { break; case 'reauthenticate': if (!activeUser()) { - alertError('No user logged in.'); + alertError( + 'No user logged in. Select the "Last User" tab to reauth the previous user.' + ); return; } inst = activeUser(); @@ -1860,6 +1897,9 @@ function initApp() { // Actions listeners. $('#sign-up-with-email-and-password').click(onSignUp); $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword); + $('#reauth-with-email-and-password').click( + onReauthenticateWithEmailAndPassword + ); $('.sign-in-with-custom-token').click(onSignInWithCustomToken); $('#sign-in-anonymously').click(onSignInAnonymously); $('#sign-in-with-generic-idp-credential').click( diff --git a/packages/auth/src/core/strategies/credential.ts b/packages/auth/src/core/strategies/credential.ts index 4d24c758fb9..00aa919f047 100644 --- a/packages/auth/src/core/strategies/credential.ts +++ b/packages/auth/src/core/strategies/credential.ts @@ -96,7 +96,8 @@ export async function linkWithCredential( * * @remarks * Use before operations such as {@link updatePassword} that require tokens from recent sign-in - * attempts. This method can be used to recover from a `CREDENTIAL_TOO_OLD_LOGIN_AGAIN` error. + * attempts. This method can be used to recover from a `CREDENTIAL_TOO_OLD_LOGIN_AGAIN` error + * or a `TOKEN_EXPIRED` error. * * @param user - The user. * @param credential - The auth credential. diff --git a/packages/auth/src/mfa/mfa_user.test.ts b/packages/auth/src/mfa/mfa_user.test.ts index 2c0da80a11a..c152c1384a5 100644 --- a/packages/auth/src/mfa/mfa_user.test.ts +++ b/packages/auth/src/mfa/mfa_user.test.ts @@ -235,8 +235,10 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => { ); }); - it('should swallow the error', async () => { - await mfaUser.unenroll(mfaInfo); + it('should throw TOKEN_EXPIRED error', async () => { + await expect(mfaUser.unenroll(mfaInfo)).to.be.rejectedWith( + 'auth/user-token-expired' + ); }); }); }); diff --git a/packages/auth/src/mfa/mfa_user.ts b/packages/auth/src/mfa/mfa_user.ts index 535de17310d..51e3433cc44 100644 --- a/packages/auth/src/mfa/mfa_user.ts +++ b/packages/auth/src/mfa/mfa_user.ts @@ -23,13 +23,12 @@ import { } from '../model/public_types'; import { withdrawMfa } from '../api/account_management/mfa'; -import { AuthErrorCode } from '../core/errors'; import { _logoutIfInvalidated } from '../core/user/invalidation'; import { UserInternal } from '../model/user'; import { MultiFactorAssertionImpl } from './mfa_assertion'; import { MultiFactorInfoImpl } from './mfa_info'; import { MultiFactorSessionImpl } from './mfa_session'; -import { FirebaseError, getModularInstance } from '@firebase/util'; +import { getModularInstance } from '@firebase/util'; export class MultiFactorUserImpl implements MultiFactorUser { enrolledFactors: MultiFactorInfo[] = []; @@ -78,30 +77,26 @@ export class MultiFactorUserImpl implements MultiFactorUser { const mfaEnrollmentId = typeof infoOrUid === 'string' ? infoOrUid : infoOrUid.uid; const idToken = await this.user.getIdToken(); - const idTokenResponse = await _logoutIfInvalidated( - this.user, - withdrawMfa(this.user.auth, { - idToken, - mfaEnrollmentId - }) - ); - // Remove the second factor from the user's list. - this.enrolledFactors = this.enrolledFactors.filter( - ({ uid }) => uid !== mfaEnrollmentId - ); - // Depending on whether the backend decided to revoke the user's session, - // the tokenResponse may be empty. If the tokens were not updated (and they - // are now invalid), reloading the user will discover this and invalidate - // the user's state accordingly. - await this.user._updateTokensIfNecessary(idTokenResponse); try { + const idTokenResponse = await _logoutIfInvalidated( + this.user, + withdrawMfa(this.user.auth, { + idToken, + mfaEnrollmentId + }) + ); + // Remove the second factor from the user's list. + this.enrolledFactors = this.enrolledFactors.filter( + ({ uid }) => uid !== mfaEnrollmentId + ); + // Depending on whether the backend decided to revoke the user's session, + // the tokenResponse may be empty. If the tokens were not updated (and they + // are now invalid), reloading the user will discover this and invalidate + // the user's state accordingly. + await this.user._updateTokensIfNecessary(idTokenResponse); await this.user.reload(); } catch (e) { - if ( - (e as FirebaseError)?.code !== `auth/${AuthErrorCode.TOKEN_EXPIRED}` - ) { - throw e; - } + throw e; } } } From 6d225681da4464c218cfbb1fdeb197a095ad10c9 Mon Sep 17 00:00:00 2001 From: Pavithra Ramesh Date: Tue, 24 Jan 2023 10:53:39 -0800 Subject: [PATCH 2/2] Changeset --- .changeset/chilled-boats-report.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilled-boats-report.md diff --git a/.changeset/chilled-boats-report.md b/.changeset/chilled-boats-report.md new file mode 100644 index 00000000000..1bdf195b85a --- /dev/null +++ b/.changeset/chilled-boats-report.md @@ -0,0 +1,5 @@ +--- +'@firebase/auth': patch +--- + +Expose TOKEN_EXPIRED error when mfa unenroll logs out the user.