authx_plugins/oauth/providers/
google.rs1use 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}