voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
318 lines (271 loc) • 9 kB
text/typescript
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import { FactorId } from '../model/public_types';
import { mockEndpoint } from '../../test/helpers/api/helper';
import { testAuth, testUser, TestAuth } from '../../test/helpers/mock_auth';
import * as mockFetch from '../../test/helpers/mock_fetch';
import { Endpoint } from '../api';
import { APIUserInfo } from '../api/account_management/account';
import { FinalizeMfaResponse } from '../api/authentication/mfa';
import { ServerError } from '../api/errors';
import { UserInternal } from '../model/user';
import { MultiFactorInfoImpl } from './mfa_info';
import { MultiFactorSessionImpl, MultiFactorSessionType } from './mfa_session';
import { multiFactor, MultiFactorUserImpl } from './mfa_user';
import { MultiFactorAssertionImpl } from './mfa_assertion';
import { AuthInternal } from '../model/auth';
import { makeJWT } from '../../test/helpers/jwt';
use(chaiAsPromised);
class MockMultiFactorAssertion extends MultiFactorAssertionImpl {
constructor(readonly response: FinalizeMfaResponse) {
super(FactorId.PHONE);
}
async _finalizeEnroll(
_auth: AuthInternal,
_idToken: string,
_displayName?: string | null
): Promise<FinalizeMfaResponse> {
return this.response;
}
async _finalizeSignIn(
_auth: AuthInternal,
_mfaPendingCredential: string
): Promise<FinalizeMfaResponse> {
return this.response;
}
}
describe('core/mfa/mfa_user/MultiFactorUser', () => {
const idToken = makeJWT({ 'exp': '3600', 'iat': '1200' });
let auth: TestAuth;
let mfaUser: MultiFactorUserImpl;
let clock: sinon.SinonFakeTimers;
beforeEach(async () => {
auth = await testAuth();
mockFetch.setUp();
clock = sinon.useFakeTimers();
mfaUser = MultiFactorUserImpl._fromUser(
testUser(auth, 'uid', undefined, true)
);
});
afterEach(() => {
mockFetch.tearDown();
sinon.restore();
});
describe('getSession', () => {
it('should return the id token', async () => {
const mfaSession = (await mfaUser.getSession()) as MultiFactorSessionImpl;
expect(mfaSession.type).to.eq(MultiFactorSessionType.ENROLL);
expect(mfaSession.credential).to.eq('access-token');
});
});
describe('enroll', () => {
let assertion: MultiFactorAssertionImpl;
const serverUser: APIUserInfo = {
localId: 'local-id',
displayName: 'display-name',
photoUrl: 'photo-url',
email: 'email',
emailVerified: true,
phoneNumber: 'phone-number',
tenantId: 'tenant-id',
createdAt: 123,
lastLoginAt: 456,
mfaInfo: [
{
mfaEnrollmentId: 'mfa-id',
enrolledAt: Date.now(),
phoneInfo: 'phone-number'
}
]
};
const serverResponse: FinalizeMfaResponse = {
idToken,
refreshToken: 'refresh-token'
};
beforeEach(() => {
assertion = new MockMultiFactorAssertion(serverResponse);
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [serverUser]
});
});
it('should update the tokens', async () => {
await mfaUser.enroll(assertion);
expect(await mfaUser.user.getIdToken()).to.eq(idToken);
expect(mfaUser.user.stsTokenManager.expirationTime).to.eq(
clock.now + 2400 * 1000
);
});
it('should update the enrolled Factors', async () => {
await mfaUser.enroll(assertion);
expect(mfaUser.enrolledFactors.length).to.eq(1);
const enrolledFactor = mfaUser.enrolledFactors[0];
expect(enrolledFactor.factorId).to.eq(FactorId.PHONE);
expect(enrolledFactor.uid).to.eq('mfa-id');
});
});
describe('unenroll', () => {
let withdrawMfaEnrollmentMock: mockFetch.Route;
const serverResponse: FinalizeMfaResponse = {
idToken,
refreshToken: 'refresh-token'
};
const mfaInfo = MultiFactorInfoImpl._fromServerResponse(auth, {
mfaEnrollmentId: 'mfa-id',
enrolledAt: Date.now(),
phoneInfo: 'phone-info'
});
const otherMfaInfo = MultiFactorInfoImpl._fromServerResponse(auth, {
mfaEnrollmentId: 'other-mfa-id',
enrolledAt: Date.now(),
phoneInfo: 'other-phone-info'
});
const serverUser: APIUserInfo = {
localId: 'local-id',
mfaInfo: [
{
mfaEnrollmentId: 'other-mfa-id',
enrolledAt: Date.now(),
phoneInfo: 'other-phone-info'
}
]
};
beforeEach(() => {
withdrawMfaEnrollmentMock = mockEndpoint(
Endpoint.WITHDRAW_MFA,
serverResponse
);
mfaUser.enrolledFactors = [mfaInfo, otherMfaInfo];
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [serverUser]
});
});
it('should withdraw the MFA', async () => {
await mfaUser.unenroll(mfaInfo);
expect(withdrawMfaEnrollmentMock.calls[0].request).to.eql({
idToken: 'access-token',
mfaEnrollmentId: mfaInfo.uid,
tenantId: auth.tenantId
});
});
it('should remove matching enrollment factors but leave any others', async () => {
await mfaUser.unenroll(mfaInfo);
expect(mfaUser.enrolledFactors).to.eql([otherMfaInfo]);
});
it('should support passing a string instead of MultiFactorInfo', async () => {
await mfaUser.unenroll(mfaInfo.uid);
expect(withdrawMfaEnrollmentMock.calls[0].request).to.eql({
idToken: 'access-token',
mfaEnrollmentId: mfaInfo.uid,
tenantId: auth.tenantId
});
});
it('should update the tokens', async () => {
await mfaUser.unenroll(mfaInfo);
expect(await mfaUser.user.getIdToken()).to.eq(idToken);
expect(mfaUser.user.stsTokenManager.expirationTime).to.eq(
clock.now + 2400 * 1000
);
});
context('token revoked by backend', () => {
beforeEach(() => {
mockEndpoint(
Endpoint.GET_ACCOUNT_INFO,
{
error: {
message: ServerError.TOKEN_EXPIRED
}
},
403
);
});
it('should swallow the error', async () => {
await mfaUser.unenroll(mfaInfo);
});
});
});
});
describe('core/mfa/mfa_user/multiFactor', () => {
let auth: TestAuth;
let user: UserInternal;
beforeEach(async () => {
auth = await testAuth();
user = testUser(auth, 'uid', undefined, true);
});
it('can be used to a create a MultiFactorUser', () => {
const mfaUser = multiFactor(user);
expect((mfaUser as MultiFactorUserImpl).user).to.eq(user);
});
it('should only create one instance of an MFA user per User', () => {
const mfaUser = multiFactor(user);
expect(multiFactor(user)).to.eq(mfaUser);
});
context('enrolledFactors', () => {
const serverUser: APIUserInfo = {
localId: 'local-id',
mfaInfo: [
{
mfaEnrollmentId: 'enrollment-id',
enrolledAt: Date.now(),
phoneInfo: 'masked-phone-number'
}
]
};
const updatedServerUser: APIUserInfo = {
localId: 'local-id',
mfaInfo: [
{
mfaEnrollmentId: 'enrollment-id',
enrolledAt: Date.now(),
phoneInfo: 'masked-phone-number'
},
{
mfaEnrollmentId: 'new-enrollment-id',
enrolledAt: Date.now(),
phoneInfo: 'other-masked-phone-number'
}
]
};
beforeEach(() => {
mockFetch.setUp();
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [serverUser]
});
});
afterEach(mockFetch.tearDown);
it('should initialize the enrolled factors from the last reload', async () => {
await user.reload();
const mfaUser = multiFactor(user);
expect(mfaUser.enrolledFactors.length).to.eq(1);
const mfaInfo = mfaUser.enrolledFactors[0];
expect(mfaInfo.uid).to.eq('enrollment-id');
expect(mfaInfo.factorId).to.eq(FactorId.PHONE);
});
it('should update the enrolled factors if the user is reloaded', async () => {
await user.reload();
const mfaUser = multiFactor(user);
expect(mfaUser.enrolledFactors.length).to.eq(1);
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
users: [updatedServerUser]
});
await user.reload();
expect(mfaUser.enrolledFactors.length).to.eq(2);
});
});
});