Skip to content

Commit 3c461d0

Browse files
committed
Add sendSignInWithEmail links
1 parent 9cc3657 commit 3c461d0

File tree

5 files changed

+464
-1
lines changed

5 files changed

+464
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* @license
3+
* Copyright 2020 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 { FirebaseError } from '@firebase/util';
19+
import { expect, use } from 'chai';
20+
import * as chaiAsPromised from 'chai-as-promised';
21+
import * as sinonChai from 'sinon-chai';
22+
import { mockEndpoint } from '../../../test/api/helper';
23+
import { mockAuth } from '../../../test/mock_auth';
24+
import * as mockFetch from '../../../test/mock_fetch';
25+
import { Endpoint } from '../../api';
26+
import { ServerError } from '../../api/errors';
27+
import { Operation } from '../../model/action_code_info';
28+
import {
29+
sendSignInLinkToEmail,
30+
isSignInWithEmailLink
31+
} from './email_link';
32+
33+
use(chaiAsPromised);
34+
use(sinonChai);
35+
36+
describe('sendSignInLinkToEmail', () => {
37+
const email = '[email protected]';
38+
39+
beforeEach(mockFetch.setUp);
40+
afterEach(mockFetch.tearDown);
41+
42+
it('should send a sign in link via email', async () => {
43+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
44+
email
45+
});
46+
await sendSignInLinkToEmail(mockAuth, email);
47+
expect(mock.calls[0].request).to.eql({
48+
requestType: Operation.EMAIL_SIGNIN,
49+
email
50+
});
51+
});
52+
53+
it('should surface errors', async () => {
54+
const mock = mockEndpoint(
55+
Endpoint.SEND_OOB_CODE,
56+
{
57+
error: {
58+
code: 400,
59+
message: ServerError.INVALID_EMAIL
60+
}
61+
},
62+
400
63+
);
64+
await expect(sendSignInLinkToEmail(mockAuth, email)).to.be.rejectedWith(
65+
FirebaseError,
66+
'Firebase: The email address is badly formatted. (auth/invalid-email).'
67+
);
68+
expect(mock.calls.length).to.eq(1);
69+
});
70+
71+
context('on iOS', () => {
72+
it('should pass action code parameters', async () => {
73+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
74+
email
75+
});
76+
await sendSignInLinkToEmail(mockAuth, email, {
77+
handleCodeInApp: true,
78+
iOS: {
79+
bundleId: 'my-bundle',
80+
appStoreId: 'my-appstore-id'
81+
},
82+
url: 'my-url',
83+
dynamicLinkDomain: 'fdl-domain'
84+
});
85+
86+
expect(mock.calls[0].request).to.eql({
87+
requestType: Operation.EMAIL_SIGNIN,
88+
email,
89+
continueUrl: 'my-url',
90+
dynamicLinkDomain: 'fdl-domain',
91+
canHandleCodeInApp: true,
92+
iosBundleId: 'my-bundle',
93+
iosAppStoreId: 'my-appstore-id'
94+
});
95+
});
96+
});
97+
98+
context('on Android', () => {
99+
it('should pass action code parameters', async () => {
100+
const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
101+
email
102+
});
103+
await sendSignInLinkToEmail(mockAuth, email, {
104+
handleCodeInApp: true,
105+
android: {
106+
installApp: false,
107+
minimumVersion: 'my-version',
108+
packageName: 'my-package'
109+
},
110+
url: 'my-url',
111+
dynamicLinkDomain: 'fdl-domain'
112+
});
113+
expect(mock.calls[0].request).to.eql({
114+
requestType: Operation.EMAIL_SIGNIN,
115+
email,
116+
continueUrl: 'my-url',
117+
dynamicLinkDomain: 'fdl-domain',
118+
canHandleCodeInApp: true,
119+
androidInstallApp: false,
120+
androidMinimumVersionCode: 'my-version',
121+
androidPackageName: 'my-package'
122+
});
123+
});
124+
});
125+
});
126+
127+
describe('isSignInWithEmailLink', () => {
128+
context('simple links', () => {
129+
it('should recognize sign in links', () => {
130+
const link = 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY';
131+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.true;
132+
});
133+
134+
it('should not recognize other email links', () => {
135+
const link = 'https://www.example.com/action?mode=verifyEmail&oobCode=oobCode&apiKey=API_KEY';
136+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.false;
137+
});
138+
139+
it('should not recognize invalid links', () => {
140+
const link = 'https://www.example.com/action?mode=signIn';
141+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.false;
142+
});
143+
});
144+
145+
context('deep links', () => {
146+
it('should recognize valid links', () => {
147+
const deepLink = 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY';
148+
const link = `https://example.app.goo.gl/?link=${encodeURIComponent(deepLink)}`;
149+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.true;
150+
});
151+
152+
it('should recognize valid links with deep_link_id', () => {
153+
const deepLink = 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY';
154+
const link = `somexampleiosurl://google/link?deep_link_id=${encodeURIComponent(deepLink)}`;
155+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.true;
156+
});
157+
158+
it('should reject other email links', () => {
159+
const deepLink = 'https://www.example.com/action?mode=verifyEmail&oobCode=oobCode&apiKey=API_KEY';
160+
const link = `https://example.app.goo.gl/?link=${encodeURIComponent(deepLink)}`;
161+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.false;
162+
});
163+
164+
it('should reject invalid links', () => {
165+
const deepLink = 'https://www.example.com/action?mode=signIn';
166+
const link = `https://example.app.goo.gl/?link=${encodeURIComponent(deepLink)}`;
167+
expect(isSignInWithEmailLink(mockAuth, link)).to.be.false;
168+
});
169+
});
170+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright 2020 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 * as api from '../../api/authentication/email_and_password';
19+
import { Operation } from '../../model/action_code_info';
20+
import { ActionCodeSettings, setActionCodeSettingsOnRequest } from '../../model/action_code_settings';
21+
import { ActionCodeURL } from '../../model/action_code_url';
22+
import { Auth } from '../../model/auth';
23+
24+
export async function sendSignInLinkToEmail(
25+
auth: Auth,
26+
email: string,
27+
actionCodeSettings?: ActionCodeSettings
28+
): Promise<void> {
29+
const request: api.EmailSignInRequest = {
30+
requestType: Operation.EMAIL_SIGNIN,
31+
email
32+
};
33+
if (actionCodeSettings) {
34+
setActionCodeSettingsOnRequest(request, actionCodeSettings);
35+
}
36+
37+
await api.sendSignInLinkToEmail(auth, request);
38+
}
39+
40+
export function isSignInWithEmailLink(auth: Auth, emailLink: string): boolean {
41+
const actionCodeUrl = ActionCodeURL._fromLink(auth, emailLink);
42+
return actionCodeUrl?.operation === Operation.EMAIL_SIGNIN;
43+
}

packages-exp/auth-exp/src/model/action_code_info.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export enum Operation {
2020
RECOVER_EMAIL = 'RECOVER_EMAIL',
2121
EMAIL_SIGNIN = 'EMAIL_SIGNIN',
2222
VERIFY_EMAIL = 'VERIFY_EMAIL',
23-
VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL'
23+
VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL',
24+
REVERT_SECOND_FACTOR_ADDITION = 'REVERT_SECOND_FACTOR_ADDITION'
2425
}
2526

2627
export interface ActionCodeInfo {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @license
3+
* Copyright 2020 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 } from 'chai';
19+
import { ActionCodeURL } from './action_code_url';
20+
import { mockAuth } from '../../test/mock_auth';
21+
import { Operation } from './action_code_info';
22+
23+
describe('ActionCodeURL', () => {
24+
describe('_fromLink', () => {
25+
it('should parse correctly formatted links', () => {
26+
const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3';
27+
const actionLink = 'https://www.example.com/finishSignIn?' +
28+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
29+
'continueUrl=' + encodeURIComponent(continueUrl) +
30+
'&languageCode=en&tenantId=TENANT_ID&state=bla';
31+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
32+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
33+
expect(actionCodeUrl!.code).to.eq('CODE');
34+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
35+
// ContinueUrl should be decoded.
36+
expect(actionCodeUrl!.continueUrl).to.eq(continueUrl);
37+
expect(actionCodeUrl!.tenantId).to.eq('TENANT_ID');
38+
expect(actionCodeUrl!.languageCode).to.eq('en');
39+
});
40+
41+
context('operation', () => {
42+
it('should identitfy EMAIL_SIGNIN', () => {
43+
const actionLink = 'https://www.example.com/finishSignIn?' +
44+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
45+
'languageCode=en';
46+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
47+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
48+
});
49+
50+
it('should identitfy VERIFY_AND_CHANGE_EMAIL', () => {
51+
const actionLink = 'https://www.example.com/finishSignIn?' +
52+
'oobCode=CODE&mode=verifyAndChangeEmail&apiKey=API_KEY&' +
53+
'languageCode=en';
54+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
55+
expect(actionCodeUrl!.operation).to.eq(Operation.VERIFY_AND_CHANGE_EMAIL);
56+
});
57+
58+
it('should identitfy VERIFY_EMAIL', () => {
59+
const actionLink = 'https://www.example.com/finishSignIn?' +
60+
'oobCode=CODE&mode=verifyEmail&apiKey=API_KEY&' +
61+
'languageCode=en';
62+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
63+
expect(actionCodeUrl!.operation).to.eq(Operation.VERIFY_EMAIL);
64+
});
65+
66+
it('should identitfy RECOVER_EMAIL', () => {
67+
const actionLink = 'https://www.example.com/finishSignIn?' +
68+
'oobCode=CODE&mode=recoverEmail&apiKey=API_KEY&' +
69+
'languageCode=en';
70+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
71+
expect(actionCodeUrl!.operation).to.eq(Operation.RECOVER_EMAIL);
72+
});
73+
74+
it('should identitfy PASSWORD_RESET', () => {
75+
const actionLink = 'https://www.example.com/finishSignIn?' +
76+
'oobCode=CODE&mode=resetPassword&apiKey=API_KEY&' +
77+
'languageCode=en';
78+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
79+
expect(actionCodeUrl!.operation).to.eq(Operation.PASSWORD_RESET);
80+
});
81+
82+
it('should identitfy REVERT_SECOND_FACTOR_ADDITION', () => {
83+
const actionLink = 'https://www.example.com/finishSignIn?' +
84+
'oobCode=CODE&mode=revertSecondFactorAddition&apiKey=API_KEY&' +
85+
'languageCode=en';
86+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
87+
expect(actionCodeUrl!.operation).to.eq(Operation.REVERT_SECOND_FACTOR_ADDITION);
88+
});
89+
});
90+
91+
it('should work if there is a port number in the URL', () => {
92+
const actionLink = 'https://www.example.com:8080/finishSignIn?' +
93+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla';
94+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
95+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
96+
expect(actionCodeUrl!.code).to.eq('CODE');
97+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
98+
expect(actionCodeUrl!.continueUrl).to.be.null;
99+
expect(actionCodeUrl!.tenantId).to.be.null;
100+
expect(actionCodeUrl!.languageCode).to.be.null;
101+
});
102+
103+
it('should ignore parameters after anchor', () => {
104+
const actionLink = 'https://www.example.com/finishSignIn?' +
105+
'oobCode=CODE1&mode=signIn&apiKey=API_KEY1&state=bla' +
106+
'#oobCode=CODE2&mode=signIn&apiKey=API_KEY2&state=bla';
107+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
108+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
109+
expect(actionCodeUrl!.code).to.eq('CODE1');
110+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY1');
111+
expect(actionCodeUrl!.continueUrl).to.be.null;
112+
expect(actionCodeUrl!.tenantId).to.be.null;
113+
expect(actionCodeUrl!.languageCode).to.be.null;
114+
});
115+
116+
context('invalid links', () => {
117+
it('should handle missing API key, code & mode', () => {
118+
const actionLink = 'https://www.example.com/finishSignIn';
119+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
120+
});
121+
122+
it('should handle invalid mode', () => {
123+
const actionLink = 'https://www.example.com/finishSignIn?oobCode=CODE&mode=INVALID_MODE&apiKey=API_KEY';
124+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
125+
});
126+
127+
it('should handle missing code', () => {
128+
const actionLink = 'https://www.example.com/finishSignIn?mode=signIn&apiKey=API_KEY';
129+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
130+
});
131+
132+
it('should handle missing API key', () => {
133+
const actionLink = 'https://www.example.com/finishSignIn?oobCode=CODE&mode=signIn';
134+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
135+
});
136+
137+
it('should handle missing mode', () => {
138+
const actionLink = 'https://www.example.com/finishSignIn?oobCode=CODE&apiKey=API_KEY';
139+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
140+
});
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)