Skip to content

Commit f9a4390

Browse files
committed
Add verifyBeforeUpdate to auth-next
Also add a couple edge cases that I missed first pass around
1 parent 1e25900 commit f9a4390

File tree

10 files changed

+302
-54
lines changed

10 files changed

+302
-54
lines changed

packages-exp/auth-exp/src/api/account_management/email_and_password.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Operation } from '@firebase/auth-types-exp';
2020
import { Endpoint, HttpMethod, _performApiRequest } from '..';
2121
import { Auth } from '../../model/auth';
2222
import { IdTokenResponse } from '../../model/id_token';
23+
import { MfaEnrollment } from './mfa';
2324

2425
export interface ResetPasswordRequest {
2526
oobCode: string;
@@ -30,6 +31,7 @@ export interface ResetPasswordResponse {
3031
email: string;
3132
newEmail?: string;
3233
requestType?: Operation;
34+
mfaInfo?: MfaEnrollment;
3335
}
3436

3537
export async function resetPassword(

packages-exp/auth-exp/src/api/authentication/email_and_password.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ import {
3333
sendPasswordResetEmail,
3434
sendSignInLinkToEmail,
3535
signInWithPassword,
36-
VerifyEmailRequest
36+
VerifyEmailRequest,
37+
verifyAndChangeEmail,
38+
VerifyAndChangeEmailRequest
3739
} from './email_and_password';
3840
import { Operation } from '@firebase/auth-types-exp';
3941

@@ -269,3 +271,61 @@ describe('api/authentication/sendSignInLinkToEmail', () => {
269271
expect(mock.calls[0].request).to.eql(request);
270272
});
271273
});
274+
275+
describe('api/authentication/verifyAndChangeEmail', () => {
276+
const request: VerifyAndChangeEmailRequest = {
277+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
278+
idToken: 'id-token',
279+
newEmail: '[email protected]'
280+
};
281+
282+
let auth: Auth;
283+
284+
beforeEach(async () => {
285+
auth = await testAuth();
286+
mockFetch.setUp();
287+
});
288+
289+
afterEach(mockFetch.tearDown);
290+
291+
it('should POST to the correct endpoint', async () => {
292+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
293+
294+
});
295+
296+
const response = await verifyAndChangeEmail(auth, request);
297+
expect(response.email).to.eq('[email protected]');
298+
expect(mock.calls[0].request).to.eql(request);
299+
expect(mock.calls[0].method).to.eq('POST');
300+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
301+
'application/json'
302+
);
303+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
304+
'testSDK/0.0.0'
305+
);
306+
});
307+
308+
it('should handle errors', async () => {
309+
const mock = mockEndpoint(
310+
Endpoint.SEND_OOB_CODE,
311+
{
312+
error: {
313+
code: 400,
314+
message: ServerError.INVALID_EMAIL,
315+
errors: [
316+
{
317+
message: ServerError.INVALID_EMAIL
318+
}
319+
]
320+
}
321+
},
322+
400
323+
);
324+
325+
await expect(verifyAndChangeEmail(auth, request)).to.be.rejectedWith(
326+
FirebaseError,
327+
'Firebase: The email address is badly formatted. (auth/invalid-email).'
328+
);
329+
expect(mock.calls[0].request).to.eql(request);
330+
});
331+
});

packages-exp/auth-exp/src/api/authentication/email_and_password.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,20 @@ export interface EmailSignInRequest extends GetOobCodeRequest {
7878
email: string;
7979
}
8080

81+
export interface VerifyAndChangeEmailRequest extends GetOobCodeRequest {
82+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL;
83+
idToken: IdToken;
84+
newEmail: string;
85+
}
86+
8187
interface GetOobCodeResponse {
8288
email: string;
8389
}
8490

8591
export interface VerifyEmailResponse extends GetOobCodeResponse {}
8692
export interface PasswordResetResponse extends GetOobCodeResponse {}
8793
export interface EmailSignInResponse extends GetOobCodeResponse {}
94+
export interface VerifyAndChangeEmailResponse extends GetOobCodeRequest {}
8895

8996
async function sendOobCode(
9097
auth: Auth,
@@ -118,3 +125,10 @@ export async function sendSignInLinkToEmail(
118125
): Promise<EmailSignInResponse> {
119126
return sendOobCode(auth, request);
120127
}
128+
129+
export async function verifyAndChangeEmail(
130+
auth: Auth,
131+
request: VerifyAndChangeEmailRequest
132+
): Promise<VerifyAndChangeEmailResponse> {
133+
return sendOobCode(auth, request);
134+
}

packages-exp/auth-exp/src/core/strategies/email.test.ts

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import { ServerError } from '../../api/errors';
3131
import { Auth } from '../../model/auth';
3232
import { User } from '../../model/user';
3333
import * as location from '../util/location';
34-
import { fetchSignInMethodsForEmail, sendEmailVerification } from './email';
34+
import {
35+
fetchSignInMethodsForEmail,
36+
sendEmailVerification,
37+
verifyBeforeUpdateEmail
38+
} from './email';
3539

3640
use(chaiAsPromised);
3741
use(sinonChai);
@@ -107,18 +111,15 @@ describe('core/strategies/fetchSignInMethodsForEmail', () => {
107111

108112
describe('core/strategies/sendEmailVerification', () => {
109113
const email = '[email protected]';
110-
const idToken = 'id-token';
114+
const idToken = 'access-token';
111115
let user: User;
112116
let auth: Auth;
113-
let idTokenStub: SinonStub;
114117
let reloadStub: SinonStub;
115118

116119
beforeEach(async () => {
117120
auth = await testAuth();
118-
user = testUser(auth, 'my-user-uid', email);
121+
user = testUser(auth, 'my-user-uid', email, true);
119122
mockFetch.setUp();
120-
idTokenStub = stub(user, 'getIdToken');
121-
idTokenStub.callsFake(async () => idToken);
122123
reloadStub = stub(user, 'reload');
123124
});
124125

@@ -214,3 +215,115 @@ describe('core/strategies/sendEmailVerification', () => {
214215
});
215216
});
216217
});
218+
219+
describe('core/strategies/verifyBeforeUpdateEmail', () => {
220+
const email = '[email protected]';
221+
const newEmail = '[email protected]';
222+
const idToken = 'access-token';
223+
let user: User;
224+
let auth: Auth;
225+
let reloadStub: SinonStub;
226+
227+
beforeEach(async () => {
228+
auth = await testAuth();
229+
user = testUser(auth, 'my-user-uid', email, true);
230+
mockFetch.setUp();
231+
reloadStub = stub(user, 'reload');
232+
});
233+
234+
afterEach(() => {
235+
mockFetch.tearDown();
236+
restore();
237+
});
238+
239+
it('should send the email verification', async () => {
240+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
241+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
242+
email
243+
});
244+
245+
await verifyBeforeUpdateEmail(user, newEmail);
246+
247+
expect(reloadStub).to.not.have.been.called;
248+
expect(mock.calls[0].request).to.eql({
249+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
250+
idToken,
251+
newEmail
252+
});
253+
});
254+
255+
it('should reload the user if the API returns a different email', async () => {
256+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
257+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
258+
259+
});
260+
261+
await verifyBeforeUpdateEmail(user, newEmail);
262+
263+
expect(reloadStub).to.have.been.calledOnce;
264+
expect(mock.calls[0].request).to.eql({
265+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
266+
idToken,
267+
newEmail
268+
});
269+
});
270+
271+
context('on iOS', () => {
272+
it('should pass action code parameters', async () => {
273+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
274+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
275+
email
276+
});
277+
await verifyBeforeUpdateEmail(user, newEmail, {
278+
handleCodeInApp: true,
279+
iOS: {
280+
bundleId: 'my-bundle',
281+
appStoreId: 'my-appstore-id'
282+
},
283+
url: 'my-url',
284+
dynamicLinkDomain: 'fdl-domain'
285+
});
286+
287+
expect(mock.calls[0].request).to.eql({
288+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
289+
idToken,
290+
newEmail,
291+
continueUrl: 'my-url',
292+
dynamicLinkDomain: 'fdl-domain',
293+
canHandleCodeInApp: true,
294+
iosBundleId: 'my-bundle',
295+
iosAppStoreId: 'my-appstore-id'
296+
});
297+
});
298+
});
299+
300+
context('on Android', () => {
301+
it('should pass action code parameters', async () => {
302+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
303+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
304+
email
305+
});
306+
await verifyBeforeUpdateEmail(user, newEmail, {
307+
handleCodeInApp: true,
308+
android: {
309+
installApp: false,
310+
minimumVersion: 'my-version',
311+
packageName: 'my-package'
312+
},
313+
url: 'my-url',
314+
dynamicLinkDomain: 'fdl-domain'
315+
});
316+
expect(mock.calls[0].request).to.eql({
317+
requestType: Operation.VERIFY_AND_CHANGE_EMAIL,
318+
idToken,
319+
newEmail,
320+
continueUrl: 'my-url',
321+
dynamicLinkDomain: 'fdl-domain',
322+
canHandleCodeInApp: true,
323+
androidInstallApp: false,
324+
androidMinimumVersionCode: 'my-version',
325+
androidPackageName: 'my-package'
326+
});
327+
});
328+
});
329+
});

packages-exp/auth-exp/src/core/strategies/email.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,28 @@ export async function sendEmailVerification(
6565
await user.reload();
6666
}
6767
}
68+
69+
export async function verifyBeforeUpdateEmail(
70+
userExtern: externs.User,
71+
newEmail: string,
72+
actionCodeSettings?: externs.ActionCodeSettings | null
73+
): Promise<void> {
74+
const user = userExtern as User;
75+
const idToken = await user.getIdToken();
76+
const request: api.VerifyAndChangeEmailRequest = {
77+
requestType: externs.Operation.VERIFY_AND_CHANGE_EMAIL,
78+
idToken,
79+
newEmail
80+
};
81+
if (actionCodeSettings) {
82+
setActionCodeSettingsOnRequest(request, actionCodeSettings);
83+
}
84+
85+
const { email } = await api.verifyAndChangeEmail(user.auth, request);
86+
87+
if (email !== user.email) {
88+
// If the local copy of the email on user is outdated, reload the
89+
// user.
90+
await user.reload();
91+
}
92+
}

packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ describe('core/strategies/checkActionCode', () => {
250250
expect(response).to.eql({
251251
data: {
252252
email,
253-
previousEmail: null
253+
previousEmail: null,
254+
multiFactorInfo: null
254255
},
255256
operation: Operation.PASSWORD_RESET
256257
});
@@ -269,7 +270,8 @@ describe('core/strategies/checkActionCode', () => {
269270
expect(response).to.eql({
270271
data: {
271272
email,
272-
previousEmail: newEmail
273+
previousEmail: newEmail,
274+
multiFactorInfo: null
273275
},
274276
operation: Operation.PASSWORD_RESET
275277
});

packages-exp/auth-exp/src/core/strategies/email_and_password.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import * as externs from '@firebase/auth-types-exp';
2020
import * as account from '../../api/account_management/email_and_password';
2121
import * as authentication from '../../api/authentication/email_and_password';
2222
import { Auth } from '../../model/auth';
23-
import { AuthErrorCode } from '../errors';
2423
import { EmailAuthProvider } from '../providers/email';
2524
import { setActionCodeSettingsOnRequest } from './action_code_settings';
2625
import { signInWithCredential } from './credential';
2726
import { UserCredentialImpl } from '../user/user_credential_impl';
2827
import { signUp } from '../../api/authentication/sign_up';
2928
import { assert } from '../util/assert';
29+
import { MultiFactorInfo } from '../../mfa/mfa_info';
3030

3131
export async function sendPasswordResetEmail(
3232
auth: externs.Auth,
@@ -73,16 +73,30 @@ export async function checkActionCode(
7373
// VERIFY_AND_CHANGE_EMAIL.
7474
// New email should not be empty if the request type is
7575
// VERIFY_AND_CHANGE_EMAIL.
76+
// Multi-factor info could not be empty if the request type is
77+
// REVERT_SECOND_FACTOR_ADDITION.
7678
const operation = response.requestType;
77-
assert(operation, auth.name, AuthErrorCode.INTERNAL_ERROR);
79+
assert(operation, auth.name);
7880
switch (operation) {
7981
case externs.Operation.EMAIL_SIGNIN:
8082
break;
8183
case externs.Operation.VERIFY_AND_CHANGE_EMAIL:
82-
assert(response.newEmail, auth.name, AuthErrorCode.INTERNAL_ERROR);
84+
assert(response.newEmail, auth.name);
8385
break;
86+
case externs.Operation.REVERT_SECOND_FACTOR_ADDITION:
87+
assert(response.mfaInfo, auth.name);
88+
// fall through
8489
default:
85-
assert(response.email, auth.name, AuthErrorCode.INTERNAL_ERROR);
90+
assert(response.email, auth.name);
91+
}
92+
93+
// The multi-factor info for revert second factor addition
94+
let multiFactorInfo: MultiFactorInfo | null = null;
95+
if (response.mfaInfo) {
96+
multiFactorInfo = MultiFactorInfo._fromServerResponse(
97+
auth as Auth,
98+
response.mfaInfo
99+
);
86100
}
87101

88102
return {
@@ -94,8 +108,8 @@ export async function checkActionCode(
94108
previousEmail:
95109
(response.requestType === externs.Operation.VERIFY_AND_CHANGE_EMAIL
96110
? response.email
97-
: response.newEmail) || null
98-
/* multiFactorInfo: MultiFactorInfo | null; */
111+
: response.newEmail) || null,
112+
multiFactorInfo
99113
},
100114
operation
101115
};

packages-exp/auth-exp/src/core/strategies/phone.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ describe('core/strategies/phone', () => {
344344
});
345345
});
346346

347-
it('works when completing the sign in flow, ignoring the supplied phone number', async () => {
347+
it('works when completing the sign in flow', async () => {
348348
const endpoint = mockEndpoint(Endpoint.START_PHONE_MFA_SIGN_IN, {
349349
phoneResponseInfo: {
350350
sessionInfo: 'session-info'
@@ -361,7 +361,6 @@ describe('core/strategies/phone', () => {
361361
const sessionInfo = await _verifyPhoneNumber(
362362
auth,
363363
{
364-
phoneNumber: 'phone-number-from-user',
365364
session,
366365
multiFactorHint: mfaInfo
367366
},

0 commit comments

Comments
 (0)