Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose TOKEN_EXPIRED error upon mfa unenroll. #6973

Merged
merged 2 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-boats-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/auth': patch
---

Expose TOKEN_EXPIRED error when mfa unenroll logs out the user.
4 changes: 4 additions & 0 deletions packages/auth/demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@
id="sign-in-with-email-and-password">
Sign In with Email and Password
</button>
<button class="btn btn-block btn-primary"
id="reauth-with-email-and-password">
Reauthenticate with Email and Password
</button>
</form>
<form class="form form-bordered no-submit">
<input type="text" id="user-custom-token" class="form-control"
Expand Down
44 changes: 42 additions & 2 deletions packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ function showMultiFactorStatus(activeUser) {
const label = info && (info.displayName || info.uid);
if (label) {
$('#enrolled-factors-drop-down').removeClass('open');
// Set the last user, in case the current user is logged out.
// This can happen if the MFA option being unenrolled is the one that was most recently enrolled into.
// See - https://github.com/firebase/firebase-js-sdk/issues/3233
setLastUser(activeUser);
mfaUser.unenroll(info).then(() => {
refreshUserData();
alertSuccess('Multi-factor successfully unenrolled.');
Expand Down Expand Up @@ -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.');
}
}
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/core/strategies/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions packages/auth/src/mfa/mfa_user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
});
});
Expand Down
41 changes: 18 additions & 23 deletions packages/auth/src/mfa/mfa_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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;
}
}
}
Expand Down