Skip to main content

authx_plugins/oauth/providers/
google.rs

1use async_trait::async_trait;
2use tracing::instrument;
3
4use authx_core::error::{AuthError, Result};
5
6use super::{OAuthProvider, OAuthTokens, OAuthUserInfo};
7
8pub struct GoogleProvider {
9    client_id: String,
10    client_secret: String,
11    http: reqwest::Client,
12}
13
14impl GoogleProvider {
15    pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
16        Self {
17            client_id: client_id.into(),
18            client_secret: client_secret.into(),
19            http: reqwest::Client::new(),
20        }
21    }
22}
23
24#[async_trait]
25impl OAuthProvider for GoogleProvider {
26    fn name(&self) -> &'static str {
27        "google"
28    }
29
30    fn authorization_url(&self, state: &str, pkce_challenge: &str) -> String {
31        format!(
32            "https://accounts.google.com/o/oauth2/v2/auth\
33             ?client_id={}\
34             &response_type=code\
35             &scope=openid%20email%20profile\
36             &redirect_uri=https%3A%2F%2Flocalhost%2Fauth%2Foauth%2Fgoogle%2Fcallback\
37             &state={}\
38             &code_challenge={}\
39             &code_challenge_method=S256\
40             &access_type=offline",
41            urlencoding::encode(&self.client_id),
42            urlencoding::encode(state),
43            urlencoding::encode(pkce_challenge),
44        )
45    }
46
47    #[instrument(skip(self, code, pkce_verifier))]
48    async fn exchange_code(
49        &self,
50        code: &str,
51        pkce_verifier: &str,
52        redirect_uri: &str,
53    ) -> Result<OAuthTokens> {
54        let res = self
55            .http
56            .post("https://oauth2.googleapis.com/token")
57            .form(&[
58                ("code", code),
59                ("client_id", &self.client_id),
60                ("client_secret", &self.client_secret),
61                ("redirect_uri", redirect_uri),
62                ("grant_type", "authorization_code"),
63                ("code_verifier", pkce_verifier),
64            ])
65            .send()
66            .await
67            .map_err(|e| AuthError::Internal(format!("google token request failed: {e}")))?;
68
69        if !res.status().is_success() {
70            let body = res.text().await.unwrap_or_default();
71            return Err(AuthError::Internal(format!(
72                "google token exchange error: {body}"
73            )));
74        }
75
76        let json: serde_json::Value = res
77            .json()
78            .await
79            .map_err(|e| AuthError::Internal(format!("google token json: {e}")))?;
80
81        tracing::debug!("google token exchange succeeded");
82        Ok(OAuthTokens {
83            access_token: json["access_token"].as_str().unwrap_or("").to_owned(),
84            refresh_token: json["refresh_token"].as_str().map(ToOwned::to_owned),
85            expires_in: json["expires_in"].as_u64(),
86        })
87    }
88
89    #[instrument(skip(self, access_token))]
90    async fn fetch_user_info(&self, access_token: &str) -> Result<OAuthUserInfo> {
91        let res = self
92            .http
93            .get("https://openidconnect.googleapis.com/v1/userinfo")
94            .bearer_auth(access_token)
95            .send()
96            .await
97            .map_err(|e| AuthError::Internal(format!("google userinfo request failed: {e}")))?;
98
99        if !res.status().is_success() {
100            return Err(AuthError::Internal("google userinfo error".into()));
101        }
102
103        let json: serde_json::Value = res
104            .json()
105            .await
106            .map_err(|e| AuthError::Internal(format!("google userinfo json: {e}")))?;
107
108        let sub = json["sub"]
109            .as_str()
110            .ok_or_else(|| AuthError::Internal("missing sub".into()))?;
111        let email = json["email"]
112            .as_str()
113            .ok_or_else(|| AuthError::Internal("missing email".into()))?;
114
115        tracing::debug!(provider = "google", "user info fetched");
116        Ok(OAuthUserInfo {
117            provider_user_id: sub.to_owned(),
118            email: email.to_owned(),
119            name: json["name"].as_str().map(ToOwned::to_owned),
120        })
121    }
122}