2025-06-10 16:00:13 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-07-10 18:59:02 -07:00
import {
OAuth2Client ,
Compute ,
CodeChallengeMethod ,
2025-10-27 16:05:11 -04:00
GoogleAuth ,
2026-03-04 05:42:59 +05:30
type Credentials ,
type AuthClient ,
type JWTInput ,
2025-07-10 18:59:02 -07:00
} from 'google-auth-library' ;
2025-08-25 22:11:27 +02:00
import * as http from 'node:http' ;
import url from 'node:url' ;
import crypto from 'node:crypto' ;
import * as net from 'node:net' ;
2025-12-02 21:27:37 -08:00
import { EventEmitter } from 'node:events' ;
2025-06-10 16:00:13 -07:00
import open from 'open' ;
2025-06-11 13:26:41 -07:00
import path from 'node:path' ;
2025-07-11 10:57:35 -07:00
import { promises as fs } from 'node:fs' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-11-21 08:31:47 -08:00
import {
getErrorMessage ,
FatalAuthenticationError ,
FatalCancellationError ,
} from '../utils/errors.js' ;
2025-08-20 10:55:47 +09:00
import { UserAccountManager } from '../utils/userAccountManager.js' ;
2025-07-07 15:02:13 -07:00
import { AuthType } from '../core/contentGenerator.js' ;
2025-07-10 18:59:02 -07:00
import readline from 'node:readline' ;
2025-08-20 10:55:47 +09:00
import { Storage } from '../config/storage.js' ;
2025-09-16 10:05:29 -04:00
import { OAuthCredentialStorage } from './oauth-credential-storage.js' ;
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-11-21 08:31:47 -08:00
import {
writeToStdout ,
2025-12-02 15:08:25 -08:00
createWorkingStdio ,
2025-11-21 08:31:47 -08:00
writeToStderr ,
} from '../utils/stdio.js' ;
import {
enableLineWrapping ,
disableMouseEvents ,
disableKittyKeyboardProtocol ,
enterAlternateScreen ,
exitAlternateScreen ,
} from '../utils/terminal.js' ;
import { coreEvents , CoreEvent } from '../utils/events.js' ;
2026-02-03 16:26:00 -05:00
import { getConsentForOauth } from '../utils/authConsent.js' ;
2025-08-20 10:55:47 +09:00
2025-12-02 21:27:37 -08:00
export const authEvents = new EventEmitter ( ) ;
async function triggerPostAuthCallbacks ( tokens : Credentials ) {
// Construct a JWTInput object to pass to callbacks, as this is the
// type expected by the downstream Google Cloud client libraries.
const jwtInput : JWTInput = {
client_id : OAUTH_CLIENT_ID ,
client_secret : OAUTH_CLIENT_SECRET ,
refresh_token : tokens.refresh_token ? ? undefined , // Ensure null is not passed
type : 'authorized_user' ,
2025-12-08 11:20:13 -08:00
client_email : userAccountManager.getCachedGoogleAccount ( ) ? ? undefined ,
2025-12-02 21:27:37 -08:00
} ;
// Execute all registered post-authentication callbacks.
authEvents . emit ( 'post_auth' , jwtInput ) ;
}
2025-08-20 10:55:47 +09:00
const userAccountManager = new UserAccountManager ( ) ;
2025-06-10 16:00:13 -07:00
// OAuth Client ID used to initiate OAuth2Client class.
const OAUTH_CLIENT_ID =
'681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com' ;
// OAuth Secret value used to initiate OAuth2Client class.
// Note: It's ok to save this in git because this is an installed application
// as described here: https://developers.google.com/identity/protocols/oauth2#installed
// "The process results in a client ID and, in some cases, a client secret,
// which you embed in the source code of your application. (In this context,
// the client secret is obviously not treated as a secret.)"
const OAUTH_CLIENT_SECRET = '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' ;
// OAuth Scopes for Cloud Code authorization.
const OAUTH_SCOPE = [
'https://www.googleapis.com/auth/cloud-platform' ,
'https://www.googleapis.com/auth/userinfo.email' ,
'https://www.googleapis.com/auth/userinfo.profile' ,
] ;
const HTTP_REDIRECT = 301 ;
const SIGN_IN_SUCCESS_URL =
'https://developers.google.com/gemini-code-assist/auth_success_gemini' ;
const SIGN_IN_FAILURE_URL =
'https://developers.google.com/gemini-code-assist/auth_failure_gemini' ;
2025-06-18 16:34:00 -07:00
/ * *
* An Authentication URL for updating the credentials of a Oauth2Client
* as well as a promise that will resolve when the credentials have
* been refreshed ( or which throws error when refreshing credentials failed ) .
* /
export interface OauthWebLogin {
authUrl : string ;
loginCompletePromise : Promise < void > ;
2025-06-11 13:26:41 -07:00
}
2025-10-27 16:05:11 -04:00
const oauthClientPromises = new Map < AuthType , Promise < AuthClient > > ( ) ;
2025-08-15 22:05:59 -07:00
2025-09-16 10:05:29 -04:00
function getUseEncryptedStorageFlag() {
return process . env [ FORCE_ENCRYPTED_FILE_ENV_VAR ] === 'true' ;
}
2025-08-15 22:05:59 -07:00
async function initOauthClient (
2025-07-07 15:02:13 -07:00
authType : AuthType ,
2025-07-10 18:59:02 -07:00
config : Config ,
2025-10-27 16:05:11 -04:00
) : Promise < AuthClient > {
const credentials = await fetchCachedCredentials ( ) ;
if (
credentials &&
2026-02-20 20:44:23 +00:00
typeof credentials === 'object' &&
'type' in credentials &&
credentials . type === 'external_account_authorized_user'
2025-10-27 16:05:11 -04:00
) {
const auth = new GoogleAuth ( {
scopes : OAUTH_SCOPE ,
} ) ;
2025-12-16 21:28:18 -08:00
const byoidClient = auth . fromJSON ( {
2025-10-27 16:05:11 -04:00
. . . credentials ,
refresh_token : credentials.refresh_token ? ? undefined ,
} ) ;
const token = await byoidClient . getAccessToken ( ) ;
if ( token ) {
debugLogger . debug ( 'Created BYOID auth client.' ) ;
return byoidClient ;
}
}
2025-06-18 16:34:00 -07:00
const client = new OAuth2Client ( {
2025-06-10 16:00:13 -07:00
clientId : OAUTH_CLIENT_ID ,
clientSecret : OAUTH_CLIENT_SECRET ,
2025-07-18 02:57:37 +08:00
transporterOptions : {
proxy : config.getProxy ( ) ,
} ,
2025-06-10 16:00:13 -07:00
} ) ;
2025-09-16 10:05:29 -04:00
const useEncryptedStorage = getUseEncryptedStorageFlag ( ) ;
2025-07-07 15:02:13 -07:00
2025-07-25 10:19:38 -07:00
if (
2025-08-17 12:43:21 -04:00
process . env [ 'GOOGLE_GENAI_USE_GCA' ] &&
process . env [ 'GOOGLE_CLOUD_ACCESS_TOKEN' ]
2025-07-25 10:19:38 -07:00
) {
client . setCredentials ( {
2025-08-17 12:43:21 -04:00
access_token : process.env [ 'GOOGLE_CLOUD_ACCESS_TOKEN' ] ,
2025-07-25 10:19:38 -07:00
} ) ;
await fetchAndCacheUserInfo ( client ) ;
return client ;
}
2025-06-30 08:47:01 -07:00
client . on ( 'tokens' , async ( tokens : Credentials ) = > {
2025-09-16 10:05:29 -04:00
if ( useEncryptedStorage ) {
await OAuthCredentialStorage . saveCredentials ( tokens ) ;
} else {
await cacheCredentials ( tokens ) ;
}
2025-12-02 21:27:37 -08:00
await triggerPostAuthCallbacks ( tokens ) ;
2025-06-30 08:47:01 -07:00
} ) ;
2025-06-10 16:00:13 -07:00
2025-10-27 16:05:11 -04:00
if ( credentials ) {
client . setCredentials ( credentials as Credentials ) ;
try {
// This will verify locally that the credentials look good.
const { token } = await client . getAccessToken ( ) ;
if ( token ) {
// This will check with the server to see if it hasn't been revoked.
await client . getTokenInfo ( token ) ;
if ( ! userAccountManager . getCachedGoogleAccount ( ) ) {
try {
await fetchAndCacheUserInfo ( client ) ;
} catch ( error ) {
// Non-fatal, continue with existing auth.
debugLogger . warn (
'Failed to fetch user info:' ,
getErrorMessage ( error ) ,
) ;
}
}
debugLogger . log ( 'Loaded cached credentials.' ) ;
2025-12-02 21:27:37 -08:00
await triggerPostAuthCallbacks ( credentials as Credentials ) ;
2025-10-27 16:05:11 -04:00
return client ;
2025-06-29 16:35:20 -04:00
}
2025-10-27 16:05:11 -04:00
} catch ( error ) {
debugLogger . debug (
` Cached credentials are not valid: ` ,
getErrorMessage ( error ) ,
) ;
2025-06-29 16:35:20 -04:00
}
2025-06-18 16:34:00 -07:00
}
2025-06-10 16:00:13 -07:00
2025-11-14 11:39:11 -05:00
// In Google Compute Engine based environments (including Cloud Shell), we can
// use Application Default Credentials (ADC) provided via its metadata server
// to authenticate non-interactively using the identity of the logged-in user.
if ( authType === AuthType . COMPUTE_ADC ) {
2025-07-07 15:02:13 -07:00
try {
2025-11-14 11:39:11 -05:00
debugLogger . log (
'Attempting to authenticate via metadata server application default credentials.' ,
) ;
2025-07-07 15:02:13 -07:00
const computeClient = new Compute ( {
// We can leave this empty, since the metadata server will provide
// the service account email.
} ) ;
await computeClient . getAccessToken ( ) ;
2025-10-21 16:35:22 -04:00
debugLogger . log ( 'Authentication successful.' ) ;
2025-07-07 15:02:13 -07:00
// Do not cache creds in this case; note that Compute client will handle its own refresh
return computeClient ;
} catch ( e ) {
throw new Error (
2025-11-14 11:39:11 -05:00
` Could not authenticate using metadata server application default credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${ getErrorMessage (
2025-07-07 15:02:13 -07:00
e ,
) } ` ,
) ;
}
}
2025-07-21 16:23:28 -07:00
if ( config . isBrowserLaunchSuppressed ( ) ) {
2026-03-04 15:35:21 -05:00
if ( ! config . isInteractive ( ) ) {
throw new FatalAuthenticationError (
'Manual authorization is required but the current session is non-interactive. ' +
'Please run the Gemini CLI in an interactive terminal to log in, ' +
'provide a GEMINI_API_KEY, or ensure Application Default Credentials are configured.' ,
) ;
}
2025-07-10 18:59:02 -07:00
let success = false ;
const maxRetries = 2 ;
2025-11-21 08:31:47 -08:00
// Enter alternate buffer
enterAlternateScreen ( ) ;
// Clear screen and move cursor to top-left.
writeToStdout ( '\u001B[2J\u001B[H' ) ;
disableMouseEvents ( ) ;
disableKittyKeyboardProtocol ( ) ;
enableLineWrapping ( ) ;
try {
for ( let i = 0 ; ! success && i < maxRetries ; i ++ ) {
success = await authWithUserCode ( client ) ;
if ( ! success ) {
writeToStderr (
'\nFailed to authenticate with user code.' +
( i === maxRetries - 1 ? '' : ' Retrying...\n' ) ,
) ;
}
2025-07-10 18:59:02 -07:00
}
2025-11-21 08:31:47 -08:00
} finally {
exitAlternateScreen ( ) ;
// If this was triggered from an active Gemini CLI TUI this event ensures
// the TUI will re-initialize the terminal state just like it will when
// another editor like VIM may have modified the buffer of settings.
coreEvents . emit ( CoreEvent . ExternalEditorClosed ) ;
2025-07-10 18:59:02 -07:00
}
2025-11-21 08:31:47 -08:00
2025-07-10 18:59:02 -07:00
if ( ! success ) {
2025-11-21 08:31:47 -08:00
writeToStderr ( 'Failed to authenticate with user code.\n' ) ;
2025-08-25 21:44:45 -07:00
throw new FatalAuthenticationError (
'Failed to authenticate with user code.' ,
) ;
2025-07-10 18:59:02 -07:00
}
2025-12-08 11:20:13 -08:00
// Retrieve and cache Google Account ID after successful user code auth
try {
await fetchAndCacheUserInfo ( client ) ;
} catch ( error ) {
debugLogger . warn (
'Failed to retrieve Google Account ID during authentication:' ,
getErrorMessage ( error ) ,
) ;
}
await triggerPostAuthCallbacks ( client . credentials ) ;
2025-07-10 18:59:02 -07:00
} else {
2026-03-05 14:57:28 -05:00
// In ACP mode, we skip the interactive consent and directly open the browser
if ( ! config . getAcpMode ( ) ) {
2026-02-25 10:04:42 -05:00
const userConsent = await getConsentForOauth ( '' ) ;
if ( ! userConsent ) {
throw new FatalCancellationError ( 'Authentication cancelled by user.' ) ;
}
2026-01-30 09:57:34 -05:00
}
2025-07-10 18:59:02 -07:00
const webLogin = await authWithWeb ( client ) ;
2025-11-21 08:31:47 -08:00
coreEvents . emit ( CoreEvent . UserFeedback , {
severity : 'info' ,
message :
2026-02-21 13:55:11 -08:00
` \ n \ nAttempting to open authentication page in your browser. \ n ` +
2025-11-21 08:31:47 -08:00
` Otherwise navigate to: \ n \ n ${ webLogin . authUrl } \ n \ n \ n ` ,
} ) ;
2025-07-18 09:55:26 +08:00
try {
// Attempt to open the authentication URL in the default browser.
// We do not use the `wait` option here because the main script's execution
// is already paused by `loginCompletePromise`, which awaits the server callback.
const childProcess = await open ( webLogin . authUrl ) ;
// IMPORTANT: Attach an error handler to the returned child process.
// Without this, if `open` fails to spawn a process (e.g., `xdg-open` is not found
// in a minimal Docker container), it will emit an unhandled 'error' event,
// causing the entire Node.js process to crash.
2025-09-03 13:51:29 -07:00
childProcess . on ( 'error' , ( error ) = > {
2025-11-21 08:31:47 -08:00
coreEvents . emit ( CoreEvent . UserFeedback , {
severity : 'error' ,
message :
` Failed to open browser with error: ${ getErrorMessage ( error ) } \ n ` +
` Please try running again with NO_BROWSER=true set. ` ,
} ) ;
2025-07-18 09:55:26 +08:00
} ) ;
} catch ( err ) {
2025-11-21 08:31:47 -08:00
coreEvents . emit ( CoreEvent . UserFeedback , {
severity : 'error' ,
message :
` Failed to open browser with error: ${ getErrorMessage ( err ) } \ n ` +
` Please try running again with NO_BROWSER=true set. ` ,
} ) ;
2025-09-03 13:51:29 -07:00
throw new FatalAuthenticationError (
` Failed to open browser: ${ getErrorMessage ( err ) } ` ,
2025-07-18 09:55:26 +08:00
) ;
}
2025-11-21 08:31:47 -08:00
coreEvents . emit ( CoreEvent . UserFeedback , {
severity : 'info' ,
message : 'Waiting for authentication...\n' ,
} ) ;
2025-07-10 18:59:02 -07:00
2025-09-03 13:51:29 -07:00
// Add timeout to prevent infinite waiting when browser tab gets stuck
const authTimeout = 5 * 60 * 1000 ; // 5 minutes timeout
const timeoutPromise = new Promise < never > ( ( _ , reject ) = > {
setTimeout ( ( ) = > {
reject (
new FatalAuthenticationError (
'Authentication timed out after 5 minutes. The browser tab may have gotten stuck in a loading state. ' +
'Please try again or use NO_BROWSER=true for manual authentication.' ,
) ,
) ;
} , authTimeout ) ;
} ) ;
2026-01-05 11:02:55 -08:00
// Listen for SIGINT to stop waiting for auth so the terminal doesn't hang
// if the user chooses not to auth.
let sigIntHandler : ( ( ) = > void ) | undefined ;
let stdinHandler : ( ( data : Buffer ) = > void ) | undefined ;
const cancellationPromise = new Promise < never > ( ( _ , reject ) = > {
sigIntHandler = ( ) = >
reject ( new FatalCancellationError ( 'Authentication cancelled by user.' ) ) ;
process . on ( 'SIGINT' , sigIntHandler ) ;
// Note that SIGINT might not get raised on Ctrl+C in raw mode
// so we also need to look for Ctrl+C directly in stdin.
2026-01-06 20:09:39 -08:00
stdinHandler = ( data : Buffer ) = > {
2026-01-05 11:02:55 -08:00
if ( data . includes ( 0x03 ) ) {
reject (
new FatalCancellationError ( 'Authentication cancelled by user.' ) ,
) ;
}
} ;
process . stdin . on ( 'data' , stdinHandler ) ;
} ) ;
try {
await Promise . race ( [
webLogin . loginCompletePromise ,
timeoutPromise ,
cancellationPromise ,
] ) ;
} finally {
if ( sigIntHandler ) {
process . removeListener ( 'SIGINT' , sigIntHandler ) ;
}
if ( stdinHandler ) {
process . stdin . removeListener ( 'data' , stdinHandler ) ;
}
}
2025-11-21 08:31:47 -08:00
coreEvents . emit ( CoreEvent . UserFeedback , {
severity : 'info' ,
message : 'Authentication succeeded\n' ,
} ) ;
2025-12-08 11:20:13 -08:00
await triggerPostAuthCallbacks ( client . credentials ) ;
2025-07-10 18:59:02 -07:00
}
2025-06-18 16:34:00 -07:00
2025-07-10 18:59:02 -07:00
return client ;
}
2025-06-18 16:34:00 -07:00
2025-08-15 22:05:59 -07:00
export async function getOauthClient (
authType : AuthType ,
config : Config ,
2025-10-27 16:05:11 -04:00
) : Promise < AuthClient > {
2025-08-15 22:05:59 -07:00
if ( ! oauthClientPromises . has ( authType ) ) {
oauthClientPromises . set ( authType , initOauthClient ( authType , config ) ) ;
}
return oauthClientPromises . get ( authType ) ! ;
}
2025-07-10 18:59:02 -07:00
async function authWithUserCode ( client : OAuth2Client ) : Promise < boolean > {
2025-11-21 08:31:47 -08:00
try {
const redirectUri = 'https://codeassist.google.com/authcode' ;
const codeVerifier = await client . generateCodeVerifierAsync ( ) ;
const state = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
const authUrl : string = client . generateAuthUrl ( {
redirect_uri : redirectUri ,
access_type : 'offline' ,
scope : OAUTH_SCOPE ,
code_challenge_method : CodeChallengeMethod.S256 ,
code_challenge : codeVerifier.codeChallenge ,
state ,
2025-07-10 18:59:02 -07:00
} ) ;
2025-11-21 08:31:47 -08:00
writeToStdout (
'Please visit the following URL to authorize the application:\n\n' +
authUrl +
'\n\n' ,
) ;
2025-06-18 16:34:00 -07:00
2026-03-04 15:35:21 -05:00
const code = await new Promise < string > ( ( resolve , reject ) = > {
2025-11-21 08:31:47 -08:00
const rl = readline . createInterface ( {
input : process.stdin ,
2025-12-02 15:08:25 -08:00
output : createWorkingStdio ( ) . stdout ,
2025-11-21 08:31:47 -08:00
terminal : true ,
} ) ;
2025-07-10 18:59:02 -07:00
2026-03-04 15:35:21 -05:00
const timeout = setTimeout ( ( ) = > {
rl . close ( ) ;
reject (
new FatalAuthenticationError (
'Authorization timed out after 5 minutes.' ,
) ,
) ;
} , 300000 ) ; // 5 minute timeout
2025-11-21 08:31:47 -08:00
rl . question ( 'Enter the authorization code: ' , ( code ) = > {
2026-03-04 15:35:21 -05:00
clearTimeout ( timeout ) ;
2025-11-21 08:31:47 -08:00
rl . close ( ) ;
resolve ( code . trim ( ) ) ;
} ) ;
2025-07-10 18:59:02 -07:00
} ) ;
2025-11-21 08:31:47 -08:00
if ( ! code ) {
writeToStderr ( 'Authorization code is required.\n' ) ;
debugLogger . error ( 'Authorization code is required.' ) ;
return false ;
}
try {
const { tokens } = await client . getToken ( {
code ,
codeVerifier : codeVerifier.codeVerifier ,
redirect_uri : redirectUri ,
} ) ;
client . setCredentials ( tokens ) ;
} catch ( error ) {
writeToStderr (
'Failed to authenticate with authorization code:' +
getErrorMessage ( error ) +
'\n' ,
) ;
debugLogger . error (
'Failed to authenticate with authorization code:' ,
getErrorMessage ( error ) ,
) ;
return false ;
}
return true ;
} catch ( err ) {
if ( err instanceof FatalCancellationError ) {
throw err ;
}
writeToStderr (
'Failed to authenticate with user code:' + getErrorMessage ( err ) + '\n' ,
) ;
2025-10-27 16:46:35 -07:00
debugLogger . error (
2025-11-21 08:31:47 -08:00
'Failed to authenticate with user code:' ,
getErrorMessage ( err ) ,
2025-09-03 13:51:29 -07:00
) ;
2025-07-10 18:59:02 -07:00
return false ;
}
2025-06-18 16:34:00 -07:00
}
async function authWithWeb ( client : OAuth2Client ) : Promise < OauthWebLogin > {
const port = await getAvailablePort ( ) ;
2025-07-18 09:55:26 +08:00
// The hostname used for the HTTP server binding (e.g., '0.0.0.0' in Docker).
2026-01-23 13:41:37 -05:00
const host = process . env [ 'OAUTH_CALLBACK_HOST' ] || '127.0.0.1' ;
2025-07-18 09:55:26 +08:00
// The `redirectUri` sent to Google's authorization server MUST use a loopback IP literal
// (i.e., 'localhost' or '127.0.0.1'). This is a strict security policy for credentials of
// type 'Desktop app' or 'Web application' (when using loopback flow) to mitigate
// authorization code interception attacks.
2026-01-23 13:41:37 -05:00
const redirectUri = ` http://127.0.0.1: ${ port } /oauth2callback ` ;
2025-06-18 16:34:00 -07:00
const state = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
2025-07-10 18:59:02 -07:00
const authUrl = client . generateAuthUrl ( {
2025-06-18 16:34:00 -07:00
redirect_uri : redirectUri ,
access_type : 'offline' ,
scope : OAUTH_SCOPE ,
state ,
} ) ;
const loginCompletePromise = new Promise < void > ( ( resolve , reject ) = > {
2025-06-10 16:00:13 -07:00
const server = http . createServer ( async ( req , res ) = > {
try {
if ( req . url ! . indexOf ( '/oauth2callback' ) === - 1 ) {
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_FAILURE_URL } ) ;
res . end ( ) ;
2025-09-03 13:51:29 -07:00
reject (
new FatalAuthenticationError (
'OAuth callback not received. Unexpected request: ' + req . url ,
) ,
) ;
2026-02-23 23:33:31 +05:30
return ;
2025-06-10 16:00:13 -07:00
}
// acquire the code from the querystring, and close the web server.
2026-01-23 13:41:37 -05:00
const qs = new url . URL ( req . url ! , 'http://127.0.0.1:3000' ) . searchParams ;
2025-06-10 16:00:13 -07:00
if ( qs . get ( 'error' ) ) {
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_FAILURE_URL } ) ;
res . end ( ) ;
2025-06-11 13:26:41 -07:00
2025-09-03 13:51:29 -07:00
const errorCode = qs . get ( 'error' ) ;
const errorDescription =
qs . get ( 'error_description' ) || 'No additional details provided' ;
reject (
new FatalAuthenticationError (
` Google OAuth error: ${ errorCode } . ${ errorDescription } ` ,
) ,
) ;
2025-06-10 16:00:13 -07:00
} else if ( qs . get ( 'state' ) !== state ) {
res . end ( 'State mismatch. Possible CSRF attack' ) ;
2025-06-11 13:26:41 -07:00
2025-09-03 13:51:29 -07:00
reject (
new FatalAuthenticationError (
'OAuth state mismatch. Possible CSRF attack or browser session issue.' ,
) ,
) ;
2025-06-10 16:00:13 -07:00
} else if ( qs . get ( 'code' ) ) {
2025-06-29 16:35:20 -04:00
try {
2025-09-03 13:51:29 -07:00
const { tokens } = await client . getToken ( {
code : qs.get ( 'code' ) ! ,
redirect_uri : redirectUri ,
} ) ;
client . setCredentials ( tokens ) ;
// Retrieve and cache Google Account ID during authentication
try {
await fetchAndCacheUserInfo ( client ) ;
} catch ( error ) {
2025-10-21 16:35:22 -04:00
debugLogger . warn (
2025-09-03 13:51:29 -07:00
'Failed to retrieve Google Account ID during authentication:' ,
getErrorMessage ( error ) ,
) ;
// Don't fail the auth flow if Google Account ID retrieval fails
}
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_SUCCESS_URL } ) ;
res . end ( ) ;
resolve ( ) ;
2025-06-29 16:35:20 -04:00
} catch ( error ) {
2025-09-03 13:51:29 -07:00
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_FAILURE_URL } ) ;
res . end ( ) ;
reject (
new FatalAuthenticationError (
` Failed to exchange authorization code for tokens: ${ getErrorMessage ( error ) } ` ,
) ,
2025-06-29 16:35:20 -04:00
) ;
}
2025-06-10 16:00:13 -07:00
} else {
2025-09-03 13:51:29 -07:00
reject (
new FatalAuthenticationError (
'No authorization code received from Google OAuth. Please try authenticating again.' ,
) ,
) ;
2025-06-10 16:00:13 -07:00
}
} catch ( e ) {
2025-09-03 13:51:29 -07:00
// Provide more specific error message for unexpected errors during OAuth flow
if ( e instanceof FatalAuthenticationError ) {
reject ( e ) ;
} else {
reject (
new FatalAuthenticationError (
` Unexpected error during OAuth authentication: ${ getErrorMessage ( e ) } ` ,
) ,
) ;
}
2025-06-10 16:00:13 -07:00
} finally {
server . close ( ) ;
}
} ) ;
2025-09-03 13:51:29 -07:00
server . listen ( port , host , ( ) = > {
// Server started successfully
} ) ;
server . on ( 'error' , ( err ) = > {
reject (
new FatalAuthenticationError (
` OAuth callback server error: ${ getErrorMessage ( err ) } ` ,
) ,
) ;
} ) ;
2025-06-10 16:00:13 -07:00
} ) ;
2025-06-18 16:34:00 -07:00
return {
authUrl ,
loginCompletePromise ,
} ;
2025-06-10 16:00:13 -07:00
}
2025-06-18 16:34:00 -07:00
export function getAvailablePort ( ) : Promise < number > {
2025-06-10 16:00:13 -07:00
return new Promise ( ( resolve , reject ) = > {
let port = 0 ;
try {
2025-08-17 12:43:21 -04:00
const portStr = process . env [ 'OAUTH_CALLBACK_PORT' ] ;
2025-07-18 09:55:26 +08:00
if ( portStr ) {
port = parseInt ( portStr , 10 ) ;
if ( isNaN ( port ) || port <= 0 || port > 65535 ) {
return reject (
new Error ( ` Invalid value for OAUTH_CALLBACK_PORT: " ${ portStr } " ` ) ,
) ;
}
return resolve ( port ) ;
}
2025-06-10 16:00:13 -07:00
const server = net . createServer ( ) ;
server . listen ( 0 , ( ) = > {
2026-02-20 20:44:23 +00:00
const address = server . address ( ) ;
if ( address && typeof address === 'object' ) {
port = address . port ;
}
2025-06-10 16:00:13 -07:00
} ) ;
server . on ( 'listening' , ( ) = > {
server . close ( ) ;
server . unref ( ) ;
} ) ;
server . on ( 'error' , ( e ) = > reject ( e ) ) ;
server . on ( 'close' , ( ) = > resolve ( port ) ) ;
} catch ( e ) {
reject ( e ) ;
}
} ) ;
}
2025-06-16 19:31:32 -07:00
2025-10-27 16:05:11 -04:00
async function fetchCachedCredentials ( ) : Promise <
Credentials | JWTInput | null
> {
2025-09-16 10:05:29 -04:00
const useEncryptedStorage = getUseEncryptedStorageFlag ( ) ;
if ( useEncryptedStorage ) {
2025-12-02 07:11:40 +09:00
return OAuthCredentialStorage . loadCredentials ( ) ;
2025-09-16 10:05:29 -04:00
}
2025-08-18 14:11:19 -07:00
const pathsToTry = [
2025-08-20 10:55:47 +09:00
Storage . getOAuthCredsPath ( ) ,
2025-08-18 14:11:19 -07:00
process . env [ 'GOOGLE_APPLICATION_CREDENTIALS' ] ,
] . filter ( ( p ) : p is string = > ! ! p ) ;
2025-06-18 17:24:46 -07:00
2025-08-18 14:11:19 -07:00
for ( const keyFile of pathsToTry ) {
try {
2025-10-27 16:05:11 -04:00
const keyFileString = await fs . readFile ( keyFile , 'utf-8' ) ;
2026-02-21 01:12:56 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
2025-10-27 16:05:11 -04:00
return JSON . parse ( keyFileString ) ;
2025-09-03 13:51:29 -07:00
} catch ( error ) {
// Log specific error for debugging, but continue trying other paths
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-09-03 13:51:29 -07:00
` Failed to load credentials from ${ keyFile } : ` ,
getErrorMessage ( error ) ,
) ;
2025-08-18 14:11:19 -07:00
}
2025-06-16 19:31:32 -07:00
}
2025-08-18 14:11:19 -07:00
2025-10-27 16:05:11 -04:00
return null ;
2025-06-16 19:31:32 -07:00
}
2025-08-19 17:06:25 -07:00
export function clearOauthClientCache() {
oauthClientPromises . clear ( ) ;
}
2025-06-19 16:52:22 -07:00
export async function clearCachedCredentialFile() {
try {
2025-09-16 10:05:29 -04:00
const useEncryptedStorage = getUseEncryptedStorageFlag ( ) ;
if ( useEncryptedStorage ) {
await OAuthCredentialStorage . clearCredentials ( ) ;
} else {
await fs . rm ( Storage . getOAuthCredsPath ( ) , { force : true } ) ;
}
2025-06-29 16:35:20 -04:00
// Clear the Google Account ID cache when credentials are cleared
2025-08-20 10:55:47 +09:00
await userAccountManager . clearCachedGoogleAccount ( ) ;
2025-08-19 17:06:25 -07:00
// Clear the in-memory OAuth client cache to force re-authentication
clearOauthClientCache ( ) ;
} catch ( e ) {
2025-10-27 16:46:35 -07:00
debugLogger . warn ( 'Failed to clear cached credentials:' , e ) ;
2025-06-19 16:52:22 -07:00
}
}
2025-06-29 16:35:20 -04:00
2025-07-11 10:57:35 -07:00
async function fetchAndCacheUserInfo ( client : OAuth2Client ) : Promise < void > {
2025-06-29 16:35:20 -04:00
try {
2025-07-11 10:57:35 -07:00
const { token } = await client . getAccessToken ( ) ;
if ( ! token ) {
return ;
}
const response = await fetch (
'https://www.googleapis.com/oauth2/v2/userinfo' ,
{
headers : {
Authorization : ` Bearer ${ token } ` ,
} ,
2025-07-03 16:54:35 -04:00
} ,
) ;
2025-07-11 10:57:35 -07:00
if ( ! response . ok ) {
2025-10-27 16:46:35 -07:00
debugLogger . log (
2025-07-11 10:57:35 -07:00
'Failed to fetch user info:' ,
response . status ,
response . statusText ,
) ;
return ;
2025-06-29 16:35:20 -04:00
}
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2025-07-11 10:57:35 -07:00
const userInfo = await response . json ( ) ;
2025-08-20 10:55:47 +09:00
await userAccountManager . cacheGoogleAccount ( userInfo . email ) ;
2025-06-29 16:35:20 -04:00
} catch ( error ) {
2025-10-27 16:46:35 -07:00
debugLogger . log ( 'Error retrieving user info:' , error ) ;
2025-06-29 16:35:20 -04:00
}
}
2025-08-15 22:05:59 -07:00
// Helper to ensure test isolation
export function resetOauthClientForTesting() {
oauthClientPromises . clear ( ) ;
}
2025-12-02 21:27:37 -08:00
async function cacheCredentials ( credentials : Credentials ) {
const filePath = Storage . getOAuthCredsPath ( ) ;
await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
const credString = JSON . stringify ( credentials , null , 2 ) ;
await fs . writeFile ( filePath , credString , { mode : 0o600 } ) ;
try {
await fs . chmod ( filePath , 0 o600 ) ;
} catch {
/* empty */
}
}