@@ -5,6 +5,7 @@ use forge_domain::{
55 ApiKey , ApiKeyRequest , AuthContextRequest , AuthContextResponse , AuthCredential , CodeRequest ,
66 DeviceCodeRequest , OAuthConfig , OAuthTokenResponse , OAuthTokens , ProviderId , URLParam ,
77} ;
8+ use google_cloud_auth:: credentials:: Builder ;
89use oauth2:: basic:: BasicClient ;
910use oauth2:: { ClientId , DeviceAuthorizationUrl , Scope , TokenUrl } ;
1011use reqwest:: header:: { HeaderMap , HeaderValue } ;
@@ -343,6 +344,99 @@ impl AuthStrategy for OAuthWithApiKeyStrategy {
343344 }
344345}
345346
347+ /// Google Application Default Credentials (ADC) Strategy
348+ /// Uses Google Cloud SDK's ADC mechanism with automatic token refresh
349+ pub struct GoogleAdcStrategy {
350+ provider_id : ProviderId ,
351+ required_params : Vec < URLParam > ,
352+ }
353+
354+ impl GoogleAdcStrategy {
355+ pub fn new ( provider_id : ProviderId , required_params : Vec < URLParam > ) -> Self {
356+ Self { provider_id, required_params }
357+ }
358+ }
359+
360+ #[ async_trait:: async_trait]
361+ impl AuthStrategy for GoogleAdcStrategy {
362+ async fn init ( & self ) -> anyhow:: Result < AuthContextRequest > {
363+ // For Google ADC, we don't need any user interaction for the API key
364+ // The credentials are automatically discovered from:
365+ // 1. GOOGLE_APPLICATION_CREDENTIALS env var (service account)
366+ // 2. gcloud ADC credentials (user credentials)
367+ // 3. Metadata server (GCP environment)
368+ // However, we still need to collect URL params like PROJECT_ID and LOCATION
369+ Ok ( AuthContextRequest :: ApiKey ( ApiKeyRequest {
370+ required_params : self . required_params . clone ( ) ,
371+ existing_params : None ,
372+ api_key : Some ( "google_adc_marker" . to_string ( ) . into ( ) ) , // Marker to indicate ADC usage
373+ } ) )
374+ }
375+
376+ async fn complete (
377+ & self ,
378+ context_response : AuthContextResponse ,
379+ ) -> anyhow:: Result < AuthCredential > {
380+ match context_response {
381+ AuthContextResponse :: ApiKey ( ctx) => {
382+ // Validate that gcloud auth is properly configured before completing
383+ // authentication This ensures the user has run 'gcloud auth
384+ // application-default login'
385+ use google_cloud_auth:: credentials:: Builder ;
386+ let credentials = Builder :: default ( )
387+ . build_access_token_credentials ( )
388+ . map_err ( |e| {
389+ AuthError :: CompletionFailed ( format ! (
390+ "Google ADC not configured: {e}. Please run 'gcloud auth application-default login' to set up credentials."
391+ ) )
392+ } ) ?;
393+
394+ // Try to fetch a token to verify authentication works
395+ credentials
396+ . access_token ( )
397+ . await
398+ . map_err ( |e| {
399+ AuthError :: CompletionFailed ( format ! (
400+ "{e}. Please run 'gcloud auth application-default login' to set up credentials."
401+ ) )
402+ } ) ?;
403+
404+ // For Google ADC, we save a marker instead of the actual token
405+ // The token will be refreshed on every use
406+ // But we still need to save the url_params (PROJECT_ID, LOCATION)
407+ Ok ( AuthCredential :: new_api_key (
408+ self . provider_id . clone ( ) ,
409+ ApiKey :: from ( "google_adc_marker" . to_string ( ) ) , /* Marker that will trigger
410+ * refresh */
411+ )
412+ . url_params ( ctx. response . url_params ) )
413+ }
414+ _ => Err ( AuthError :: InvalidContext ( "Expected ApiKey context" . to_string ( ) ) . into ( ) ) ,
415+ }
416+ }
417+
418+ async fn refresh ( & self , _credential : & AuthCredential ) -> anyhow:: Result < AuthCredential > {
419+ // Google ADC handles token refresh automatically
420+ // We just need to get a fresh token using the Builder API
421+ let credentials = Builder :: default ( )
422+ . build_access_token_credentials ( )
423+ . map_err ( |e| {
424+ AuthError :: RefreshFailed ( format ! (
425+ "Failed to create Google credentials builder: {e}"
426+ ) )
427+ } ) ?;
428+
429+ let access_token = credentials. access_token ( ) . await . map_err ( |e| {
430+ AuthError :: RefreshFailed ( format ! ( "Failed to refresh Google access token: {e}" ) )
431+ } ) ?;
432+
433+ Ok ( AuthCredential :: new_api_key (
434+ self . provider_id . clone ( ) ,
435+ ApiKey :: from ( access_token. token ) ,
436+ ) )
437+ }
438+ }
439+
346440/// Refresh OAuth credential - handles all OAuth flows
347441async fn refresh_oauth_credential (
348442 credential : & AuthCredential ,
@@ -590,6 +684,7 @@ pub enum AnyAuthStrategy {
590684 OAuthCodeGithub ( OAuthCodeStrategy < GithubHttpProvider > ) ,
591685 OAuthDevice ( OAuthDeviceStrategy ) ,
592686 OAuthWithApiKey ( OAuthWithApiKeyStrategy ) ,
687+ GoogleAdc ( GoogleAdcStrategy ) ,
593688}
594689
595690#[ async_trait:: async_trait]
@@ -602,6 +697,7 @@ impl AuthStrategy for AnyAuthStrategy {
602697 Self :: OAuthCodeGithub ( s) => s. init ( ) . await ,
603698 Self :: OAuthDevice ( s) => s. init ( ) . await ,
604699 Self :: OAuthWithApiKey ( s) => s. init ( ) . await ,
700+ Self :: GoogleAdc ( s) => s. init ( ) . await ,
605701 }
606702 }
607703
@@ -616,6 +712,7 @@ impl AuthStrategy for AnyAuthStrategy {
616712 Self :: OAuthCodeGithub ( s) => s. complete ( context_response) . await ,
617713 Self :: OAuthDevice ( s) => s. complete ( context_response) . await ,
618714 Self :: OAuthWithApiKey ( s) => s. complete ( context_response) . await ,
715+ Self :: GoogleAdc ( s) => s. complete ( context_response) . await ,
619716 }
620717 }
621718
@@ -627,6 +724,7 @@ impl AuthStrategy for AnyAuthStrategy {
627724 Self :: OAuthCodeGithub ( s) => s. refresh ( credential) . await ,
628725 Self :: OAuthDevice ( s) => s. refresh ( credential) . await ,
629726 Self :: OAuthWithApiKey ( s) => s. refresh ( credential) . await ,
727+ Self :: GoogleAdc ( s) => s. refresh ( credential) . await ,
630728 }
631729 }
632730}
@@ -696,6 +794,9 @@ impl StrategyFactory for ForgeAuthStrategyFactory {
696794 ) ) )
697795 }
698796 }
797+ forge_domain:: AuthMethod :: GoogleAdc => Ok ( AnyAuthStrategy :: GoogleAdc (
798+ GoogleAdcStrategy :: new ( provider_id, required_params) ,
799+ ) ) ,
699800 }
700801 }
701802}
0 commit comments