Skip to content

Commit 3bb6376

Browse files
authored
Add sendSignInWithEmail to auth-next (#2960)
* Add sendSignInWithEmail links * [AUTOMATED]: Prettier Code Styling * PR Feedback * PR Feedback
1 parent 05e9b23 commit 3bb6376

File tree

5 files changed

+491
-1
lines changed

5 files changed

+491
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 '../model/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 =
28+
'https://www.example.com/finishSignIn?' +
29+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
30+
'continueUrl=' +
31+
encodeURIComponent(continueUrl) +
32+
'&languageCode=en&tenantId=TENANT_ID&state=bla';
33+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
34+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
35+
expect(actionCodeUrl!.code).to.eq('CODE');
36+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
37+
// ContinueUrl should be decoded.
38+
expect(actionCodeUrl!.continueUrl).to.eq(continueUrl);
39+
expect(actionCodeUrl!.tenantId).to.eq('TENANT_ID');
40+
expect(actionCodeUrl!.languageCode).to.eq('en');
41+
});
42+
43+
context('operation', () => {
44+
it('should identitfy EMAIL_SIGNIN', () => {
45+
const actionLink =
46+
'https://www.example.com/finishSignIn?' +
47+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
48+
'languageCode=en';
49+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
50+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
51+
});
52+
53+
it('should identitfy VERIFY_AND_CHANGE_EMAIL', () => {
54+
const actionLink =
55+
'https://www.example.com/finishSignIn?' +
56+
'oobCode=CODE&mode=verifyAndChangeEmail&apiKey=API_KEY&' +
57+
'languageCode=en';
58+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
59+
expect(actionCodeUrl!.operation).to.eq(
60+
Operation.VERIFY_AND_CHANGE_EMAIL
61+
);
62+
});
63+
64+
it('should identitfy VERIFY_EMAIL', () => {
65+
const actionLink =
66+
'https://www.example.com/finishSignIn?' +
67+
'oobCode=CODE&mode=verifyEmail&apiKey=API_KEY&' +
68+
'languageCode=en';
69+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
70+
expect(actionCodeUrl!.operation).to.eq(Operation.VERIFY_EMAIL);
71+
});
72+
73+
it('should identitfy RECOVER_EMAIL', () => {
74+
const actionLink =
75+
'https://www.example.com/finishSignIn?' +
76+
'oobCode=CODE&mode=recoverEmail&apiKey=API_KEY&' +
77+
'languageCode=en';
78+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
79+
expect(actionCodeUrl!.operation).to.eq(Operation.RECOVER_EMAIL);
80+
});
81+
82+
it('should identitfy PASSWORD_RESET', () => {
83+
const actionLink =
84+
'https://www.example.com/finishSignIn?' +
85+
'oobCode=CODE&mode=resetPassword&apiKey=API_KEY&' +
86+
'languageCode=en';
87+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
88+
expect(actionCodeUrl!.operation).to.eq(Operation.PASSWORD_RESET);
89+
});
90+
91+
it('should identitfy REVERT_SECOND_FACTOR_ADDITION', () => {
92+
const actionLink =
93+
'https://www.example.com/finishSignIn?' +
94+
'oobCode=CODE&mode=revertSecondFactorAddition&apiKey=API_KEY&' +
95+
'languageCode=en';
96+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
97+
expect(actionCodeUrl!.operation).to.eq(
98+
Operation.REVERT_SECOND_FACTOR_ADDITION
99+
);
100+
});
101+
});
102+
103+
it('should work if there is a port number in the URL', () => {
104+
const actionLink =
105+
'https://www.example.com:8080/finishSignIn?' +
106+
'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla';
107+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
108+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
109+
expect(actionCodeUrl!.code).to.eq('CODE');
110+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
111+
expect(actionCodeUrl!.continueUrl).to.be.null;
112+
expect(actionCodeUrl!.tenantId).to.be.null;
113+
expect(actionCodeUrl!.languageCode).to.be.null;
114+
});
115+
116+
it('should ignore parameters after anchor', () => {
117+
const actionLink =
118+
'https://www.example.com/finishSignIn?' +
119+
'oobCode=CODE1&mode=signIn&apiKey=API_KEY1&state=bla' +
120+
'#oobCode=CODE2&mode=signIn&apiKey=API_KEY2&state=bla';
121+
const actionCodeUrl = ActionCodeURL._fromLink(mockAuth, actionLink);
122+
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
123+
expect(actionCodeUrl!.code).to.eq('CODE1');
124+
expect(actionCodeUrl!.apiKey).to.eq('API_KEY1');
125+
expect(actionCodeUrl!.continueUrl).to.be.null;
126+
expect(actionCodeUrl!.tenantId).to.be.null;
127+
expect(actionCodeUrl!.languageCode).to.be.null;
128+
});
129+
130+
context('invalid links', () => {
131+
it('should handle missing API key, code & mode', () => {
132+
const actionLink = 'https://www.example.com/finishSignIn';
133+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
134+
});
135+
136+
it('should handle invalid mode', () => {
137+
const actionLink =
138+
'https://www.example.com/finishSignIn?oobCode=CODE&mode=INVALID_MODE&apiKey=API_KEY';
139+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
140+
});
141+
142+
it('should handle missing code', () => {
143+
const actionLink =
144+
'https://www.example.com/finishSignIn?mode=signIn&apiKey=API_KEY';
145+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
146+
});
147+
148+
it('should handle missing API key', () => {
149+
const actionLink =
150+
'https://www.example.com/finishSignIn?oobCode=CODE&mode=signIn';
151+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
152+
});
153+
154+
it('should handle missing mode', () => {
155+
const actionLink =
156+
'https://www.example.com/finishSignIn?oobCode=CODE&apiKey=API_KEY';
157+
expect(ActionCodeURL._fromLink(mockAuth, actionLink)).to.be.null;
158+
});
159+
});
160+
});
161+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 { AuthErrorCode, AUTH_ERROR_FACTORY } from './errors';
19+
import { Operation } from '../model/action_code_info';
20+
import { Auth } from '../model/auth';
21+
22+
/**
23+
* Enums for fields in URL query string.
24+
* @enum {string}
25+
*/
26+
enum QueryField {
27+
API_KEY = 'apiKey',
28+
CODE = 'oobCode',
29+
CONTINUE_URL = 'continueUrl',
30+
LANGUAGE_CODE = 'languageCode',
31+
MODE = 'mode',
32+
TENANT_ID = 'tenantId'
33+
}
34+
35+
/**
36+
* Map from mode string in action code URL to Action Code Info operation.
37+
*/
38+
const MODE_TO_OPERATION_MAP: { [key: string]: Operation } = {
39+
'recoverEmail': Operation.RECOVER_EMAIL,
40+
'resetPassword': Operation.PASSWORD_RESET,
41+
'signIn': Operation.EMAIL_SIGNIN,
42+
'verifyEmail': Operation.VERIFY_EMAIL,
43+
'verifyAndChangeEmail': Operation.VERIFY_AND_CHANGE_EMAIL,
44+
'revertSecondFactorAddition': Operation.REVERT_SECOND_FACTOR_ADDITION
45+
};
46+
47+
/**
48+
* Maps the mode string in action code URL to Action Code Info operation.
49+
*/
50+
function parseMode(mode: string | null): Operation | null {
51+
return mode ? MODE_TO_OPERATION_MAP[mode] || null : null;
52+
}
53+
54+
function parseDeepLink(url: string): string {
55+
const uri = new URL(url);
56+
const link = uri.searchParams.get('link');
57+
// Double link case (automatic redirect).
58+
const doubleDeepLink = link ? new URL(link).searchParams.get('link') : null;
59+
// iOS custom scheme links.
60+
const iOSDeepLink = uri.searchParams.get('deep_link_id');
61+
const iOSDoubleDeepLink = iOSDeepLink
62+
? new URL(iOSDeepLink).searchParams.get('link')
63+
: null;
64+
return iOSDoubleDeepLink || iOSDeepLink || doubleDeepLink || link || url;
65+
}
66+
67+
export class ActionCodeURL {
68+
readonly apiKey: string;
69+
readonly code: string;
70+
readonly continueUrl: string | null;
71+
readonly languageCode: string | null;
72+
readonly operation: Operation;
73+
readonly tenantId: string | null;
74+
75+
constructor(auth: Auth, actionLink: string) {
76+
const uri = new URL(actionLink);
77+
const apiKey = uri.searchParams.get(QueryField.API_KEY);
78+
const code = uri.searchParams.get(QueryField.CODE);
79+
const operation = parseMode(uri.searchParams.get(QueryField.MODE));
80+
// Validate API key, code and mode.
81+
if (!apiKey || !code || !operation) {
82+
throw AUTH_ERROR_FACTORY.create(AuthErrorCode.ARGUMENT_ERROR, {
83+
appName: auth.name
84+
});
85+
}
86+
this.apiKey = apiKey;
87+
this.operation = operation;
88+
this.code = code;
89+
this.continueUrl = uri.searchParams.get(QueryField.CONTINUE_URL);
90+
this.languageCode = uri.searchParams.get(QueryField.LANGUAGE_CODE);
91+
this.tenantId = uri.searchParams.get(QueryField.TENANT_ID);
92+
}
93+
94+
static _fromLink(auth: Auth, link: string): ActionCodeURL | null {
95+
const actionLink = parseDeepLink(link);
96+
try {
97+
return new ActionCodeURL(auth, actionLink);
98+
} catch {
99+
return null;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)