greppy/auth/
google.rs

1use crate::auth::server;
2use anyhow::{Context, Result};
3use oauth2::reqwest::async_http_client;
4use oauth2::{
5    basic::BasicClient, AuthUrl, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
6    Scope, TokenResponse, TokenUrl,
7};
8use std::net::TcpListener;
9
10// Gemini CLI OAuth credentials (from opencode-gemini-auth)
11const GEMINI_CLIENT_ID: &str =
12    "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
13const GEMINI_CLIENT_SECRET: &str = "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";
14const AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth";
15const TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
16
17pub async fn authenticate() -> Result<String> {
18    // Use fixed port 8085 to match Gemini CLI redirect URI
19    let listener =
20        TcpListener::bind("127.0.0.1:8085").or_else(|_| TcpListener::bind("127.0.0.1:0"))?;
21    let port = listener.local_addr()?.port();
22    let redirect_url = format!("http://localhost:{}/oauth2callback", port);
23
24    let client = BasicClient::new(
25        ClientId::new(GEMINI_CLIENT_ID.to_string()),
26        Some(ClientSecret::new(GEMINI_CLIENT_SECRET.to_string())),
27        AuthUrl::new(AUTH_URL.to_string())?,
28        Some(TokenUrl::new(TOKEN_URL.to_string())?),
29    )
30    .set_redirect_uri(RedirectUrl::new(redirect_url)?);
31
32    // Generate PKCE challenge
33    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
34
35    // Generate Auth URL with required scopes
36    let (authorize_url, csrf_state) = client
37        .authorize_url(CsrfToken::new_random)
38        .add_scope(Scope::new(
39            "https://www.googleapis.com/auth/cloud-platform".to_string(),
40        ))
41        .add_scope(Scope::new(
42            "https://www.googleapis.com/auth/userinfo.email".to_string(),
43        ))
44        .add_scope(Scope::new(
45            "https://www.googleapis.com/auth/userinfo.profile".to_string(),
46        ))
47        .add_extra_param("access_type", "offline")
48        .add_extra_param("prompt", "consent")
49        .set_pkce_challenge(pkce_challenge)
50        .url();
51
52    println!("Opening browser for Google authentication...");
53    println!("If browser doesn't open, visit: {}", authorize_url);
54
55    if let Err(e) = open::that(authorize_url.to_string()) {
56        eprintln!(
57            "Failed to open browser: {}. Please open the URL manually.",
58            e
59        );
60    }
61
62    // Start Server and wait for code
63    let code = server::run_server(listener, csrf_state.secret().clone()).await?;
64
65    // Exchange code for token
66    let token_result = client
67        .exchange_code(code)
68        .set_pkce_verifier(pkce_verifier)
69        .request_async(async_http_client)
70        .await
71        .context("Failed to exchange code for token")?;
72
73    // Return refresh token if available, otherwise access token
74    if let Some(refresh_token) = token_result.refresh_token() {
75        Ok(refresh_token.secret().clone())
76    } else {
77        Ok(token_result.access_token().secret().clone())
78    }
79}