Skip to content

Commit 2be9c66

Browse files
sam-gcavolkovi
authored andcommitted
Add user invalidation handling (#3804)
* Add user invalidation handling * Formatting
1 parent d761886 commit 2be9c66

File tree

8 files changed

+187
-30
lines changed

8 files changed

+187
-30
lines changed

packages-exp/auth-exp/src/core/user/account_info.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../../api/account_management/email_and_password';
2424
import { updateProfile as apiUpdateProfile } from '../../api/account_management/profile';
2525
import { User } from '../../model/user';
26+
import { _logoutIfInvalidated } from './invalidation';
2627
import { _reloadWithoutSaving } from './reload';
2728

2829
interface Profile {
@@ -41,7 +42,10 @@ export async function updateProfile(
4142
const user = externUser as User;
4243
const idToken = await user.getIdToken();
4344
const profileRequest = { idToken, displayName, photoUrl };
44-
const response = await apiUpdateProfile(user.auth, profileRequest);
45+
const response = await _logoutIfInvalidated(
46+
user,
47+
apiUpdateProfile(user.auth, profileRequest)
48+
);
4549

4650
user.displayName = response.displayName || null;
4751
user.photoURL = response.photoUrl || null;
@@ -91,6 +95,9 @@ async function updateEmailOrPassword(
9195
request.password = password;
9296
}
9397

94-
const response = await apiUpdateEmailPassword(auth, request);
98+
const response = await _logoutIfInvalidated(
99+
user,
100+
apiUpdateEmailPassword(auth, request)
101+
);
95102
await user._updateTokensIfNecessary(response, /* reload */ true);
96103
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
21+
import { FirebaseError } from '@firebase/util';
22+
23+
import { testAuth, testUser } from '../../../test/helpers/mock_auth';
24+
import { Auth } from '../../model/auth';
25+
import { User } from '../../model/user';
26+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
27+
import { _logoutIfInvalidated } from './invalidation';
28+
29+
use(chaiAsPromised);
30+
31+
describe('src/core/user/invalidation', () => {
32+
let user: User;
33+
let auth: Auth;
34+
35+
beforeEach(async () => {
36+
auth = await testAuth();
37+
user = testUser(auth, 'uid');
38+
await auth.updateCurrentUser(user);
39+
});
40+
41+
function makeError(code: AuthErrorCode): FirebaseError {
42+
return AUTH_ERROR_FACTORY.create(code, { appName: auth.name });
43+
}
44+
45+
it('leaves non-invalidation errors alone', async () => {
46+
const error = makeError(AuthErrorCode.TOO_MANY_ATTEMPTS_TRY_LATER);
47+
await expect(
48+
_logoutIfInvalidated(user, Promise.reject(error))
49+
).to.be.rejectedWith(error);
50+
expect(auth.currentUser).to.eq(user);
51+
});
52+
53+
it('does nothing if the promise resolves', async () => {
54+
await _logoutIfInvalidated(user, Promise.resolve({}));
55+
expect(auth.currentUser).to.eq(user);
56+
});
57+
58+
it('logs out the user if the error is user_disabled', async () => {
59+
const error = makeError(AuthErrorCode.USER_DISABLED);
60+
await expect(
61+
_logoutIfInvalidated(user, Promise.reject(error))
62+
).to.be.rejectedWith(error);
63+
expect(auth.currentUser).to.be.null;
64+
});
65+
66+
it('logs out the user if the error is token_expired', async () => {
67+
const error = makeError(AuthErrorCode.TOKEN_EXPIRED);
68+
await expect(
69+
_logoutIfInvalidated(user, Promise.reject(error))
70+
).to.be.rejectedWith(error);
71+
expect(auth.currentUser).to.be.null;
72+
});
73+
74+
context('with another logged in user', () => {
75+
let user2: User;
76+
77+
beforeEach(async () => {
78+
user2 = testUser(auth, 'uid2');
79+
await auth.updateCurrentUser(user2);
80+
});
81+
82+
it('does not log out user2 if the error is user_disabled', async () => {
83+
const error = makeError(AuthErrorCode.USER_DISABLED);
84+
await expect(
85+
_logoutIfInvalidated(user, Promise.reject(error))
86+
).to.be.rejectedWith(error);
87+
expect(auth.currentUser).to.eq(user2);
88+
});
89+
});
90+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { FirebaseError } from '@firebase/util';
19+
20+
import { User } from '../../model/user';
21+
import { AuthErrorCode } from '../errors';
22+
23+
export async function _logoutIfInvalidated<T>(
24+
user: User,
25+
promise: Promise<T>
26+
): Promise<T> {
27+
try {
28+
return await promise;
29+
} catch (e) {
30+
if (e instanceof FirebaseError && isUserInvalidated(e)) {
31+
if (user.auth.currentUser === user) {
32+
await user.auth.signOut();
33+
}
34+
}
35+
36+
throw e;
37+
}
38+
}
39+
40+
function isUserInvalidated({ code }: FirebaseError): boolean {
41+
return (
42+
code === `auth/${AuthErrorCode.USER_DISABLED}` ||
43+
code === `auth/${AuthErrorCode.TOKEN_EXPIRED}`
44+
);
45+
}

packages-exp/auth-exp/src/core/user/link_unlink.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import * as externs from '@firebase/auth-types-exp';
1919

2020
import { deleteLinkedAccounts } from '../../api/account_management/account';
2121
import { _processCredentialSavingMfaContextIfNecessary } from '../../mfa/mfa_error';
22+
import { User, UserCredential } from '../../model/user';
2223
import { AuthCredential } from '../credentials';
2324
import { AuthErrorCode } from '../errors';
2425
import { assert } from '../util/assert';
2526
import { providerDataAsNames } from '../util/providers';
27+
import { _logoutIfInvalidated } from './invalidation';
2628
import { _reloadWithoutSaving } from './reload';
2729
import { UserCredentialImpl } from './user_credential_impl';
28-
import { User, UserCredential } from '../../model/user';
2930

3031
/**
3132
* This is the externally visible unlink function
@@ -61,9 +62,9 @@ export async function _link(
6162
user: User,
6263
credential: AuthCredential
6364
): Promise<UserCredential> {
64-
const response = await credential._linkToIdToken(
65-
user.auth,
66-
await user.getIdToken()
65+
const response = await _logoutIfInvalidated(
66+
user,
67+
credential._linkToIdToken(user.auth, await user.getIdToken())
6768
);
6869
return UserCredentialImpl._forOperation(
6970
user,

packages-exp/auth-exp/src/core/user/reauthenticate.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
*/
1717

1818
import { OperationType } from '@firebase/auth-types-exp';
19+
1920
import { _processCredentialSavingMfaContextIfNecessary } from '../../mfa/mfa_error';
2021
import { User } from '../../model/user';
2122
import { AuthCredential } from '../credentials';
2223
import { AuthErrorCode } from '../errors';
2324
import { assert, fail } from '../util/assert';
2425
import { _parseToken } from './id_token_result';
26+
import { _logoutIfInvalidated } from './invalidation';
2527
import { UserCredentialImpl } from './user_credential_impl';
2628

2729
export async function _reauthenticate(
@@ -32,11 +34,14 @@ export async function _reauthenticate(
3234
const operationType = OperationType.REAUTHENTICATE;
3335

3436
try {
35-
const response = await _processCredentialSavingMfaContextIfNecessary(
36-
user.auth,
37-
operationType,
38-
credential,
39-
user
37+
const response = await _logoutIfInvalidated(
38+
user,
39+
_processCredentialSavingMfaContextIfNecessary(
40+
user.auth,
41+
operationType,
42+
credential,
43+
user
44+
)
4045
);
4146
assert(response.idToken, AuthErrorCode.INTERNAL_ERROR, { appName });
4247
const parsed = _parseToken(response.idToken);

packages-exp/auth-exp/src/core/user/reload.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ import {
2222
ProviderUserInfo
2323
} from '../../api/account_management/account';
2424
import { User } from '../../model/user';
25-
import { UserMetadata } from './user_metadata';
26-
import { assert } from '../util/assert';
2725
import { AuthErrorCode } from '../errors';
26+
import { assert } from '../util/assert';
27+
import { _logoutIfInvalidated } from './invalidation';
28+
import { UserMetadata } from './user_metadata';
2829

2930
export async function _reloadWithoutSaving(user: User): Promise<void> {
3031
const auth = user.auth;
3132
const idToken = await user.getIdToken();
32-
const response = await getAccountInfo(auth, { idToken });
33+
const response = await _logoutIfInvalidated(
34+
user,
35+
getAccountInfo(auth, { idToken })
36+
);
3337

3438
assert(response?.users.length, AuthErrorCode.INTERNAL_ERROR, {
3539
appName: auth.name

packages-exp/auth-exp/src/core/user/user_impl.ts

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

1818
import * as externs from '@firebase/auth-types-exp';
1919
import { NextFn } from '@firebase/util';
20+
2021
import {
2122
APIUserInfo,
2223
deleteAccount
@@ -29,8 +30,9 @@ import { AuthErrorCode } from '../errors';
2930
import { PersistedBlob } from '../persistence';
3031
import { assert } from '../util/assert';
3132
import { getIdTokenResult } from './id_token_result';
33+
import { _logoutIfInvalidated } from './invalidation';
3234
import { ProactiveRefresh } from './proactive_refresh';
33-
import { reload, _reloadWithoutSaving } from './reload';
35+
import { _reloadWithoutSaving, reload } from './reload';
3436
import { StsTokenManager } from './token_manager';
3537
import { UserMetadata } from './user_metadata';
3638

@@ -83,9 +85,9 @@ export class UserImpl implements User {
8385
}
8486

8587
async getIdToken(forceRefresh?: boolean): Promise<string> {
86-
const accessToken = await this.stsTokenManager.getToken(
87-
this.auth,
88-
forceRefresh
88+
const accessToken = await _logoutIfInvalidated(
89+
this,
90+
this.stsTokenManager.getToken(this.auth, forceRefresh)
8991
);
9092
assert(accessToken, AuthErrorCode.INTERNAL_ERROR, {
9193
appName: this.auth.name
@@ -184,7 +186,7 @@ export class UserImpl implements User {
184186

185187
async delete(): Promise<void> {
186188
const idToken = await this.getIdToken();
187-
await deleteAccount(this.auth, { idToken });
189+
await _logoutIfInvalidated(this, deleteAccount(this.auth, { idToken }));
188190
this.stsTokenManager.clearRefreshToken();
189191

190192
// TODO: Determine if cancellable-promises are necessary to use in this class so that delete()

packages-exp/auth-exp/src/mfa/mfa_user.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
*/
1717
import * as externs from '@firebase/auth-types-exp';
1818

19-
import { User } from '../model/user';
20-
import { MultiFactorSession } from './mfa_session';
21-
import { MultiFactorAssertion } from './assertions';
2219
import { withdrawMfa } from '../api/account_management/mfa';
2320
import { AuthErrorCode } from '../core/errors';
21+
import { _logoutIfInvalidated } from '../core/user/invalidation';
22+
import { User } from '../model/user';
23+
import { MultiFactorAssertion } from './assertions';
2424
import { MultiFactorInfo } from './mfa_info';
25+
import { MultiFactorSession } from './mfa_session';
2526

2627
export class MultiFactorUser implements externs.MultiFactorUser {
2728
enrolledFactors: externs.MultiFactorInfo[] = [];
@@ -50,10 +51,9 @@ export class MultiFactorUser implements externs.MultiFactorUser {
5051
): Promise<void> {
5152
const assertion = assertionExtern as MultiFactorAssertion;
5253
const session = (await this.getSession()) as MultiFactorSession;
53-
const finalizeMfaResponse = await assertion._process(
54-
this.user.auth,
55-
session,
56-
displayName
54+
const finalizeMfaResponse = await _logoutIfInvalidated(
55+
this.user,
56+
assertion._process(this.user.auth, session, displayName)
5757
);
5858
// New tokens will be issued after enrollment of the new second factors.
5959
// They need to be updated on the user.
@@ -68,10 +68,13 @@ export class MultiFactorUser implements externs.MultiFactorUser {
6868
const mfaEnrollmentId =
6969
typeof infoOrUid === 'string' ? infoOrUid : infoOrUid.uid;
7070
const idToken = await this.user.getIdToken();
71-
const idTokenResponse = await withdrawMfa(this.user.auth, {
72-
idToken,
73-
mfaEnrollmentId
74-
});
71+
const idTokenResponse = await _logoutIfInvalidated(
72+
this.user,
73+
withdrawMfa(this.user.auth, {
74+
idToken,
75+
mfaEnrollmentId
76+
})
77+
);
7578
// Remove the second factor from the user's list.
7679
this.enrolledFactors = this.enrolledFactors.filter(
7780
({ uid }) => uid !== mfaEnrollmentId

0 commit comments

Comments
 (0)