(client: OAuth2Client)
| 247 | } |
| 248 | |
| 249 | async function authWithWeb(client: OAuth2Client): Promise<OauthWebLogin> { |
| 250 | const port = await getAvailablePort(); |
| 251 | // The hostname used for the HTTP server binding (e.g., '0.0.0.0' in Docker). |
| 252 | const host = process.env['OAUTH_CALLBACK_HOST'] || 'localhost'; |
| 253 | // The `redirectUri` sent to Google's authorization server MUST use a loopback IP literal |
| 254 | // (i.e., 'localhost' or '127.0.0.1'). This is a strict security policy for credentials of |
| 255 | // type 'Desktop app' or 'Web application' (when using loopback flow) to mitigate |
| 256 | // authorization code interception attacks. |
| 257 | const redirectUri = `http://localhost:${port}/oauth2callback`; |
| 258 | const state = crypto.randomBytes(32).toString('hex'); |
| 259 | const authUrl = client.generateAuthUrl({ |
| 260 | redirect_uri: redirectUri, |
| 261 | access_type: 'offline', |
| 262 | scope: OAUTH_SCOPE, |
| 263 | state, |
| 264 | }); |
| 265 | |
| 266 | const loginCompletePromise = new Promise<void>((resolve, reject) => { |
| 267 | const server = http.createServer(async (req, res) => { |
| 268 | try { |
| 269 | if (req.url!.indexOf('/oauth2callback') === -1) { |
| 270 | res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL }); |
| 271 | res.end(); |
| 272 | reject(new Error('Unexpected request: ' + req.url)); |
| 273 | } |
| 274 | // acquire the code from the querystring, and close the web server. |
| 275 | const qs = new url.URL(req.url!, 'http://localhost:3000').searchParams; |
| 276 | if (qs.get('error')) { |
| 277 | res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL }); |
| 278 | res.end(); |
| 279 | |
| 280 | reject(new Error(`Error during authentication: ${qs.get('error')}`)); |
| 281 | } else if (qs.get('state') !== state) { |
| 282 | res.end('State mismatch. Possible CSRF attack'); |
| 283 | |
| 284 | reject(new Error('State mismatch. Possible CSRF attack')); |
| 285 | } else if (qs.get('code')) { |
| 286 | const { tokens } = await client.getToken({ |
| 287 | code: qs.get('code')!, |
| 288 | redirect_uri: redirectUri, |
| 289 | }); |
| 290 | client.setCredentials(tokens); |
| 291 | // Retrieve and cache Google Account ID during authentication |
| 292 | try { |
| 293 | await fetchAndCacheUserInfo(client); |
| 294 | } catch (error) { |
| 295 | console.error( |
| 296 | 'Failed to retrieve Google Account ID during authentication:', |
| 297 | error, |
| 298 | ); |
| 299 | // Don't fail the auth flow if Google Account ID retrieval fails |
| 300 | } |
| 301 | |
| 302 | res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_SUCCESS_URL }); |
| 303 | res.end(); |
| 304 | resolve(); |
| 305 | } else { |
| 306 | reject(new Error('No code found in request')); |
no test coverage detected