@yahoo/uds 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
Binary file
package/cli/preload.ts CHANGED
@@ -51,6 +51,7 @@ mock.module('googleapis', () => ({
51
51
  OAuth2: mock(() => {}).mockImplementation(() => ({
52
52
  getToken: () => Promise.resolve({ tokens: 'test' }),
53
53
  setCredentials: mock(() => {}),
54
+ generateAuthUrl: mock(() => 'https://accounts.google.com/o/oauth2/v2/auth'),
54
55
  })),
55
56
  },
56
57
  oauth2: () => ({
@@ -6,11 +6,15 @@ import http from 'http';
6
6
  import httpMocks from 'node-mocks-http';
7
7
 
8
8
  import {
9
+ configuratorUrlOrigin,
9
10
  getAuthenticatedUser,
11
+ getAuthorizeUrl,
10
12
  isYahooEmployee,
11
13
  login,
14
+ type LoginProvider,
12
15
  logout,
13
- onRequestHandler,
16
+ onGETHandler,
17
+ onPostHandler,
14
18
  type User,
15
19
  } from './auth';
16
20
 
@@ -29,6 +33,22 @@ describe('auth', () => {
29
33
  });
30
34
  });
31
35
 
36
+ describe('getAuthorizeUrl', () => {
37
+ it('uses configurator by default', () => {
38
+ expect(getAuthorizeUrl()).toEqual(
39
+ `${configuratorUrlOrigin}/login?continue=http://localhost:3000/oauth2callback`,
40
+ );
41
+ });
42
+
43
+ it('generates the google auth url for other providers', () => {
44
+ (['google', 'firebase'] as LoginProvider[]).forEach((provider) => {
45
+ expect(getAuthorizeUrl(provider)).toStartWith(
46
+ `https://accounts.google.com/o/oauth2/v2/auth`,
47
+ );
48
+ });
49
+ });
50
+ });
51
+
32
52
  describe('getAuthenticatedUser', () => {
33
53
  it('does not return a user on first use', async () => {
34
54
  expect(await getAuthenticatedUser(__dirname)).toBeUndefined();
@@ -72,7 +92,7 @@ describe('auth', () => {
72
92
 
73
93
  const request = httpMocks.createRequest({ method: 'GET', url: '/oauth2callback' });
74
94
  const response = httpMocks.createResponse();
75
- await onRequestHandler.bind(mockServer)(request, response, resolve, reject);
95
+ await onGETHandler.bind(mockServer)(request, response, resolve, reject);
76
96
  expect(response._getData()).toInclude('no code parameter');
77
97
  });
78
98
 
@@ -82,13 +102,75 @@ describe('auth', () => {
82
102
 
83
103
  const request = httpMocks.createRequest({ method: 'GET', url: '/oauth2callback?code=123' });
84
104
  const response = httpMocks.createResponse();
85
- await onRequestHandler.bind(mockServer)(request, response, resolve, reject);
105
+ await onGETHandler.bind(mockServer)(request, response, resolve, reject);
86
106
  expect(response._getData()).toInclude('Authentication successful! Please close this window.');
87
107
  expect(resolve).toHaveBeenCalledWith(mockUser);
88
108
  expect(reject).not.toHaveBeenCalled();
89
109
  });
90
110
  });
91
111
 
112
+ describe('onPostHandler', () => {
113
+ const mockServer = Object.assign(Object.create(http.Server.prototype), {
114
+ listen: mock(),
115
+ on: mock(),
116
+ close: mock(),
117
+ });
118
+
119
+ it('ignores non-POST requests', () => {
120
+ const req = httpMocks.createRequest({ method: 'GET' });
121
+ const resp = httpMocks.createResponse();
122
+ onPostHandler.bind(mockServer)(req, resp, mock(), mock());
123
+ expect(resp._getStatusCode()).toBe(405);
124
+ expect(resp._getData()).toInclude('Method Not Allowed');
125
+ });
126
+
127
+ it('rejects requests from 3rd party origins', () => {
128
+ const req = httpMocks.createRequest({
129
+ method: 'POST',
130
+ headers: { origin: 'https://bad.com' },
131
+ });
132
+ const resp = httpMocks.createResponse();
133
+ const reject = mock();
134
+ onPostHandler.bind(mockServer)(req, resp, mock(), reject);
135
+ expect(reject).toHaveBeenCalledWith('Request origin not allowed.');
136
+ });
137
+
138
+ it('sets CORS headers', () => {
139
+ const req = httpMocks.createRequest({
140
+ method: 'POST',
141
+ headers: { origin: configuratorUrlOrigin },
142
+ // body: JSON.stringify({ email: ''}),
143
+ });
144
+ const resp = httpMocks.createResponse();
145
+ const resolve = mock();
146
+ const reject = mock();
147
+ onPostHandler.bind(mockServer)(req, resp, resolve, reject);
148
+ expect(resp.getHeaders()).toEqual({
149
+ 'access-control-allow-origin': configuratorUrlOrigin,
150
+ 'access-control-allow-methods': 'OPTIONS, GET, POST',
151
+ });
152
+ });
153
+
154
+ it('sends a user obj', async () => {
155
+ const mockUser = { email: 'foo@yahooinc.com', displayName: 'Foo' };
156
+
157
+ const req = httpMocks.createRequest({
158
+ method: 'POST',
159
+ headers: { origin: configuratorUrlOrigin },
160
+ });
161
+ const resp = httpMocks.createResponse({
162
+ eventEmitter: (await import('events')).EventEmitter,
163
+ });
164
+ const resolve = mock();
165
+ const reject = mock();
166
+ onPostHandler.bind(mockServer)(req, resp, resolve, reject);
167
+ req.send(mockUser);
168
+ expect(resp._getData()).toInclude('Authentication successful! Please close this window.');
169
+ expect(reject).not.toHaveBeenCalled();
170
+ expect(resolve).toHaveBeenCalledWith(mockUser);
171
+ });
172
+ });
173
+
92
174
  describe('logout', () => {
93
175
  it('removes the user cache', async () => {
94
176
  await writeCache(filepath, { email: 'user@yahooinc.com', name: 'User' });
package/cli/utils/auth.ts CHANGED
@@ -17,15 +17,23 @@ import open from 'open';
17
17
 
18
18
  import clientSecrets from './client_secrets.json';
19
19
 
20
+ type User = oauth2_v2.Schema$Userinfo | FirebaseUser;
21
+ type LoginProvider = 'google' | 'firebase' | 'configurator';
22
+
23
+ const LOGIN_PROVIDER: LoginProvider =
24
+ (process.env.LOGIN_PROVIDER as LoginProvider) ?? 'configurator';
25
+
20
26
  const REDIRECT_URL = clientSecrets.web.redirect_uris[0];
21
- const { port: PORT, origin: BASE_URL } = new URL(REDIRECT_URL);
27
+ const { port: PORT, origin: SERVER_ORIGIN } = new URL(REDIRECT_URL);
28
+
29
+ const configuratorUrlOrigin =
30
+ process.env.NODE_ENV === 'production' ? 'https://config.uds.build' : 'http://localhost:4001';
22
31
 
23
32
  const CACHE_FILEPATH = '.uds/user.json';
24
33
  const DEFAULT_CLI_PATH = path.resolve(import.meta.dir, '..');
25
34
  const CACHED_USER_FILE = path.resolve(DEFAULT_CLI_PATH, CACHE_FILEPATH);
26
35
 
27
36
  const isEmulator = process.env.EMULATOR || process.env.NEXT_PUBLIC_EMULATOR;
28
- const loginProvider = process.env.LOGIN_PROVIDER ?? 'firebase';
29
37
 
30
38
  // TODO: consolidate with the firebase config and setup in database/firebase.ts
31
39
  const firebaseConfig = !isEmulator
@@ -50,13 +58,72 @@ const oauth2Client = new google.auth.OAuth2(
50
58
  REDIRECT_URL,
51
59
  );
52
60
 
53
- type User = oauth2_v2.Schema$Userinfo | FirebaseUser;
54
-
55
61
  function isYahooEmployee(user?: User) {
56
62
  return user?.email?.endsWith('@yahooinc.com');
57
63
  }
58
64
 
59
- async function onRequestHandler(
65
+ function getAuthorizeUrl(loginProvider = LOGIN_PROVIDER) {
66
+ if (loginProvider === 'configurator') {
67
+ return `${configuratorUrlOrigin}/login?continue=${REDIRECT_URL}`;
68
+ }
69
+
70
+ return oauth2Client.generateAuthUrl({
71
+ access_type: 'offline',
72
+ scope: [
73
+ 'https://www.googleapis.com/auth/userinfo.profile',
74
+ 'https://www.googleapis.com/auth/userinfo.email',
75
+ ].join(' '),
76
+ hd: 'yahooinc.com',
77
+ include_granted_scopes: true,
78
+ });
79
+ }
80
+
81
+ function onPostHandler(
82
+ this: http.Server,
83
+ req: http.IncomingMessage,
84
+ res: http.ServerResponse,
85
+ resolve: (user: User) => void,
86
+ reject: (reason?: unknown) => void,
87
+ ) {
88
+ if (req.method !== 'POST') {
89
+ res.writeHead(405, { Allow: 'POST' }); // Set the 405 status and Allow header
90
+ res.end('Method Not Allowed');
91
+ reject('Method Not Allowed');
92
+ return;
93
+ }
94
+
95
+ if (req.headers.origin !== configuratorUrlOrigin) {
96
+ reject(`Request origin not allowed.`);
97
+ return;
98
+ }
99
+
100
+ res.setHeader('Access-Control-Allow-Origin', configuratorUrlOrigin);
101
+ res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
102
+
103
+ let data = '';
104
+
105
+ req.on('data', (chunk) => {
106
+ data += chunk.toString();
107
+ });
108
+
109
+ req.on('error', (err: NodeJS.ErrnoException) => {
110
+ reject(err);
111
+ });
112
+
113
+ req.on('end', () => {
114
+ try {
115
+ const user: FirebaseUser = JSON.parse(data);
116
+ res.end('Authentication successful! Please close this window.');
117
+ resolve(user);
118
+ } catch (err) {
119
+ reject(err);
120
+ } finally {
121
+ this.close();
122
+ }
123
+ });
124
+ }
125
+
126
+ async function onGETHandler(
60
127
  this: http.Server,
61
128
  req: http.IncomingMessage,
62
129
  res: http.ServerResponse,
@@ -64,7 +131,7 @@ async function onRequestHandler(
64
131
  reject: (reason?: unknown) => void,
65
132
  ) {
66
133
  try {
67
- const code = new URL(req.url || '', BASE_URL).searchParams.get('code');
134
+ const code = new URL(req.url || '', SERVER_ORIGIN).searchParams.get('code');
68
135
  if (!code) {
69
136
  res.end('There was no code parameter on the url.');
70
137
  this.close();
@@ -72,7 +139,6 @@ async function onRequestHandler(
72
139
  }
73
140
 
74
141
  res.end('Authentication successful! Please close this window.');
75
-
76
142
  this.close();
77
143
 
78
144
  const { tokens } = await oauth2Client.getToken(code);
@@ -80,7 +146,7 @@ async function onRequestHandler(
80
146
 
81
147
  let user: User;
82
148
 
83
- if (loginProvider === 'firebase') {
149
+ if (LOGIN_PROVIDER === 'firebase') {
84
150
  // Build Firebase credential using the Google ID token.
85
151
  const credential = GoogleAuthProvider.credential(tokens.id_token);
86
152
  user = (await signInWithCredential(auth, credential)).user;
@@ -102,34 +168,39 @@ async function onRequestHandler(
102
168
  */
103
169
  async function authenticateUser(): Promise<User> {
104
170
  return new Promise((resolve, reject) => {
105
- const authorizeUrl = oauth2Client.generateAuthUrl({
106
- access_type: 'offline',
107
- scope: [
108
- 'https://www.googleapis.com/auth/userinfo.profile',
109
- 'https://www.googleapis.com/auth/userinfo.email',
110
- ].join(' '),
111
- hd: 'yahooinc.com',
112
- include_granted_scopes: true,
113
- });
114
-
115
171
  // TODO: If port (3000) is already in use, this will fail.
116
172
  // Setup https://www.npmjs.com/package/find-free-ports, but that won't
117
173
  // play well with the pre-configured redirect_uris in the Google Cloud Console.
118
- const server = http
119
- .createServer()
120
- .listen(PORT, async () => {
121
- const childProcess = await open(authorizeUrl, { wait: false });
122
- childProcess.unref();
123
- })
124
- .on('request', (req, res) => onRequestHandler.call(server, req, res, resolve, reject))
125
- .on('error', (err: NodeJS.ErrnoException) => {
126
- if (err.code && err.code.includes('EADDRINUSE')) {
127
- print(
128
- red(`🚨 Port ${PORT} already in use. Cannot start local server to handle OAuth flow.`),
129
- );
130
- server.close();
131
- }
174
+ const server = http.createServer();
175
+
176
+ server.listen(PORT, async () => {
177
+ const authorizeUrl = getAuthorizeUrl();
178
+
179
+ print(`Please visit the following URL if it didn't open automatically:\n${authorizeUrl}`);
180
+
181
+ const childProcess = await open(authorizeUrl, { wait: false });
182
+ childProcess.unref();
183
+
184
+ process.on('SIGINT', () => {
185
+ server.close();
186
+ reject('Received SIGINT.');
132
187
  });
188
+ });
189
+
190
+ server.on('error', (err: NodeJS.ErrnoException) => {
191
+ if (err.code && err.code.includes('EADDRINUSE')) {
192
+ print(
193
+ red(`🚨 Port ${PORT} already in use. Cannot start local server to handle OAuth flow.`),
194
+ );
195
+ server.close();
196
+ }
197
+ });
198
+
199
+ if (LOGIN_PROVIDER === 'configurator') {
200
+ server.on('request', (req, res) => onPostHandler.call(server, req, res, resolve, reject));
201
+ } else {
202
+ server.on('request', (req, res) => onGETHandler.call(server, req, res, resolve, reject));
203
+ }
133
204
  });
134
205
  }
135
206
 
@@ -157,6 +228,7 @@ async function login() {
157
228
  await Bun.write(CACHED_USER_FILE, JSON.stringify(user, null, 2));
158
229
  } catch (err) {
159
230
  console.error('Error:', err);
231
+ throw err;
160
232
  }
161
233
  }
162
234
 
@@ -193,10 +265,14 @@ async function getAuthenticatedUser(cliPath = DEFAULT_CLI_PATH): Promise<User |
193
265
 
194
266
  export {
195
267
  authenticateUser,
268
+ configuratorUrlOrigin,
196
269
  getAuthenticatedUser,
270
+ getAuthorizeUrl,
197
271
  isYahooEmployee,
198
272
  login,
273
+ type LoginProvider,
199
274
  logout,
200
- onRequestHandler,
275
+ onGETHandler,
276
+ onPostHandler,
201
277
  type User,
202
278
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yahoo/uds",
3
3
  "description": "Yahoo Universal System",
4
- "version": "1.3.4",
4
+ "version": "1.4.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "uds": "./cli/uds-cli"