Skip to content

Commit cee33b9

Browse files
committed
integration tests for totp (#6784)
* totp integration test * test cases working with verified email * removing debug logs * changed test email and fixed handling of user delete for totp * reverting unwanted changes in helper.ts * modified comments
1 parent 655d6b3 commit cee33b9

File tree

4 files changed

+209
-5
lines changed

4 files changed

+209
-5
lines changed

packages/auth/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,14 @@
117117
"rollup": "2.79.1",
118118
"rollup-plugin-sourcemaps": "0.6.3",
119119
"rollup-plugin-typescript2": "0.31.2",
120+
<<<<<<< HEAD
120121
"selenium-webdriver": "4.5.0",
121122
"typescript": "4.7.4",
122123
"@types/express": "4.17.14"
124+
=======
125+
"typescript": "4.2.2",
126+
"totp-generator": "0.0.14"
127+
>>>>>>> 0a20bcf41 ( integration tests for totp (#6784))
123128
},
124129
"repository": {
125130
"directory": "packages/auth",

packages/auth/test/helpers/integration/helpers.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node
2323
import { _generateEventId } from '../../../src/core/util/event_id';
2424
import { getAppConfig, getEmulatorUrl } from './settings';
2525
import { resetEmulator } from './emulator_rest_helpers';
26-
26+
import totp from 'totp-generator';
2727
interface IntegrationTestAuth extends Auth {
2828
cleanUp(): Promise<void>;
2929
}
@@ -62,10 +62,12 @@ export function getTestInstance(requireEmulator = false): Auth {
6262
} else {
6363
// Clear out any new users that were created in the course of the test
6464
for (const user of createdUsers) {
65-
try {
66-
await user.delete();
67-
} catch {
68-
// Best effort. Maybe the test already deleted the user ¯\_(ツ)_/¯
65+
if (!user.email?.includes('donotdelete')) {
66+
try {
67+
await user.delete();
68+
} catch {
69+
// Best effort. Maybe the test already deleted the user ¯\_(ツ)_/¯
70+
}
6971
}
7072
}
7173
}
@@ -93,3 +95,25 @@ function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub {
9395
}
9496
});
9597
}
98+
99+
export function getTotpCode(
100+
sharedSecretKey: string,
101+
periodSec: number,
102+
verificationCodeLength: number
103+
): string {
104+
const token = totp(sharedSecretKey, {
105+
period: periodSec,
106+
digits: verificationCodeLength,
107+
algorithm: 'SHA-1'
108+
});
109+
110+
return token;
111+
}
112+
113+
export function delay(dt: number): Promise<void> {
114+
return new Promise(resolve => setTimeout(resolve, dt));
115+
}
116+
117+
export const email = '[email protected]';
118+
//1000000 is always incorrect since it has 7 digits and we expect 6.
119+
export const incorrectTotpCode = '1000000';
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* @license
3+
* Copyright 2022 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 chaiAsPromised from 'chai-as-promised';
20+
import sinonChai from 'sinon-chai';
21+
import {
22+
Auth,
23+
multiFactor,
24+
signInWithEmailAndPassword,
25+
getMultiFactorResolver
26+
} from '@firebase/auth';
27+
import { FirebaseError } from '@firebase/app';
28+
import {
29+
cleanUpTestInstance,
30+
getTestInstance,
31+
getTotpCode,
32+
delay,
33+
email,
34+
incorrectTotpCode
35+
} from '../../helpers/integration/helpers';
36+
37+
import {
38+
TotpMultiFactorGenerator,
39+
TotpSecret
40+
} from '../../../src/mfa/assertions/totp';
41+
42+
use(chaiAsPromised);
43+
use(sinonChai);
44+
45+
describe(' Integration tests: Mfa TOTP', () => {
46+
let auth: Auth;
47+
let totpSecret: TotpSecret;
48+
let displayName: string;
49+
beforeEach(async () => {
50+
auth = getTestInstance();
51+
displayName = 'totp-integration-test';
52+
});
53+
54+
afterEach(async () => {
55+
await cleanUpTestInstance(auth);
56+
});
57+
58+
it('should not enroll if incorrect totp supplied', async () => {
59+
const cr = await signInWithEmailAndPassword(auth, email, 'password');
60+
const mfaUser = multiFactor(cr.user);
61+
const session = await mfaUser.getSession();
62+
totpSecret = await TotpMultiFactorGenerator.generateSecret(session);
63+
const multiFactorAssertion =
64+
TotpMultiFactorGenerator.assertionForEnrollment(
65+
totpSecret,
66+
incorrectTotpCode
67+
);
68+
69+
await expect(
70+
mfaUser.enroll(multiFactorAssertion, displayName)
71+
).to.be.rejectedWith('auth/invalid-verification-code');
72+
});
73+
74+
it('should enroll using correct otp', async () => {
75+
const cr = await signInWithEmailAndPassword(auth, email, 'password');
76+
77+
const mfaUser = multiFactor(cr.user);
78+
79+
const session = await mfaUser.getSession();
80+
81+
totpSecret = await TotpMultiFactorGenerator.generateSecret(session);
82+
83+
const totpVerificationCode = getTotpCode(
84+
totpSecret.secretKey,
85+
totpSecret.codeIntervalSeconds,
86+
totpSecret.codeLength
87+
);
88+
89+
const multiFactorAssertion =
90+
TotpMultiFactorGenerator.assertionForEnrollment(
91+
totpSecret,
92+
totpVerificationCode
93+
);
94+
await expect(mfaUser.enroll(multiFactorAssertion, displayName)).to.be
95+
.fulfilled;
96+
});
97+
98+
it('should not allow sign-in with incorrect totp', async () => {
99+
let resolver;
100+
101+
try {
102+
await signInWithEmailAndPassword(auth, email, 'password');
103+
104+
throw new Error('Signin should not have been successful');
105+
} catch (error) {
106+
expect(error).to.be.an.instanceOf(FirebaseError);
107+
expect((error as any).code).to.eql('auth/multi-factor-auth-required');
108+
109+
resolver = getMultiFactorResolver(auth, error as any);
110+
expect(resolver.hints).to.have.length(1);
111+
112+
const assertion = TotpMultiFactorGenerator.assertionForSignIn(
113+
resolver.hints[0].uid,
114+
incorrectTotpCode
115+
);
116+
117+
await expect(resolver.resolveSignIn(assertion)).to.be.rejectedWith(
118+
'auth/invalid-verification-code'
119+
);
120+
}
121+
});
122+
123+
it('should allow sign-in with for correct totp and unenroll successfully', async () => {
124+
let resolver;
125+
126+
await delay(30 * 1000);
127+
//TODO(bhparijat) generate the otp code for the next time window by passing the appropriate
128+
//timestamp to avoid the 30s delay. The delay is needed because the otp code used for enrollment
129+
//cannot be reused for signing in.
130+
try {
131+
await signInWithEmailAndPassword(auth, email, 'password');
132+
133+
throw new Error('Signin should not have been successful');
134+
} catch (error) {
135+
expect(error).to.be.an.instanceOf(FirebaseError);
136+
expect((error as any).code).to.eql('auth/multi-factor-auth-required');
137+
138+
resolver = getMultiFactorResolver(auth, error as any);
139+
expect(resolver.hints).to.have.length(1);
140+
141+
const totpVerificationCode = getTotpCode(
142+
totpSecret.secretKey,
143+
totpSecret.codeIntervalSeconds,
144+
totpSecret.codeLength
145+
);
146+
const assertion = TotpMultiFactorGenerator.assertionForSignIn(
147+
resolver.hints[0].uid,
148+
totpVerificationCode
149+
);
150+
const userCredential = await resolver.resolveSignIn(assertion);
151+
152+
const mfaUser = multiFactor(userCredential.user);
153+
154+
await expect(mfaUser.unenroll(resolver.hints[0].uid)).to.be.fulfilled;
155+
}
156+
}).timeout(32000);
157+
});

packages/auth/totp-generator.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright 2022 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+
declare module 'totp-generator';

0 commit comments

Comments
 (0)