Skip to main content

mkt_google/
auth.rs

1//! `OAuth2` refresh-token exchange for the Google Ads API.
2
3use mkt_core::error::{MktError, Result};
4use secrecy::SecretString;
5
6/// Google's `OAuth2` token endpoint.
7pub const GOOGLE_TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
8
9/// Exchange an `OAuth2` refresh token for a short-lived access token.
10///
11/// `token_url` is parameterized for testing; production callers pass
12/// [`GOOGLE_TOKEN_URL`].
13///
14/// # Errors
15///
16/// Returns [`MktError::AuthError`] if the token endpoint rejects the
17/// exchange or the response lacks an `access_token` field, and
18/// [`MktError::Http`] on transport failures.
19pub async fn fetch_access_token(
20    client_id: &str,
21    client_secret: &str,
22    refresh_token: &str,
23    token_url: &str,
24) -> Result<SecretString> {
25    let http = mkt_core::http::build_http_client(None)?;
26    let params = [
27        ("grant_type", "refresh_token"),
28        ("client_id", client_id),
29        ("client_secret", client_secret),
30        ("refresh_token", refresh_token),
31    ];
32
33    let response = http.post(token_url).form(&params).send().await?;
34    let status = response.status().as_u16();
35    let body: serde_json::Value = response.json().await?;
36
37    if !(200..300).contains(&status) {
38        let detail = body["error_description"]
39            .as_str()
40            .or_else(|| body["error"].as_str())
41            .unwrap_or("token endpoint returned an error");
42        return Err(MktError::auth_error(
43            "google",
44            &format!("OAuth refresh failed ({status}): {detail}"),
45        ));
46    }
47
48    body["access_token"].as_str().map_or_else(
49        || {
50            Err(MktError::auth_error(
51                "google",
52                "token endpoint response missing 'access_token'",
53            ))
54        },
55        |token| Ok(SecretString::new(token.to_string().into())),
56    )
57}