UNPKG

voluptasmollitia

Version:
318 lines (271 loc) 9 kB
/** * @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); }); }); });