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 ,
Credentials ,
Compute ,
CodeChallengeMethod ,
} 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-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-07-10 18:59:02 -07:00
import { Config } from '../config/config.js' ;
2025-07-07 15:02:13 -07:00
import { getErrorMessage } 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' ;
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-08-15 22:05:59 -07:00
const oauthClientPromises = new Map < AuthType , Promise < OAuth2Client > > ( ) ;
async function initOauthClient (
2025-07-07 15:02:13 -07:00
authType : AuthType ,
2025-07-10 18:59:02 -07:00
config : Config ,
2025-07-07 15:02:13 -07:00
) : Promise < OAuth2Client > {
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-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 ) = > {
await cacheCredentials ( tokens ) ;
} ) ;
2025-06-10 16:00:13 -07:00
2025-07-07 15:02:13 -07:00
// If there are cached creds on disk, they always take precedence
2025-06-18 16:34:00 -07:00
if ( await loadCachedCredentials ( client ) ) {
// Found valid cached credentials.
2025-07-11 10:57:35 -07:00
// Check if we need to retrieve Google Account ID or Email
2025-08-20 10:55:47 +09:00
if ( ! userAccountManager . getCachedGoogleAccount ( ) ) {
2025-06-29 16:35:20 -04:00
try {
2025-07-11 10:57:35 -07:00
await fetchAndCacheUserInfo ( client ) ;
2025-07-07 15:02:13 -07:00
} catch {
// Non-fatal, continue with existing auth.
2025-06-29 16:35:20 -04:00
}
}
2025-07-07 15:02:13 -07:00
console . log ( 'Loaded cached credentials.' ) ;
2025-06-18 16:34:00 -07:00
return client ;
}
2025-06-10 16:00:13 -07:00
2025-07-07 15:02:13 -07:00
// In Google Cloud Shell, we can use Application Default Credentials (ADC)
// provided via its metadata server to authenticate non-interactively using
// the identity of the user logged into Cloud Shell.
if ( authType === AuthType . CLOUD_SHELL ) {
try {
console . log ( "Attempting to authenticate via Cloud Shell VM's ADC." ) ;
const computeClient = new Compute ( {
// We can leave this empty, since the metadata server will provide
// the service account email.
} ) ;
await computeClient . getAccessToken ( ) ;
console . log ( 'Authentication successful.' ) ;
// Do not cache creds in this case; note that Compute client will handle its own refresh
return computeClient ;
} catch ( e ) {
throw new Error (
` Could not authenticate using Cloud Shell credentials. Please select a different authentication method or ensure you are in a properly configured environment. Error: ${ getErrorMessage (
e ,
) } ` ,
) ;
}
}
2025-07-21 16:23:28 -07:00
if ( config . isBrowserLaunchSuppressed ( ) ) {
2025-07-10 18:59:02 -07:00
let success = false ;
const maxRetries = 2 ;
for ( let i = 0 ; ! success && i < maxRetries ; i ++ ) {
success = await authWithUserCode ( client ) ;
if ( ! success ) {
console . error (
'\nFailed to authenticate with user code.' ,
i === maxRetries - 1 ? '' : 'Retrying...\n' ,
) ;
}
}
if ( ! success ) {
process . exit ( 1 ) ;
}
} else {
const webLogin = await authWithWeb ( client ) ;
console . log (
` \ n \ nCode Assist login required. \ n ` +
` Attempting to open authentication page in your browser. \ n ` +
` Otherwise navigate to: \ n \ n ${ webLogin . authUrl } \ 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.
childProcess . on ( 'error' , ( _ ) = > {
console . error (
2025-07-18 17:22:50 -07:00
'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.' ,
2025-07-18 09:55:26 +08:00
) ;
2025-07-18 17:22:50 -07:00
process . exit ( 1 ) ;
2025-07-18 09:55:26 +08:00
} ) ;
} catch ( err ) {
console . error (
'An unexpected error occurred while trying to open the browser:' ,
err ,
2025-07-18 17:22:50 -07:00
'\nPlease try running again with NO_BROWSER=true set.' ,
2025-07-18 09:55:26 +08:00
) ;
2025-07-18 17:22:50 -07:00
process . exit ( 1 ) ;
2025-07-18 09:55:26 +08:00
}
2025-07-10 18:59:02 -07:00
console . log ( 'Waiting for authentication...' ) ;
await webLogin . loginCompletePromise ;
}
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 ,
) : Promise < OAuth2Client > {
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-07-18 17:22:50 -07:00
const redirectUri = 'https://codeassist.google.com/authcode' ;
2025-07-10 18:59:02 -07:00
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-12 15:42:47 -07:00
console . log ( 'Please visit the following URL to authorize the application:' ) ;
console . log ( '' ) ;
console . log ( authUrl ) ;
console . log ( '' ) ;
2025-07-10 18:59:02 -07:00
const code = await new Promise < string > ( ( resolve ) = > {
const rl = readline . createInterface ( {
input : process.stdin ,
output : process.stdout ,
} ) ;
2025-07-12 15:42:47 -07:00
rl . question ( 'Enter the authorization code: ' , ( code ) = > {
2025-07-10 18:59:02 -07:00
rl . close ( ) ;
2025-07-12 15:42:47 -07:00
resolve ( code . trim ( ) ) ;
2025-07-10 18:59:02 -07:00
} ) ;
} ) ;
2025-06-18 16:34:00 -07:00
2025-07-10 18:59:02 -07:00
if ( ! code ) {
console . error ( 'Authorization code is required.' ) ;
return false ;
}
try {
2025-07-12 15:42:47 -07:00
const { tokens } = await client . getToken ( {
2025-07-10 18:59:02 -07:00
code ,
codeVerifier : codeVerifier.codeVerifier ,
redirect_uri : redirectUri ,
} ) ;
2025-07-12 15:42:47 -07:00
client . setCredentials ( tokens ) ;
2025-07-10 18:59:02 -07:00
} catch ( _error ) {
return false ;
}
return true ;
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).
2025-08-17 12:43:21 -04:00
const host = process . env [ 'OAUTH_CALLBACK_HOST' ] || 'localhost' ;
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.
2025-06-18 16:34:00 -07:00
const redirectUri = ` http://localhost: ${ port } /oauth2callback ` ;
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 ( ) ;
reject ( new Error ( 'Unexpected request: ' + req . url ) ) ;
}
// acquire the code from the querystring, and close the web server.
const qs = new url . URL ( req . url ! , 'http://localhost:3000' ) . searchParams ;
if ( qs . get ( 'error' ) ) {
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_FAILURE_URL } ) ;
res . end ( ) ;
2025-06-11 13:26:41 -07:00
2025-06-10 16:00:13 -07:00
reject ( new Error ( ` Error during authentication: ${ qs . get ( 'error' ) } ` ) ) ;
} else if ( qs . get ( 'state' ) !== state ) {
res . end ( 'State mismatch. Possible CSRF attack' ) ;
2025-06-11 13:26:41 -07:00
2025-06-10 16:00:13 -07:00
reject ( new Error ( 'State mismatch. Possible CSRF attack' ) ) ;
} else if ( qs . get ( 'code' ) ) {
2025-06-18 16:34:00 -07:00
const { tokens } = await client . getToken ( {
code : qs.get ( 'code' ) ! ,
redirect_uri : redirectUri ,
} ) ;
client . setCredentials ( tokens ) ;
2025-06-29 16:35:20 -04:00
// Retrieve and cache Google Account ID during authentication
try {
2025-07-11 10:57:35 -07:00
await fetchAndCacheUserInfo ( client ) ;
2025-06-29 16:35:20 -04:00
} catch ( error ) {
console . error (
'Failed to retrieve Google Account ID during authentication:' ,
error ,
) ;
// Don't fail the auth flow if Google Account ID retrieval fails
}
2025-06-10 16:00:13 -07:00
res . writeHead ( HTTP_REDIRECT , { Location : SIGN_IN_SUCCESS_URL } ) ;
res . end ( ) ;
2025-06-18 16:34:00 -07:00
resolve ( ) ;
2025-06-10 16:00:13 -07:00
} else {
reject ( new Error ( 'No code found in request' ) ) ;
}
} catch ( e ) {
reject ( e ) ;
} finally {
server . close ( ) ;
}
} ) ;
2025-07-18 09:55:26 +08:00
server . listen ( port , host ) ;
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 , ( ) = > {
const address = server . address ( ) ! as net . AddressInfo ;
port = address . port ;
} ) ;
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-06-18 16:34:00 -07:00
async function loadCachedCredentials ( client : OAuth2Client ) : Promise < boolean > {
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 {
const creds = await fs . readFile ( keyFile , 'utf-8' ) ;
client . setCredentials ( JSON . parse ( creds ) ) ;
2025-06-18 17:24:46 -07:00
2025-08-18 14:11:19 -07:00
// This will verify locally that the credentials look good.
const { token } = await client . getAccessToken ( ) ;
if ( ! token ) {
continue ;
}
2025-06-18 17:24:46 -07:00
2025-08-18 14:11:19 -07:00
// This will check with the server to see if it hasn't been revoked.
await client . getTokenInfo ( token ) ;
2025-06-18 16:34:00 -07:00
2025-08-18 14:11:19 -07:00
return true ;
} catch ( _ ) {
// Ignore and try next path.
}
2025-06-16 19:31:32 -07:00
}
2025-08-18 14:11:19 -07:00
return false ;
2025-06-16 19:31:32 -07:00
}
2025-06-18 16:34:00 -07:00
async function cacheCredentials ( credentials : Credentials ) {
2025-08-20 10:55:47 +09:00
const filePath = Storage . getOAuthCredsPath ( ) ;
2025-06-18 09:49:13 -07:00
await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
const credString = JSON . stringify ( credentials , null , 2 ) ;
2025-08-08 23:05:30 -04:00
await fs . writeFile ( filePath , credString , { mode : 0o600 } ) ;
2025-06-18 09:49:13 -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-08-20 10:55:47 +09:00
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 ) {
console . error ( '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 ) {
console . error (
'Failed to fetch user info:' ,
response . status ,
response . statusText ,
) ;
return ;
2025-06-29 16:35:20 -04:00
}
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-07-11 10:57:35 -07:00
console . error ( '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 ( ) ;
}