fetcher_core/auth/
google.rs1#![doc = "This module contains the Google authenticator that can access Google services via OAuth2"]
9
10use serde::Deserialize;
11use std::time::{Duration, Instant};
12
13const GOOGLE_AUTH_URL: &str = "https://accounts.google.com/o/oauth2/token";
14
15#[allow(clippy::doc_markdown)]
16#[derive(Clone, Debug)]
18pub struct AccessToken {
19 pub token: String,
21
22 pub expires: Instant,
24}
25
26#[derive(Deserialize)]
27struct AccessTokenResponce {
28 access_token: String,
29 expires_in: u64,
30}
31
32#[allow(clippy::doc_markdown)]
33#[derive(Clone, Debug)]
36pub struct Google {
37 pub client_id: String,
39
40 pub client_secret: String,
42
43 pub refresh_token: String,
45
46 access_token: Option<AccessToken>,
48}
49
50#[allow(missing_docs)] #[derive(thiserror::Error, Debug)]
52pub enum GoogleOAuth2Error {
53 #[error("Error contacting Google servers for authentication")]
54 Post(#[source] reqwest::Error),
55
56 #[error("Can't get a new OAuth2 refresh token from Google: {0}")]
57 RefreshToken(String),
58
59 #[error("Can't get a new OAuth2 access token from Google: {0}")]
60 AccessToken(String),
61}
62
63impl Google {
64 #[allow(clippy::doc_markdown)]
65 #[must_use]
67 pub fn new(client_id: String, client_secret: String, refresh_token: String) -> Self {
68 Self {
69 client_id,
70 client_secret,
71 refresh_token,
72 access_token: None,
73 }
74 }
75
76 pub async fn get_new_access_token(&mut self) -> Result<&AccessToken, GoogleOAuth2Error> {
82 let AccessTokenResponce {
83 access_token,
84 expires_in,
85 } = generate_access_token(&self.client_id, &self.client_secret, &self.refresh_token).await?;
86
87 tracing::debug!("New access token expires in {expires_in}s");
88
89 self.access_token = Some(AccessToken {
90 token: access_token,
91 expires: Instant::now() + Duration::from_secs(expires_in),
92 });
93
94 Ok(self
95 .access_token
96 .as_ref()
97 .expect("Token should have just been validated and thus be present and valid"))
98 }
99
100 #[tracing::instrument(name = "google_oauth2_access_token")]
106 pub async fn access_token(&mut self) -> Result<&str, GoogleOAuth2Error> {
107 if {
111 let access_token_doesnt_exist = self.access_token.is_none();
113 if access_token_doesnt_exist {
114 tracing::trace!("Access token doesn't exist");
115 }
116
117 access_token_doesnt_exist
118 } || {
119 let is_expired = self
121 .access_token
122 .as_ref()
123 .and_then(|x| Instant::now().checked_duration_since(x.expires))
124 .is_some();
125
126 if is_expired {
127 tracing::trace!("Access token has expired");
128 }
129
130 is_expired
131 } {
132 self.get_new_access_token().await?;
133 }
134
135 #[allow(clippy::missing_panics_doc)] let access_token = self
137 .access_token
138 .as_ref()
139 .expect("Token should have just been validated and thus be present and valid");
140
141 tracing::debug!(
142 "Access token is still valid for {:?}s",
143 access_token
144 .expires
145 .checked_duration_since(Instant::now())
146 .map(|dur| dur.as_secs())
147 );
148
149 Ok(&access_token.token)
150 }
151}
152
153impl GoogleOAuth2Error {
154 pub(crate) fn is_connection_err(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
155 #[allow(clippy::match_wildcard_for_single_variants)]
157 match self {
158 GoogleOAuth2Error::Post(_) => Some(self),
159 _ => None,
160 }
161 }
162}
163
164#[allow(clippy::doc_markdown)]
165pub async fn generate_refresh_token(
171 client_id: &str,
172 client_secret: &str,
173 access_code: &str,
174) -> Result<String, GoogleOAuth2Error> {
175 #[derive(Deserialize)]
176 struct Response {
177 refresh_token: String,
178 }
179
180 tracing::debug!("Generating a new OAuth2 refresh token from client_id: {client_id:?}, client_secret: {client_secret:?}, and access_code: {access_code:?}");
181
182 let body = [
183 ("client_id", client_id),
184 ("client_secret", client_secret),
185 ("code", access_code),
186 ("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"),
187 ("grant_type", "authorization_code"),
188 ];
189
190 let resp = reqwest::Client::new()
191 .post(GOOGLE_AUTH_URL)
192 .form(&body)
193 .send()
194 .await
195 .map_err(GoogleOAuth2Error::Post)?
196 .text()
197 .await
198 .map_err(GoogleOAuth2Error::Post)?;
199
200 tracing::debug!("Got {resp:?} from the Google OAuth2 endpoint");
201
202 let Response { refresh_token } =
203 serde_json::from_str(&resp).map_err(|_| GoogleOAuth2Error::RefreshToken(resp))?;
204
205 Ok(refresh_token)
206}
207
208async fn generate_access_token(
209 client_id: &str,
210 client_secret: &str,
211 refresh_token: &str,
212) -> Result<AccessTokenResponce, GoogleOAuth2Error> {
213 tracing::debug!("Generating a new OAuth2 access token from client_id: {client_id:?}, client_secret: {client_secret:?}, and refresh_token: {refresh_token:?}");
214
215 let body = [
216 ("client_id", client_id),
217 ("client_secret", client_secret),
218 ("refresh_token", refresh_token),
219 ("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"),
220 ("grant_type", "refresh_token"),
221 ];
222
223 let resp = reqwest::Client::new()
224 .post(GOOGLE_AUTH_URL)
225 .form(&body)
226 .send()
227 .await
228 .map_err(GoogleOAuth2Error::Post)?
229 .text()
230 .await
231 .map_err(GoogleOAuth2Error::Post)?;
232
233 tracing::debug!("Got {resp:?} from the Google OAuth2 endpoint");
234
235 serde_json::from_str(&resp).map_err(|_| GoogleOAuth2Error::AccessToken(resp))
236}