From 2c02cc87b35f89fdc23a2307026690558a708e86 Mon Sep 17 00:00:00 2001 From: Parijat Bhatt Date: Fri, 11 Nov 2022 15:07:40 -0800 Subject: [PATCH 1/6] totp integration test --- .../auth/test/helpers/integration/helpers.ts | 20 +++ .../auth/test/integration/flows/totp.test.ts | 147 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 packages/auth/test/integration/flows/totp.test.ts diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 4f9518a6717..d2affbee918 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -23,7 +23,12 @@ import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node import { _generateEventId } from '../../../src/core/util/event_id'; import { getAppConfig, getEmulatorUrl } from './settings'; import { resetEmulator } from './emulator_rest_helpers'; +import { StartTotpMfaEnrollmentResponse } from '../../../src/api/account_management/mfa'; +//import * as otpauth from "https://deno.land/x/otpauth@v9.0.1/dist/otpauth.esm.js"; +//// +const totp = require('totp-generator'); +//import * as totp from 'otpauth'; interface IntegrationTestAuth extends Auth { cleanUp(): Promise; } @@ -93,3 +98,18 @@ function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { } }); } + +export async function mockTotp(sharedSecretKey: string, periodSec: number, verificationCodeLength: number){ + console.log("**** starting to mock totp"); + + let digits = 9; + let period = 30; + let secret = "private"; + const headers = new Headers(); + + let token = totp(sharedSecretKey, { period: periodSec, digits: verificationCodeLength }); + console.log("***"+token); + +return token + +} \ No newline at end of file diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts new file mode 100644 index 00000000000..e1445c75cd6 --- /dev/null +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -0,0 +1,147 @@ +/** + * @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 chaiAsPromised from 'chai-as-promised'; + import * as sinon from 'sinon'; + import sinonChai from 'sinon-chai'; + +//import { mockTotp } from '../../helpers/integration/helpers'; +import {Auth, createUserWithEmailAndPassword, multiFactor, signInAnonymously, signInWithEmailAndPassword, UserCredential} from '@firebase/auth'; + +import { + cleanUpTestInstance, + getTestInstance, + mockTotp, + randomEmail +} from '../../helpers/integration/helpers'; +import { MultiFactorAssertionImpl } from '../../../src/mfa/mfa_assertion'; + +import { MultiFactorSessionImpl } from '../../../src/mfa/mfa_session'; +import { TotpMultiFactorAssertionImpl, TotpMultiFactorGenerator, TotpSecret } from '../../../src/mfa/assertions/totp'; +import * as MFA from '../../../src/api/account_management/mfa'; +import { FirebaseError } from '@firebase/util'; + + + +use(chaiAsPromised); +use(sinonChai); + +const TOTP_COMB_A = { + + response: { sharedSecretKey: 'secretKey3', + verificationCodeLength: 30, + hashingAlgorithm: 'sha1', + periodSec:30, + sessionInfo: 'testsSessionInfo', + finalizeEnrollmentTime: Date.now() + }, + code: '...' + }; + + const TOTP_COMB_B = { + + response: { sharedSecretKey: 'secretKey2', + verificationCodeLength: 30, + hashingAlgorithm: 'sha1', + periodSec: 30, + sessionInfo: 'testsSessionInfo', + finalizeEnrollmentTime: Date.now() + }, + code: '...' + }; + +describe(' Integration tests: Mfa TOTP', () => { + let auth: Auth; + let idToken: string; + let signUpCred: UserCredential; + let email: string; + let assertion: MultiFactorAssertionImpl; + let _request: MFA.StartTotpMfaEnrollmentRequest; + let startMfaResponse: MFA.StartTotpMfaEnrollmentResponse; + let displayName: string; + beforeEach(async () => { + auth = getTestInstance(); + email =randomEmail(); + idToken = 'testIdToken'; + signUpCred = await createUserWithEmailAndPassword( + auth, + email, + 'password' + ); + await auth.signOut(); + }); + + afterEach(async () => { + await cleanUpTestInstance(auth); + + }); + it('should verify using otp', async () => { + + console.log(email); + const cr = await signInWithEmailAndPassword(auth, email, 'password'); + + startMfaResponse = { totpSessionInfo: TOTP_COMB_A.response} + + + + const mfaUser = multiFactor(cr.user); + sinon.spy(MultiFactorSessionImpl, '_fromIdtoken'); + + sinon.stub(mfaUser, 'getSession').returns( + Promise.resolve(MultiFactorSessionImpl._fromIdtoken(idToken, auth as any))); + + sinon.stub(MFA, 'startEnrollTotpMfa').callsFake((_auth,_request)=>{ + + return Promise.resolve(startMfaResponse) + }) + + + + const session = await mfaUser.getSession(); + + console.log(session); + + const totpSecret = await TotpMultiFactorGenerator.generateSecret( + session + ); + + console.log("**** totpSecret"+ totpSecret); + // https://stackoverflow.com/questions/48931815/sinon-stub-not-replacing-function + // https://stackoverflow.com/questions/61051247/chai-spies-expect-to-have-been-called-is-failing-on-local-methods + expect(MultiFactorSessionImpl._fromIdtoken).to.have.been.calledOnce; + //expect(TotpSecret._fromStartTotpMfaEnrollmentResponse).to.have.been.calledOnce; + expect(MFA.startEnrollTotpMfa).to.have.been.calledOnce; + + expect(await MFA.startEnrollTotpMfa(auth as any, _request)).to.eql(startMfaResponse) + + expect(totpSecret.secretKey).to.eql(startMfaResponse.totpSessionInfo.sharedSecretKey) + expect(totpSecret.codeLength).to.eql(startMfaResponse.totpSessionInfo.verificationCodeLength) + + const totpVerificationCode = await mockTotp(totpSecret.secretKey, totpSecret.codeLength, totpSecret.codeIntervalSeconds); + + const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( + totpSecret, + totpVerificationCode + ); + console.log(totpVerificationCode); + // auth/invalid-idToken + await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.rejectedWith('auth/invalid-user-token') + + + }) +}) \ No newline at end of file From 8718914691d82f8b3885f3a665193a8997454840 Mon Sep 17 00:00:00 2001 From: Parijat bhatt Date: Thu, 17 Nov 2022 01:44:30 -0800 Subject: [PATCH 2/6] test cases working with verified email --- packages/auth/test/helpers/api/helper.ts | 1 + .../test/helpers/integration/helpers.d.ts | 0 .../auth/test/helpers/integration/helpers.ts | 43 ++- .../auth/test/integration/flows/totp.test.ts | 253 +++++++++++++----- 4 files changed, 212 insertions(+), 85 deletions(-) create mode 100644 packages/auth/test/helpers/integration/helpers.d.ts diff --git a/packages/auth/test/helpers/api/helper.ts b/packages/auth/test/helpers/api/helper.ts index 0385ef62f66..395a0944261 100644 --- a/packages/auth/test/helpers/api/helper.ts +++ b/packages/auth/test/helpers/api/helper.ts @@ -30,3 +30,4 @@ export function mockEndpoint( ): Route { return mock(endpointUrl(endpoint), response, status); } + diff --git a/packages/auth/test/helpers/integration/helpers.d.ts b/packages/auth/test/helpers/integration/helpers.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index d2affbee918..9aa2cda1b16 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -22,8 +22,8 @@ import { Auth, User } from '../../../src/model/public_types'; import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node dist entrypoint depending on test env. import { _generateEventId } from '../../../src/core/util/event_id'; import { getAppConfig, getEmulatorUrl } from './settings'; -import { resetEmulator } from './emulator_rest_helpers'; -import { StartTotpMfaEnrollmentResponse } from '../../../src/api/account_management/mfa'; +import { getOobCodes, OobCodeSession, resetEmulator } from './emulator_rest_helpers'; +import * as config from '../../../../../config/project.json'; //import * as otpauth from "https://deno.land/x/otpauth@v9.0.1/dist/otpauth.esm.js"; //// @@ -62,6 +62,8 @@ export function getTestInstance(requireEmulator = false): Auth { auth.cleanUp = async () => { // If we're in an emulated environment, the emulator will clean up for us + + console.log('Auth cleanup should not be called'); if (emulatorUrl) { await resetEmulator(); } else { @@ -81,9 +83,13 @@ export function getTestInstance(requireEmulator = false): Auth { return auth; } -export async function cleanUpTestInstance(auth: Auth): Promise { +export async function cleanUpTestInstance(auth: Auth, tests? : string): Promise { await auth.signOut(); - await (auth as IntegrationTestAuth).cleanUp(); + + if(typeof tests === 'undefined') { + await (auth as IntegrationTestAuth).cleanUp(); + } + } function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { @@ -99,17 +105,28 @@ function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { }); } -export async function mockTotp(sharedSecretKey: string, periodSec: number, verificationCodeLength: number){ - console.log("**** starting to mock totp"); +export async function code(toEmail: string): Promise { + const codes = await getOobCodes(); + console.log('codes: ', codes); + + return codes.reverse().find(({ email }) => email === toEmail)!; +} + - let digits = 9; - let period = 30; - let secret = "private"; - const headers = new Headers(); +export function getTotpCode(sharedSecretKey: string, periodSec: number, verificationCodeLength: number, hashingAlgorithm: string){ + + let token = totp(sharedSecretKey, { period: periodSec, digits: verificationCodeLength, algorithm: 'SHA-1'}); - let token = totp(sharedSecretKey, { period: periodSec, digits: verificationCodeLength }); - console.log("***"+token); return token -} \ No newline at end of file +} + +export function delay(dt:number){ + + console.log('Delay called'); + + return new Promise(resolve => setTimeout(resolve, dt)); +} + +export const email = 'testemail@test.com'; diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts index e1445c75cd6..a0fb07ca9df 100644 --- a/packages/auth/test/integration/flows/totp.test.ts +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -21,127 +21,236 @@ import sinonChai from 'sinon-chai'; //import { mockTotp } from '../../helpers/integration/helpers'; -import {Auth, createUserWithEmailAndPassword, multiFactor, signInAnonymously, signInWithEmailAndPassword, UserCredential} from '@firebase/auth'; - +import {Auth, createUserWithEmailAndPassword, multiFactor, signInWithEmailAndPassword, UserCredential, sendEmailVerification, applyActionCode, getMultiFactorResolver} from '@firebase/auth'; +import { FirebaseError, getApp } from '@firebase/app'; import { cleanUpTestInstance, + code, getTestInstance, - mockTotp, - randomEmail + getTotpCode, + delay, + randomEmail, + verifyEmail, + email } from '../../helpers/integration/helpers'; import { MultiFactorAssertionImpl } from '../../../src/mfa/mfa_assertion'; import { MultiFactorSessionImpl } from '../../../src/mfa/mfa_session'; -import { TotpMultiFactorAssertionImpl, TotpMultiFactorGenerator, TotpSecret } from '../../../src/mfa/assertions/totp'; +import { TotpMultiFactorGenerator, TotpSecret } from '../../../src/mfa/assertions/totp'; import * as MFA from '../../../src/api/account_management/mfa'; -import { FirebaseError } from '@firebase/util'; +import { async } from '@firebase/util'; +import { UserCredentialImpl } from '../../../src/core/user/user_credential_impl'; +import { resolve } from 'dns'; +import { UserCredentialInternal } from '../../../internal'; +import { verify } from 'crypto'; use(chaiAsPromised); use(sinonChai); -const TOTP_COMB_A = { - - response: { sharedSecretKey: 'secretKey3', - verificationCodeLength: 30, - hashingAlgorithm: 'sha1', - periodSec:30, - sessionInfo: 'testsSessionInfo', - finalizeEnrollmentTime: Date.now() - }, - code: '...' - }; - - const TOTP_COMB_B = { - - response: { sharedSecretKey: 'secretKey2', - verificationCodeLength: 30, - hashingAlgorithm: 'sha1', - periodSec: 30, - sessionInfo: 'testsSessionInfo', - finalizeEnrollmentTime: Date.now() - }, - code: '...' - }; - describe(' Integration tests: Mfa TOTP', () => { + + let auth: Auth; let idToken: string; let signUpCred: UserCredential; - let email: string; + let totpSecret: TotpSecret; let assertion: MultiFactorAssertionImpl; let _request: MFA.StartTotpMfaEnrollmentRequest; let startMfaResponse: MFA.StartTotpMfaEnrollmentResponse; let displayName: string; beforeEach(async () => { auth = getTestInstance(); - email =randomEmail(); - idToken = 'testIdToken'; - signUpCred = await createUserWithEmailAndPassword( - auth, - email, - 'password' - ); - await auth.signOut(); + displayName = 'totp-integration-test'; }); afterEach(async () => { - await cleanUpTestInstance(auth); - + await cleanUpTestInstance(auth, 'totp'); + }); - it('should verify using otp', async () => { + it('should not enroll if incorrect totp supplied', async () => { + let session; + console.log(email); + console.log('session info for: ', getApp().options.projectId); + console.log('auth current User:', auth.currentUser); + + await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); + + const cr = await signInWithEmailAndPassword(auth, email, 'password'); + + console.log('signed In for totp'); + const mfaUser = multiFactor(cr.user); + + console.log('session info for: '); + session = await mfaUser.getSession(); + console.log(JSON.stringify(session)); + + + totpSecret = await TotpMultiFactorGenerator.generateSecret( + session + ); + + console.log("**** totpSecret****"); + console.log(totpSecret.secretKey); + console.log(totpSecret.codeLength); + console.log(totpSecret.codeIntervalSeconds); + console.log(totpSecret.hashingAlgorithm); + + const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); + + const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( + totpSecret, + totpVerificationCode + '0' + ); + + console.log(totpVerificationCode); + await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.rejectedWith('auth/invalid-verification-code'); + await auth.signOut(); + }) + it('should enroll using correct otp', async () => { + + let session; console.log(email); + console.log('session info for: ', getApp().options.projectId); + console.log('auth current User:', auth.currentUser); + await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); const cr = await signInWithEmailAndPassword(auth, email, 'password'); - startMfaResponse = { totpSessionInfo: TOTP_COMB_A.response} + console.log('signed In for totp'); + const mfaUser = multiFactor(cr.user); + console.log('session info for: '); - - const mfaUser = multiFactor(cr.user); - sinon.spy(MultiFactorSessionImpl, '_fromIdtoken'); - - sinon.stub(mfaUser, 'getSession').returns( - Promise.resolve(MultiFactorSessionImpl._fromIdtoken(idToken, auth as any))); - - sinon.stub(MFA, 'startEnrollTotpMfa').callsFake((_auth,_request)=>{ - return Promise.resolve(startMfaResponse) - }) - - + session = await mfaUser.getSession(); - const session = await mfaUser.getSession(); - - console.log(session); - const totpSecret = await TotpMultiFactorGenerator.generateSecret( + console.log('session'); + console.log(JSON.stringify(session)); + + + totpSecret = await TotpMultiFactorGenerator.generateSecret( session ); - console.log("**** totpSecret"+ totpSecret); - // https://stackoverflow.com/questions/48931815/sinon-stub-not-replacing-function - // https://stackoverflow.com/questions/61051247/chai-spies-expect-to-have-been-called-is-failing-on-local-methods - expect(MultiFactorSessionImpl._fromIdtoken).to.have.been.calledOnce; - //expect(TotpSecret._fromStartTotpMfaEnrollmentResponse).to.have.been.calledOnce; - expect(MFA.startEnrollTotpMfa).to.have.been.calledOnce; + console.log("**** totpSecret****"); - expect(await MFA.startEnrollTotpMfa(auth as any, _request)).to.eql(startMfaResponse) - - expect(totpSecret.secretKey).to.eql(startMfaResponse.totpSessionInfo.sharedSecretKey) - expect(totpSecret.codeLength).to.eql(startMfaResponse.totpSessionInfo.verificationCodeLength) + console.log(totpSecret.secretKey); + console.log(totpSecret.codeLength); + console.log(totpSecret.codeIntervalSeconds); + console.log(totpSecret.hashingAlgorithm); - const totpVerificationCode = await mockTotp(totpSecret.secretKey, totpSecret.codeLength, totpSecret.codeIntervalSeconds); + + + + const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( totpSecret, totpVerificationCode ); console.log(totpVerificationCode); - // auth/invalid-idToken - await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.rejectedWith('auth/invalid-user-token') + + await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.fulfilled; + await auth.signOut(); }) + + it('should not allow sign-in with incorrect totp', async () => { + let session; + let cr; + let resolver; + console.log(email); + console.log('session info for: ', getApp().options.projectId); + await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); + // Added a delay so that getTotpCode() actually generates a new totp code + await delay(30*1000); + try{ + + const userCredential = await signInWithEmailAndPassword(auth, email, 'password'); + + console.log('success: ', userCredential); + + throw new Error('Signin should not have been successful'); + + } catch(error ){ + + + console.log('error occured: ', (error as any).code); + expect((error as any).code).to.eql('auth/multi-factor-auth-required'); + + resolver = getMultiFactorResolver(auth,error as any); + console.log(resolver.hints, totpSecret.secretKey); + expect(resolver.hints).to.have.length(1); + + const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); + console.log(totpVerificationCode, resolver.hints[0].uid ) + const assertion = TotpMultiFactorGenerator.assertionForSignIn( + resolver.hints[0].uid, + totpVerificationCode + '0' + ); + + console.log(assertion); + + + await expect(resolver.resolveSignIn(assertion)).to.be.rejectedWith('auth/invalid-verification-code'); + + await auth.signOut(); + + } + + + }).timeout(31000); + + it('should allow sign-in with for correct totp and unenroll successfully', async() => { + + let resolver; + + console.log(email); + console.log('session info for: ', getApp().options.projectId); + + await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); + // Added a delay so that getTotpCode() actually generates a new totp code + await delay(30*1000); + try{ + + const userCredential = await signInWithEmailAndPassword(auth, email, 'password'); + + console.log('success: ', userCredential); + + throw new Error('Signin should not have been successful'); + + } catch(error ){ + + + console.log('error occured: ', (error as any).code); + expect((error as any).code).to.eql('auth/multi-factor-auth-required'); + + resolver = getMultiFactorResolver(auth,error as any); + console.log(resolver.hints, totpSecret.secretKey); + expect(resolver.hints).to.have.length(1); + + const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); + console.log(totpVerificationCode, resolver.hints[0].uid ) + const assertion = TotpMultiFactorGenerator.assertionForSignIn( + resolver.hints[0].uid, + totpVerificationCode + ); + + console.log(assertion); + + + const userCredential = await resolver.resolveSignIn(assertion); + + const mfaUser = multiFactor(userCredential.user); + + await expect(mfaUser.unenroll(resolver.hints[0].uid)).to.be.fulfilled; + + await auth.signOut(); + + } + }).timeout(35000); }) \ No newline at end of file From 9c054b597522af442c53c24f13f95ccc573de689 Mon Sep 17 00:00:00 2001 From: Parijat bhatt Date: Thu, 17 Nov 2022 22:17:34 -0800 Subject: [PATCH 3/6] removing debug logs --- packages/auth/package.json | 3 +- packages/auth/test/helpers/api/helper.ts | 1 - .../test/helpers/integration/helpers.d.ts | 0 .../auth/test/helpers/integration/helpers.ts | 50 ++-- .../auth/test/integration/flows/totp.test.ts | 280 ++++++------------ packages/auth/totp-generator.d.ts | 18 ++ 6 files changed, 134 insertions(+), 218 deletions(-) delete mode 100644 packages/auth/test/helpers/integration/helpers.d.ts create mode 100644 packages/auth/totp-generator.d.ts diff --git a/packages/auth/package.json b/packages/auth/package.json index 23eeaa5e2c9..6124c0cbe11 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -118,7 +118,8 @@ "rollup": "2.72.1", "rollup-plugin-sourcemaps": "0.6.3", "rollup-plugin-typescript2": "0.31.2", - "typescript": "4.2.2" + "typescript": "4.2.2", + "totp-generator": "0.0.14" }, "repository": { "directory": "packages/auth", diff --git a/packages/auth/test/helpers/api/helper.ts b/packages/auth/test/helpers/api/helper.ts index 395a0944261..0385ef62f66 100644 --- a/packages/auth/test/helpers/api/helper.ts +++ b/packages/auth/test/helpers/api/helper.ts @@ -30,4 +30,3 @@ export function mockEndpoint( ): Route { return mock(endpointUrl(endpoint), response, status); } - diff --git a/packages/auth/test/helpers/integration/helpers.d.ts b/packages/auth/test/helpers/integration/helpers.d.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 9aa2cda1b16..2bb2ba909db 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -22,13 +22,12 @@ import { Auth, User } from '../../../src/model/public_types'; import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node dist entrypoint depending on test env. import { _generateEventId } from '../../../src/core/util/event_id'; import { getAppConfig, getEmulatorUrl } from './settings'; -import { getOobCodes, OobCodeSession, resetEmulator } from './emulator_rest_helpers'; -import * as config from '../../../../../config/project.json'; - -//import * as otpauth from "https://deno.land/x/otpauth@v9.0.1/dist/otpauth.esm.js"; -//// -const totp = require('totp-generator'); -//import * as totp from 'otpauth'; +import { + getOobCodes, + OobCodeSession, + resetEmulator +} from './emulator_rest_helpers'; +import totp from 'totp-generator'; interface IntegrationTestAuth extends Auth { cleanUp(): Promise; } @@ -62,8 +61,6 @@ export function getTestInstance(requireEmulator = false): Auth { auth.cleanUp = async () => { // If we're in an emulated environment, the emulator will clean up for us - - console.log('Auth cleanup should not be called'); if (emulatorUrl) { await resetEmulator(); } else { @@ -83,13 +80,15 @@ export function getTestInstance(requireEmulator = false): Auth { return auth; } -export async function cleanUpTestInstance(auth: Auth, tests? : string): Promise { +export async function cleanUpTestInstance( + auth: Auth, + tests?: string +): Promise { await auth.signOut(); - if(typeof tests === 'undefined') { + if (typeof tests === 'undefined') { await (auth as IntegrationTestAuth).cleanUp(); } - } function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { @@ -107,26 +106,25 @@ function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { export async function code(toEmail: string): Promise { const codes = await getOobCodes(); - console.log('codes: ', codes); - return codes.reverse().find(({ email }) => email === toEmail)!; } +export function getTotpCode( + sharedSecretKey: string, + periodSec: number, + verificationCodeLength: number +): string { + const token = totp(sharedSecretKey, { + period: periodSec, + digits: verificationCodeLength, + algorithm: 'SHA-1' + }); -export function getTotpCode(sharedSecretKey: string, periodSec: number, verificationCodeLength: number, hashingAlgorithm: string){ - - let token = totp(sharedSecretKey, { period: periodSec, digits: verificationCodeLength, algorithm: 'SHA-1'}); - - -return token - + return token; } -export function delay(dt:number){ - - console.log('Delay called'); - - return new Promise(resolve => setTimeout(resolve, dt)); +export function delay(dt: number): Promise { + return new Promise(resolve => setTimeout(resolve, dt)); } export const email = 'testemail@test.com'; diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts index a0fb07ca9df..71424ba285d 100644 --- a/packages/auth/test/integration/flows/totp.test.ts +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -15,242 +15,142 @@ * limitations under the License. */ - import { expect, use } from 'chai'; - import chaiAsPromised from 'chai-as-promised'; - import * as sinon from 'sinon'; - import sinonChai from 'sinon-chai'; - -//import { mockTotp } from '../../helpers/integration/helpers'; -import {Auth, createUserWithEmailAndPassword, multiFactor, signInWithEmailAndPassword, UserCredential, sendEmailVerification, applyActionCode, getMultiFactorResolver} from '@firebase/auth'; -import { FirebaseError, getApp } from '@firebase/app'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinonChai from 'sinon-chai'; +import { + Auth, + multiFactor, + signInWithEmailAndPassword, + getMultiFactorResolver +} from '@firebase/auth'; +import { FirebaseError } from '@firebase/app'; import { cleanUpTestInstance, - code, getTestInstance, getTotpCode, delay, - randomEmail, - verifyEmail, email } from '../../helpers/integration/helpers'; -import { MultiFactorAssertionImpl } from '../../../src/mfa/mfa_assertion'; - -import { MultiFactorSessionImpl } from '../../../src/mfa/mfa_session'; -import { TotpMultiFactorGenerator, TotpSecret } from '../../../src/mfa/assertions/totp'; -import * as MFA from '../../../src/api/account_management/mfa'; -import { async } from '@firebase/util'; -import { UserCredentialImpl } from '../../../src/core/user/user_credential_impl'; -import { resolve } from 'dns'; -import { UserCredentialInternal } from '../../../internal'; -import { verify } from 'crypto'; - +import { + TotpMultiFactorGenerator, + TotpSecret +} from '../../../src/mfa/assertions/totp'; use(chaiAsPromised); use(sinonChai); describe(' Integration tests: Mfa TOTP', () => { - - - let auth: Auth; - let idToken: string; - let signUpCred: UserCredential; - let totpSecret: TotpSecret; - let assertion: MultiFactorAssertionImpl; - let _request: MFA.StartTotpMfaEnrollmentRequest; - let startMfaResponse: MFA.StartTotpMfaEnrollmentResponse; - let displayName: string; + let auth: Auth; + let totpSecret: TotpSecret; + let displayName: string; beforeEach(async () => { auth = getTestInstance(); displayName = 'totp-integration-test'; }); - + afterEach(async () => { await cleanUpTestInstance(auth, 'totp'); - }); it('should not enroll if incorrect totp supplied', async () => { - let session; - console.log(email); - console.log('session info for: ', getApp().options.projectId); - console.log('auth current User:', auth.currentUser); + const cr = await signInWithEmailAndPassword(auth, email, 'password'); + const mfaUser = multiFactor(cr.user); + const session = await mfaUser.getSession(); + totpSecret = await TotpMultiFactorGenerator.generateSecret(session); + const incorrectTotpCode = '1000000'; + const multiFactorAssertion = + TotpMultiFactorGenerator.assertionForEnrollment( + totpSecret, + incorrectTotpCode + ); - await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); + await expect( + mfaUser.enroll(multiFactorAssertion, displayName) + ).to.be.rejectedWith('auth/invalid-verification-code'); + }); + it('should enroll using correct otp', async () => { const cr = await signInWithEmailAndPassword(auth, email, 'password'); - console.log('signed In for totp'); - const mfaUser = multiFactor(cr.user); + const mfaUser = multiFactor(cr.user); - console.log('session info for: '); - session = await mfaUser.getSession(); - console.log(JSON.stringify(session)); - - - totpSecret = await TotpMultiFactorGenerator.generateSecret( - session - ); + const session = await mfaUser.getSession(); - console.log("**** totpSecret****"); - console.log(totpSecret.secretKey); - console.log(totpSecret.codeLength); - console.log(totpSecret.codeIntervalSeconds); - console.log(totpSecret.hashingAlgorithm); + totpSecret = await TotpMultiFactorGenerator.generateSecret(session); - const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); - - const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( - totpSecret, - totpVerificationCode + '0' + const totpVerificationCode = getTotpCode( + totpSecret.secretKey, + totpSecret.codeIntervalSeconds, + totpSecret.codeLength ); - console.log(totpVerificationCode); - await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.rejectedWith('auth/invalid-verification-code'); - await auth.signOut(); - }) - it('should enroll using correct otp', async () => { - - let session; - console.log(email); - console.log('session info for: ', getApp().options.projectId); - console.log('auth current User:', auth.currentUser); - await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); - const cr = await signInWithEmailAndPassword(auth, email, 'password'); - - console.log('signed In for totp'); - const mfaUser = multiFactor(cr.user); - - console.log('session info for: '); - - - session = await mfaUser.getSession(); - - - console.log('session'); - console.log(JSON.stringify(session)); - - - totpSecret = await TotpMultiFactorGenerator.generateSecret( - session - ); - - console.log("**** totpSecret****"); - - console.log(totpSecret.secretKey); - console.log(totpSecret.codeLength); - console.log(totpSecret.codeIntervalSeconds); - console.log(totpSecret.hashingAlgorithm); - - - - - const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); - - const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( + const multiFactorAssertion = + TotpMultiFactorGenerator.assertionForEnrollment( totpSecret, totpVerificationCode ); - console.log(totpVerificationCode); - - await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be.fulfilled; - - await auth.signOut(); - - }) - - it('should not allow sign-in with incorrect totp', async () => { - let session; - let cr; - let resolver; - console.log(email); - console.log('session info for: ', getApp().options.projectId); - await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); - // Added a delay so that getTotpCode() actually generates a new totp code - await delay(30*1000); - try{ - - const userCredential = await signInWithEmailAndPassword(auth, email, 'password'); - - console.log('success: ', userCredential); - - throw new Error('Signin should not have been successful'); - - } catch(error ){ + await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be + .fulfilled; + }); - - console.log('error occured: ', (error as any).code); - expect((error as any).code).to.eql('auth/multi-factor-auth-required'); + it('should not allow sign-in with incorrect totp', async () => { + let resolver; - resolver = getMultiFactorResolver(auth,error as any); - console.log(resolver.hints, totpSecret.secretKey); - expect(resolver.hints).to.have.length(1); + try { + await signInWithEmailAndPassword(auth, email, 'password'); - const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); - console.log(totpVerificationCode, resolver.hints[0].uid ) - const assertion = TotpMultiFactorGenerator.assertionForSignIn( - resolver.hints[0].uid, - totpVerificationCode + '0' - ); + throw new Error('Signin should not have been successful'); + } catch (error) { + expect(error).to.be.an.instanceOf(FirebaseError); + expect((error as any).code).to.eql('auth/multi-factor-auth-required'); - console.log(assertion); + resolver = getMultiFactorResolver(auth, error as any); + expect(resolver.hints).to.have.length(1); + const incorrectTotpCode = '1000000'; + const assertion = TotpMultiFactorGenerator.assertionForSignIn( + resolver.hints[0].uid, + incorrectTotpCode + ); - - await expect(resolver.resolveSignIn(assertion)).to.be.rejectedWith('auth/invalid-verification-code'); - - await auth.signOut(); - + await expect(resolver.resolveSignIn(assertion)).to.be.rejectedWith( + 'auth/invalid-verification-code' + ); } - - - }).timeout(31000); - - it('should allow sign-in with for correct totp and unenroll successfully', async() => { - - let resolver; - - console.log(email); - console.log('session info for: ', getApp().options.projectId); - - await expect(createUserWithEmailAndPassword(auth, email, 'password')).to.be.rejectedWith('auth/email-already-in-use'); - // Added a delay so that getTotpCode() actually generates a new totp code - await delay(30*1000); - try{ + }); - const userCredential = await signInWithEmailAndPassword(auth, email, 'password'); + it('should allow sign-in with for correct totp and unenroll successfully', async () => { + let resolver; - console.log('success: ', userCredential); - - throw new Error('Signin should not have been successful'); + await delay(30 * 1000); + // Added a delay so that getTotpCode() actually generates a new totp code - } catch(error ){ + try { + await signInWithEmailAndPassword(auth, email, 'password'); - - console.log('error occured: ', (error as any).code); - expect((error as any).code).to.eql('auth/multi-factor-auth-required'); + throw new Error('Signin should not have been successful'); + } catch (error) { + expect(error).to.be.an.instanceOf(FirebaseError); + expect((error as any).code).to.eql('auth/multi-factor-auth-required'); - resolver = getMultiFactorResolver(auth,error as any); - console.log(resolver.hints, totpSecret.secretKey); - expect(resolver.hints).to.have.length(1); + resolver = getMultiFactorResolver(auth, error as any); + expect(resolver.hints).to.have.length(1); - const totpVerificationCode = getTotpCode(totpSecret.secretKey, totpSecret.codeIntervalSeconds, totpSecret.codeLength, totpSecret.hashingAlgorithm); - console.log(totpVerificationCode, resolver.hints[0].uid ) - const assertion = TotpMultiFactorGenerator.assertionForSignIn( - resolver.hints[0].uid, - totpVerificationCode - ); + const totpVerificationCode = getTotpCode( + totpSecret.secretKey, + totpSecret.codeIntervalSeconds, + totpSecret.codeLength + ); + const assertion = TotpMultiFactorGenerator.assertionForSignIn( + resolver.hints[0].uid, + totpVerificationCode + ); + const userCredential = await resolver.resolveSignIn(assertion); - console.log(assertion); + const mfaUser = multiFactor(userCredential.user); - - const userCredential = await resolver.resolveSignIn(assertion); - - const mfaUser = multiFactor(userCredential.user); - - await expect(mfaUser.unenroll(resolver.hints[0].uid)).to.be.fulfilled; - - await auth.signOut(); - + await expect(mfaUser.unenroll(resolver.hints[0].uid)).to.be.fulfilled; } - }).timeout(35000); -}) \ No newline at end of file + }).timeout(31000); +}); diff --git a/packages/auth/totp-generator.d.ts b/packages/auth/totp-generator.d.ts new file mode 100644 index 00000000000..01defa03c6a --- /dev/null +++ b/packages/auth/totp-generator.d.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2022 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. + */ + +declare module 'totp-generator'; From 10aa3f18523b7f5ab3a403151598be8e51f780d7 Mon Sep 17 00:00:00 2001 From: Parijat bhatt Date: Fri, 18 Nov 2022 14:05:22 -0800 Subject: [PATCH 4/6] changed test email and fixed handling of user delete for totp --- packages/auth/test/helpers/integration/helpers.ts | 13 ++++++++----- packages/auth/test/integration/flows/totp.test.ts | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 2bb2ba909db..962fc76fa45 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -66,10 +66,12 @@ export function getTestInstance(requireEmulator = false): Auth { } else { // Clear out any new users that were created in the course of the test for (const user of createdUsers) { - try { - await user.delete(); - } catch { - // Best effort. Maybe the test already deleted the user ¯\_(ツ)_/¯ + if (!user.email?.includes('donotdelete')) { + try { + await user.delete(); + } catch { + // Best effort. Maybe the test already deleted the user ¯\_(ツ)_/¯ + } } } } @@ -127,4 +129,5 @@ export function delay(dt: number): Promise { return new Promise(resolve => setTimeout(resolve, dt)); } -export const email = 'testemail@test.com'; +export const email = 'totpuser-donotdelete@test.com'; +export const incorrectTotpCode = '1000000'; diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts index 71424ba285d..6fe9d191bb7 100644 --- a/packages/auth/test/integration/flows/totp.test.ts +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -30,7 +30,8 @@ import { getTestInstance, getTotpCode, delay, - email + email, + incorrectTotpCode } from '../../helpers/integration/helpers'; import { @@ -59,7 +60,6 @@ describe(' Integration tests: Mfa TOTP', () => { const mfaUser = multiFactor(cr.user); const session = await mfaUser.getSession(); totpSecret = await TotpMultiFactorGenerator.generateSecret(session); - const incorrectTotpCode = '1000000'; const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment( totpSecret, @@ -108,7 +108,7 @@ describe(' Integration tests: Mfa TOTP', () => { resolver = getMultiFactorResolver(auth, error as any); expect(resolver.hints).to.have.length(1); - const incorrectTotpCode = '1000000'; + const assertion = TotpMultiFactorGenerator.assertionForSignIn( resolver.hints[0].uid, incorrectTotpCode @@ -152,5 +152,5 @@ describe(' Integration tests: Mfa TOTP', () => { await expect(mfaUser.unenroll(resolver.hints[0].uid)).to.be.fulfilled; } - }).timeout(31000); + }).timeout(32000); }); From c56b85d288010ed787beea1bb583801fed883101 Mon Sep 17 00:00:00 2001 From: Parijat bhatt Date: Fri, 18 Nov 2022 14:21:39 -0800 Subject: [PATCH 5/6] reverting unwanted changes in helper.ts --- .../auth/test/helpers/integration/helpers.ts | 21 +++---------------- .../auth/test/integration/flows/totp.test.ts | 2 +- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 962fc76fa45..54320df9965 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -22,11 +22,7 @@ import { Auth, User } from '../../../src/model/public_types'; import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node dist entrypoint depending on test env. import { _generateEventId } from '../../../src/core/util/event_id'; import { getAppConfig, getEmulatorUrl } from './settings'; -import { - getOobCodes, - OobCodeSession, - resetEmulator -} from './emulator_rest_helpers'; +import { resetEmulator } from './emulator_rest_helpers'; import totp from 'totp-generator'; interface IntegrationTestAuth extends Auth { cleanUp(): Promise; @@ -82,15 +78,9 @@ export function getTestInstance(requireEmulator = false): Auth { return auth; } -export async function cleanUpTestInstance( - auth: Auth, - tests?: string -): Promise { +export async function cleanUpTestInstance(auth: Auth): Promise { await auth.signOut(); - - if (typeof tests === 'undefined') { - await (auth as IntegrationTestAuth).cleanUp(); - } + await (auth as IntegrationTestAuth).cleanUp(); } function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { @@ -106,11 +96,6 @@ function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { }); } -export async function code(toEmail: string): Promise { - const codes = await getOobCodes(); - return codes.reverse().find(({ email }) => email === toEmail)!; -} - export function getTotpCode( sharedSecretKey: string, periodSec: number, diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts index 6fe9d191bb7..2e951fc4fba 100644 --- a/packages/auth/test/integration/flows/totp.test.ts +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -52,7 +52,7 @@ describe(' Integration tests: Mfa TOTP', () => { }); afterEach(async () => { - await cleanUpTestInstance(auth, 'totp'); + await cleanUpTestInstance(auth); }); it('should not enroll if incorrect totp supplied', async () => { From 3f9736fd6c6cde848c534e143eec44d0c3ce95f5 Mon Sep 17 00:00:00 2001 From: Parijat bhatt Date: Mon, 21 Nov 2022 21:26:45 -0800 Subject: [PATCH 6/6] modified comments --- packages/auth/test/helpers/integration/helpers.ts | 1 + packages/auth/test/integration/flows/totp.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 54320df9965..a7ca9120344 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -115,4 +115,5 @@ export function delay(dt: number): Promise { } export const email = 'totpuser-donotdelete@test.com'; +//1000000 is always incorrect since it has 7 digits and we expect 6. export const incorrectTotpCode = '1000000'; diff --git a/packages/auth/test/integration/flows/totp.test.ts b/packages/auth/test/integration/flows/totp.test.ts index 2e951fc4fba..dd2b2846055 100644 --- a/packages/auth/test/integration/flows/totp.test.ts +++ b/packages/auth/test/integration/flows/totp.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2020 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,8 +124,9 @@ describe(' Integration tests: Mfa TOTP', () => { let resolver; await delay(30 * 1000); - // Added a delay so that getTotpCode() actually generates a new totp code - + //TODO(bhparijat) generate the otp code for the next time window by passing the appropriate + //timestamp to avoid the 30s delay. The delay is needed because the otp code used for enrollment + //cannot be reused for signing in. try { await signInWithEmailAndPassword(auth, email, 'password');