Skip to content

Commit 29e1ae0

Browse files
prameshjbhparijat
authored andcommitted
Implement TOTP MFA enrollment flows (#6598)
* Implement TOTP MFA enrollment. This includes changes to mfa_info, addition of TotpMultiFactorImpl and unit tests. * move all TOTP implementation into core/mfa/assertions. We do not need to restrict this to platform_browser. SMS mfa is in platform_browser since it requires a recaptcha step. * Include a reference to Auth in TotpSecret This is cleaner than looking up the app and auth instance with getApp and getAuth. * addressed review comments, added totp subdirectory.
1 parent 4527cd0 commit 29e1ae0

File tree

10 files changed

+812
-139
lines changed

10 files changed

+812
-139
lines changed

common/api-review/auth.api.md

+4
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,10 @@ export function signOut(auth: Auth): Promise<void>;
750750
export interface TotpMultiFactorAssertion extends MultiFactorAssertion {
751751
}
752752

753+
// @public
754+
export interface TotpMultiFactorInfo extends MultiFactorInfo {
755+
}
756+
753757
// @public
754758
export class TwitterAuthProvider extends BaseOAuthProvider {
755759
constructor();

packages/auth/src/api/account_management/mfa.test.ts

+136-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import * as mockFetch from '../../../test/helpers/mock_fetch';
2727
import { ServerError } from '../errors';
2828
import {
2929
finalizeEnrollPhoneMfa,
30+
finalizeEnrollTotpMfa,
3031
startEnrollPhoneMfa,
32+
startEnrollTotpMfa,
3133
withdrawMfa
3234
} from './mfa';
3335

@@ -89,7 +91,7 @@ describe('api/account_management/startEnrollPhoneMfa', () => {
8991

9092
await expect(startEnrollPhoneMfa(auth, request)).to.be.rejectedWith(
9193
FirebaseError,
92-
"Firebase: This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key. (auth/invalid-user-token)."
94+
'auth/invalid-user-token'
9395
);
9496
expect(mock.calls[0].request).to.eql(request);
9597
});
@@ -152,6 +154,139 @@ describe('api/account_management/finalizeEnrollPhoneMfa', () => {
152154
);
153155

154156
await expect(finalizeEnrollPhoneMfa(auth, request)).to.be.rejectedWith(
157+
FirebaseError,
158+
'auth/invalid-verification-id'
159+
);
160+
expect(mock.calls[0].request).to.eql(request);
161+
});
162+
});
163+
164+
describe('api/account_management/startEnrollTotpMfa', () => {
165+
const request = {
166+
idToken: 'id-token',
167+
totpEnrollmentInfo: {}
168+
};
169+
170+
let auth: TestAuth;
171+
172+
beforeEach(async () => {
173+
auth = await testAuth();
174+
mockFetch.setUp();
175+
});
176+
177+
afterEach(mockFetch.tearDown);
178+
179+
it('should POST to the correct endpoint', async () => {
180+
const currentTime = new Date().toISOString();
181+
const mock = mockEndpoint(Endpoint.START_MFA_ENROLLMENT, {
182+
totpSessionInfo: {
183+
sharedSecretKey: 'key123',
184+
verificationCodeLength: 6,
185+
hashingAlgorithm: 'SHA256',
186+
periodSec: 30,
187+
sessionInfo: 'session-info',
188+
finalizeEnrollmentTime: currentTime
189+
}
190+
});
191+
192+
const response = await startEnrollTotpMfa(auth, request);
193+
expect(response.totpSessionInfo.sharedSecretKey).to.eq('key123');
194+
expect(response.totpSessionInfo.verificationCodeLength).to.eq(6);
195+
expect(response.totpSessionInfo.hashingAlgorithm).to.eq('SHA256');
196+
expect(response.totpSessionInfo.periodSec).to.eq(30);
197+
expect(response.totpSessionInfo.sessionInfo).to.eq('session-info');
198+
expect(response.totpSessionInfo.finalizeEnrollmentTime).to.eq(currentTime);
199+
expect(mock.calls[0].request).to.eql(request);
200+
expect(mock.calls[0].method).to.eq('POST');
201+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
202+
'application/json'
203+
);
204+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
205+
'testSDK/0.0.0'
206+
);
207+
});
208+
209+
it('should handle errors', async () => {
210+
const mock = mockEndpoint(
211+
Endpoint.START_MFA_ENROLLMENT,
212+
{
213+
error: {
214+
code: 400,
215+
message: ServerError.INVALID_ID_TOKEN,
216+
errors: [
217+
{
218+
message: ServerError.INVALID_ID_TOKEN
219+
}
220+
]
221+
}
222+
},
223+
400
224+
);
225+
226+
await expect(startEnrollTotpMfa(auth, request)).to.be.rejectedWith(
227+
FirebaseError,
228+
'auth/invalid-user-token'
229+
);
230+
expect(mock.calls[0].request).to.eql(request);
231+
});
232+
});
233+
234+
describe('api/account_management/finalizeEnrollTotpMfa', () => {
235+
const request = {
236+
idToken: 'id-token',
237+
displayName: 'my-otp-app',
238+
totpVerificationInfo: {
239+
sessionInfo: 'session-info',
240+
verificationCode: 'code'
241+
}
242+
};
243+
244+
let auth: TestAuth;
245+
246+
beforeEach(async () => {
247+
auth = await testAuth();
248+
mockFetch.setUp();
249+
});
250+
251+
afterEach(mockFetch.tearDown);
252+
253+
it('should POST to the correct endpoint', async () => {
254+
const mock = mockEndpoint(Endpoint.FINALIZE_MFA_ENROLLMENT, {
255+
idToken: 'id-token',
256+
refreshToken: 'refresh-token'
257+
});
258+
259+
const response = await finalizeEnrollTotpMfa(auth, request);
260+
expect(response.idToken).to.eq('id-token');
261+
expect(response.refreshToken).to.eq('refresh-token');
262+
expect(mock.calls[0].request).to.eql(request);
263+
expect(mock.calls[0].method).to.eq('POST');
264+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
265+
'application/json'
266+
);
267+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
268+
'testSDK/0.0.0'
269+
);
270+
});
271+
272+
it('should handle errors', async () => {
273+
const mock = mockEndpoint(
274+
Endpoint.FINALIZE_MFA_ENROLLMENT,
275+
{
276+
error: {
277+
code: 400,
278+
message: ServerError.INVALID_SESSION_INFO,
279+
errors: [
280+
{
281+
message: ServerError.INVALID_SESSION_INFO
282+
}
283+
]
284+
}
285+
},
286+
400
287+
);
288+
289+
await expect(finalizeEnrollTotpMfa(auth, request)).to.be.rejectedWith(
155290
FirebaseError,
156291
'Firebase: The verification ID used to create the phone auth credential is invalid. (auth/invalid-verification-id).'
157292
);

packages/auth/src/api/account_management/mfa.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { FinalizeMfaResponse } from '../authentication/mfa';
2626
import { AuthInternal } from '../../model/auth';
2727

2828
/**
29-
* MFA Info as returned by the API
29+
* MFA Info as returned by the API.
3030
*/
3131
interface BaseMfaEnrollment {
3232
mfaEnrollmentId: string;
@@ -35,16 +35,21 @@ interface BaseMfaEnrollment {
3535
}
3636

3737
/**
38-
* An MFA provided by SMS verification
38+
* An MFA provided by SMS verification.
3939
*/
4040
export interface PhoneMfaEnrollment extends BaseMfaEnrollment {
4141
phoneInfo: string;
4242
}
4343

4444
/**
45-
* MfaEnrollment can be any subtype of BaseMfaEnrollment, currently only PhoneMfaEnrollment is supported
45+
* An MFA provided by TOTP (Time-based One Time Password).
4646
*/
47-
export type MfaEnrollment = PhoneMfaEnrollment;
47+
export interface TotpMfaEnrollment extends BaseMfaEnrollment {}
48+
49+
/**
50+
* MfaEnrollment can be any subtype of BaseMfaEnrollment, currently only PhoneMfaEnrollment and TotpMfaEnrollment are supported.
51+
*/
52+
export type MfaEnrollment = PhoneMfaEnrollment | TotpMfaEnrollment;
4853

4954
export interface StartPhoneMfaEnrollmentRequest {
5055
idToken: string;
@@ -100,6 +105,66 @@ export function finalizeEnrollPhoneMfa(
100105
_addTidIfNecessary(auth, request)
101106
);
102107
}
108+
export interface StartTotpMfaEnrollmentRequest {
109+
idToken: string;
110+
totpEnrollmentInfo: {};
111+
tenantId?: string;
112+
}
113+
114+
export interface StartTotpMfaEnrollmentResponse {
115+
totpSessionInfo: {
116+
sharedSecretKey: string;
117+
verificationCodeLength: number;
118+
hashingAlgorithm: string;
119+
periodSec: number;
120+
sessionInfo: string;
121+
finalizeEnrollmentTime: number;
122+
};
123+
}
124+
125+
export function startEnrollTotpMfa(
126+
auth: AuthInternal,
127+
request: StartTotpMfaEnrollmentRequest
128+
): Promise<StartTotpMfaEnrollmentResponse> {
129+
return _performApiRequest<
130+
StartTotpMfaEnrollmentRequest,
131+
StartTotpMfaEnrollmentResponse
132+
>(
133+
auth,
134+
HttpMethod.POST,
135+
Endpoint.START_MFA_ENROLLMENT,
136+
_addTidIfNecessary(auth, request)
137+
);
138+
}
139+
140+
export interface TotpVerificationInfo {
141+
sessionInfo: string;
142+
verificationCode: string;
143+
}
144+
export interface FinalizeTotpMfaEnrollmentRequest {
145+
idToken: string;
146+
totpVerificationInfo: TotpVerificationInfo;
147+
displayName?: string | null;
148+
tenantId?: string;
149+
}
150+
151+
export interface FinalizeTotpMfaEnrollmentResponse
152+
extends FinalizeMfaResponse {}
153+
154+
export function finalizeEnrollTotpMfa(
155+
auth: AuthInternal,
156+
request: FinalizeTotpMfaEnrollmentRequest
157+
): Promise<FinalizeTotpMfaEnrollmentResponse> {
158+
return _performApiRequest<
159+
FinalizeTotpMfaEnrollmentRequest,
160+
FinalizeTotpMfaEnrollmentResponse
161+
>(
162+
auth,
163+
HttpMethod.POST,
164+
Endpoint.FINALIZE_MFA_ENROLLMENT,
165+
_addTidIfNecessary(auth, request)
166+
);
167+
}
103168

104169
export interface WithdrawMfaRequest {
105170
idToken: string;

packages/auth/src/mfa/assertions/totp.ts

-78
This file was deleted.

0 commit comments

Comments
 (0)