Skip to content

Commit d14d508

Browse files
sam-gcavolkovi
authored andcommitted
Add link and reauth implementation to email credential (#3226)
* Add linking and reauth to email credential * Formatting * Add tests for new api method * Formatting
1 parent b5365c7 commit d14d508

File tree

4 files changed

+199
-69
lines changed

4 files changed

+199
-69
lines changed

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

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,14 @@ import { testAuth } from '../../../test/mock_auth';
2626
import * as mockFetch from '../../../test/mock_fetch';
2727
import { Auth } from '../../model/auth';
2828
import { ServerError } from '../errors';
29-
import { signInWithEmailLink } from './email_link';
29+
import {
30+
signInWithEmailLink,
31+
signInWithEmailLinkForLinking
32+
} from './email_link';
3033

3134
use(chaiAsPromised);
3235

33-
describe('api/authentication/signInWithEmailLink', () => {
34-
const request = {
35-
36-
oobCode: 'my-code'
37-
};
38-
36+
describe('api/authentication/email_link', () => {
3937
let auth: Auth;
4038

4139
beforeEach(async () => {
@@ -45,44 +43,102 @@ describe('api/authentication/signInWithEmailLink', () => {
4543

4644
afterEach(mockFetch.tearDown);
4745

48-
it('should POST to the correct endpoint', async () => {
49-
const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, {
50-
displayName: 'my-name',
51-
46+
context('signInWithEmailLink', () => {
47+
const request = {
48+
49+
oobCode: 'my-code'
50+
};
51+
52+
it('should POST to the correct endpoint', async () => {
53+
const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, {
54+
displayName: 'my-name',
55+
56+
});
57+
58+
const response = await signInWithEmailLink(auth, request);
59+
expect(response.displayName).to.eq('my-name');
60+
expect(response.email).to.eq('[email protected]');
61+
expect(mock.calls[0].request).to.eql(request);
62+
expect(mock.calls[0].method).to.eq('POST');
63+
expect(mock.calls[0].headers).to.eql({
64+
'Content-Type': 'application/json',
65+
'X-Client-Version': 'testSDK/0.0.0'
66+
});
5267
});
5368

54-
const response = await signInWithEmailLink(auth, request);
55-
expect(response.displayName).to.eq('my-name');
56-
expect(response.email).to.eq('[email protected]');
57-
expect(mock.calls[0].request).to.eql(request);
58-
expect(mock.calls[0].method).to.eq('POST');
59-
expect(mock.calls[0].headers).to.eql({
60-
'Content-Type': 'application/json',
61-
'X-Client-Version': 'testSDK/0.0.0'
69+
it('should handle errors', async () => {
70+
const mock = mockEndpoint(
71+
Endpoint.SIGN_IN_WITH_EMAIL_LINK,
72+
{
73+
error: {
74+
code: 400,
75+
message: ServerError.INVALID_EMAIL,
76+
errors: [
77+
{
78+
message: ServerError.INVALID_EMAIL
79+
}
80+
]
81+
}
82+
},
83+
400
84+
);
85+
86+
await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith(
87+
FirebaseError,
88+
'Firebase: The email address is badly formatted. (auth/invalid-email).'
89+
);
90+
expect(mock.calls[0].request).to.eql(request);
6291
});
6392
});
6493

65-
it('should handle errors', async () => {
66-
const mock = mockEndpoint(
67-
Endpoint.SIGN_IN_WITH_EMAIL_LINK,
68-
{
69-
error: {
70-
code: 400,
71-
message: ServerError.INVALID_EMAIL,
72-
errors: [
73-
{
74-
message: ServerError.INVALID_EMAIL
75-
}
76-
]
77-
}
78-
},
79-
400
80-
);
81-
82-
await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith(
83-
FirebaseError,
84-
'Firebase: The email address is badly formatted. (auth/invalid-email).'
85-
);
86-
expect(mock.calls[0].request).to.eql(request);
94+
context('signInWithEmailLinkForLinking', () => {
95+
const request = {
96+
97+
oobCode: 'my-code',
98+
idToken: 'id-token-2'
99+
};
100+
101+
it('should POST to the correct endpoint', async () => {
102+
const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, {
103+
displayName: 'my-name',
104+
105+
});
106+
107+
const response = await signInWithEmailLinkForLinking(auth, request);
108+
expect(response.displayName).to.eq('my-name');
109+
expect(response.email).to.eq('[email protected]');
110+
expect(mock.calls[0].request).to.eql(request);
111+
expect(mock.calls[0].method).to.eq('POST');
112+
expect(mock.calls[0].headers).to.eql({
113+
'Content-Type': 'application/json',
114+
'X-Client-Version': 'testSDK/0.0.0'
115+
});
116+
});
117+
118+
it('should handle errors', async () => {
119+
const mock = mockEndpoint(
120+
Endpoint.SIGN_IN_WITH_EMAIL_LINK,
121+
{
122+
error: {
123+
code: 400,
124+
message: ServerError.INVALID_EMAIL,
125+
errors: [
126+
{
127+
message: ServerError.INVALID_EMAIL
128+
}
129+
]
130+
}
131+
},
132+
400
133+
);
134+
135+
await expect(
136+
signInWithEmailLinkForLinking(auth, request)
137+
).to.be.rejectedWith(
138+
FirebaseError,
139+
'Firebase: The email address is badly formatted. (auth/invalid-email).'
140+
);
141+
expect(mock.calls[0].request).to.eql(request);
142+
});
87143
});
88144
});

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Endpoint, HttpMethod, _performSignInRequest } from '..';
18+
import { _performSignInRequest, Endpoint, HttpMethod } from '../';
1919
import { Auth } from '../../model/auth';
2020
import { IdTokenResponse } from '../../model/id_token';
2121

@@ -38,3 +38,18 @@ export async function signInWithEmailLink(
3838
SignInWithEmailLinkResponse
3939
>(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request);
4040
}
41+
42+
export interface SignInWithEmailLinkForLinkingRequest
43+
extends SignInWithEmailLinkRequest {
44+
idToken: string;
45+
}
46+
47+
export async function signInWithEmailLinkForLinking(
48+
auth: Auth,
49+
request: SignInWithEmailLinkForLinkingRequest
50+
): Promise<SignInWithEmailLinkResponse> {
51+
return _performSignInRequest<
52+
SignInWithEmailLinkForLinkingRequest,
53+
SignInWithEmailLinkResponse
54+
>(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request);
55+
}

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

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('core/credentials/email', () => {
7575
});
7676

7777
describe('#_getIdTokenResponse', () => {
78-
it('call sign in with password', async () => {
78+
it('calls sign in with password', async () => {
7979
const idTokenResponse = await credential._getIdTokenResponse(auth);
8080
expect(idTokenResponse.idToken).to.eq('id-token');
8181
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
@@ -90,18 +90,43 @@ describe('core/credentials/email', () => {
9090
});
9191

9292
describe('#_linkToIdToken', () => {
93-
it('throws', async () => {
94-
await expect(
95-
credential._linkToIdToken(auth, 'id-token')
96-
).to.be.rejectedWith(Error);
93+
it('calls update email password', async () => {
94+
apiMock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
95+
idToken: 'id-token',
96+
refreshToken: 'refresh-token',
97+
expiresIn: '1234',
98+
localId: serverUser.localId!
99+
});
100+
101+
const idTokenResponse = await credential._linkToIdToken(
102+
auth,
103+
'id-token-2'
104+
);
105+
expect(idTokenResponse.idToken).to.eq('id-token');
106+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
107+
expect(idTokenResponse.expiresIn).to.eq('1234');
108+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
109+
expect(apiMock.calls[0].request).to.eql({
110+
idToken: 'id-token-2',
111+
returnSecureToken: true,
112+
email: 'some-email',
113+
password: 'some-password'
114+
});
97115
});
98116
});
99117

100118
describe('#_getReauthenticationResolver', () => {
101-
it('throws', () => {
102-
expect(() => credential._getReauthenticationResolver(auth)).to.throw(
103-
Error
104-
);
119+
it('calls sign in with password', async () => {
120+
const idTokenResponse = await credential._getIdTokenResponse(auth);
121+
expect(idTokenResponse.idToken).to.eq('id-token');
122+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
123+
expect(idTokenResponse.expiresIn).to.eq('1234');
124+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
125+
expect(apiMock.calls[0].request).to.eql({
126+
returnSecureToken: true,
127+
email: 'some-email',
128+
password: 'some-password'
129+
});
105130
});
106131
});
107132
});
@@ -153,18 +178,34 @@ describe('core/credentials/email', () => {
153178
});
154179

155180
describe('#_linkToIdToken', () => {
156-
it('throws', async () => {
157-
await expect(
158-
credential._linkToIdToken(auth, 'id-token')
159-
).to.be.rejectedWith(Error);
181+
it('calls sign in with the new token', async () => {
182+
const idTokenResponse = await credential._linkToIdToken(
183+
auth,
184+
'id-token-2'
185+
);
186+
expect(idTokenResponse.idToken).to.eq('id-token');
187+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
188+
expect(idTokenResponse.expiresIn).to.eq('1234');
189+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
190+
expect(apiMock.calls[0].request).to.eql({
191+
idToken: 'id-token-2',
192+
email: 'some-email',
193+
oobCode: 'oob-code'
194+
});
160195
});
161196
});
162197

163198
describe('#_matchIdTokenWithUid', () => {
164-
it('throws', () => {
165-
expect(() => credential._getReauthenticationResolver(auth)).to.throw(
166-
Error
167-
);
199+
it('call sign in with email link', async () => {
200+
const idTokenResponse = await credential._getIdTokenResponse(auth);
201+
expect(idTokenResponse.idToken).to.eq('id-token');
202+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
203+
expect(idTokenResponse.expiresIn).to.eq('1234');
204+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
205+
expect(apiMock.calls[0].request).to.eql({
206+
email: 'some-email',
207+
oobCode: 'oob-code'
208+
});
168209
});
169210
});
170211
});

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717

1818
import * as externs from '@firebase/auth-types-exp';
1919

20+
import { updateEmailPassword } from '../../api/account_management/email_and_password';
2021
import { signInWithPassword } from '../../api/authentication/email_and_password';
21-
import { signInWithEmailLink } from '../../api/authentication/email_link';
22+
import {
23+
signInWithEmailLink,
24+
signInWithEmailLinkForLinking
25+
} from '../../api/authentication/email_link';
2226
import { Auth } from '../../model/auth';
2327
import { IdTokenResponse } from '../../model/id_token';
24-
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
28+
import { AuthErrorCode } from '../errors';
2529
import { EmailAuthProvider } from '../providers/email';
26-
import { debugFail } from '../util/assert';
30+
import { debugFail, fail } from '../util/assert';
2731
import { AuthCredential } from './';
2832

2933
export class EmailAuthCredential implements AuthCredential {
@@ -57,17 +61,31 @@ export class EmailAuthCredential implements AuthCredential {
5761
oobCode: this.password
5862
});
5963
default:
60-
throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, {
61-
appName: auth.name
62-
});
64+
fail(auth.name, AuthErrorCode.INTERNAL_ERROR);
6365
}
6466
}
6567

66-
async _linkToIdToken(_auth: Auth, _idToken: string): Promise<never> {
67-
debugFail('Method not implemented.');
68+
async _linkToIdToken(auth: Auth, idToken: string): Promise<IdTokenResponse> {
69+
switch (this.signInMethod) {
70+
case EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD:
71+
return updateEmailPassword(auth, {
72+
idToken,
73+
returnSecureToken: true,
74+
email: this.email,
75+
password: this.password
76+
});
77+
case EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD:
78+
return signInWithEmailLinkForLinking(auth, {
79+
idToken,
80+
email: this.email,
81+
oobCode: this.password
82+
});
83+
default:
84+
fail(auth.name, AuthErrorCode.INTERNAL_ERROR);
85+
}
6886
}
6987

70-
_getReauthenticationResolver(_auth: Auth): Promise<never> {
71-
debugFail('Method not implemented.');
88+
_getReauthenticationResolver(auth: Auth): Promise<IdTokenResponse> {
89+
return this._getIdTokenResponse(auth);
7290
}
7391
}

0 commit comments

Comments
 (0)