drive_v3/token.rs
1use std::fmt;
2use serde::{Deserialize, Serialize};
3use reqwest::{Url, blocking::Client};
4
5use crate::{Error, ErrorKind, LocalServer, ClientSecrets};
6
7/// An opaque (proprietary format) token that conforms to the
8/// [OAuth 2.0 framework](https://datatracker.ietf.org/doc/html/rfc6749#section-1.4).
9///
10/// They contain authorization information, but not identity information. They
11/// are used to authenticate and provide authorization information to Google
12/// APIs.
13///
14/// # Examples:
15///
16/// ```no_run
17/// use drive_v3::AccessToken;
18/// use drive_v3::ClientSecrets;
19/// # use drive_v3::Error;
20///
21/// // Load your client_secrets file
22/// let secrets_path = "my_client_secrets.json";
23/// # let secrets_path = "../.secure-files/google_drive_secrets.json";
24/// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
25///
26/// // Request an access token, this will prompt you to authorize via the browser
27/// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
28/// let my_access_token = AccessToken::request(&my_client_secrets, &scopes)?;
29///
30/// // After getting your token you can make a request (with reqwest for example) using it for authorization
31/// let client = reqwest::blocking::Client::new();
32/// let body = client.get("google-api-endpoint")
33/// .bearer_auth(&my_access_token.access_token)
34/// .send()?
35/// .text()?;
36///
37/// println!("response: {:?}", body);
38/// # Ok::<(), Error>(())
39/// ```
40#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct AccessToken {
42 /// The value used for for authentication or authorization.
43 pub access_token: String,
44
45 /// The number of seconds until the token expires.
46 pub expires_in: i128,
47
48 /// A special token used to request a new token after this one expires or is revoked.
49 pub refresh_token: String,
50
51 /// The [Drive API scopes](https://developers.google.com/drive/api/guides/api-specific-auth) added to this access token as a
52 /// string separated by spaces.
53 pub scope: String,
54
55 /// Type of the token, an access token should always be of the
56 /// [Bearer](https://cloud.google.com/docs/authentication/token-types#bearer) type.
57 pub token_type: String,
58}
59
60#[cfg(not(tarpaulin_include))]
61impl fmt::Debug for AccessToken {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("AccessToken")
64 .field("access_token", &format_args!("[hidden for security]"))
65 .field("expires_in", &self.expires_in)
66 .field("refresh_token", &format_args!("[hidden for security]"))
67 .field("scope", &self.scope)
68 .field("token_type", &self.token_type)
69 .finish()
70 }
71}
72
73impl AccessToken {
74 const TOKEN_INFO_URI: &'static str = "https://oauth2.googleapis.com/tokeninfo";
75
76 /// Updates an [`AccessToken`] with the values of a [`RefreshToken`].
77 fn update_with( &mut self, refresh_token: &RefreshToken ) {
78 self.access_token = refresh_token.access_token.clone();
79 self.expires_in = refresh_token.expires_in;
80 self.scope = refresh_token.scope.clone();
81 self.token_type = refresh_token.token_type.clone();
82 }
83
84 /// Checks if an [`AccessToken`] is valid by making a request to the
85 /// [tokeninfo](https://developers.google.com/identity/sign-in/web/backend-auth#calling-the-tokeninfo-endpoint)
86 /// endpoint of the Google Drive API.
87 ///
88 /// # Note
89 ///
90 /// [`is_valid`](AccessToken::is_valid) will return false if the token has expired or has been revoked, but it will also
91 /// return false if the tokeninfo endpoint cannot be reached or if the request returns an error response of any kind.
92 ///
93 /// # Examples:
94 ///
95 /// ```no_run
96 /// use drive_v3::ClientSecrets;
97 ///
98 /// # use drive_v3::{Credentials, Error};
99 /// # use drive_v3::AccessToken;
100 /// #
101 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
102 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
103 /// #
104 /// # let credentials = Credentials::from_file(&credentials_path, &scopes)?;
105 /// #
106 /// # let mut access_token = credentials.access_token;
107 /// # let my_client_secrets = credentials.client_secrets;
108 /// #
109 /// let secrets_path = "my_client_secrets.json";
110 /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
111 /// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
112 ///
113 /// if access_token.is_valid() {
114 /// // Do something with your valid token
115 /// } else {
116 /// access_token.refresh(&my_client_secrets)?;
117 /// }
118 /// # Ok::<(), Error>(())
119 /// ```
120 pub fn is_valid( &self ) -> bool {
121 let parameters = [ ("access_token", &self.access_token) ];
122
123 let request_url = match Url::parse_with_params(Self::TOKEN_INFO_URI, ¶meters) {
124 Ok(url) => url,
125 #[cfg(not(tarpaulin_include))]
126 Err(_) => return false,
127 };
128
129 let response = match reqwest::blocking::get(request_url) {
130 Ok(response) => response,
131 #[cfg(not(tarpaulin_include))]
132 Err(_) => return false,
133 };
134
135 response.status() == 200
136 }
137
138 /// Checks if an [`AccessToken`] has all of the specified `scopes`.
139 ///
140 /// See [Choose scopes](https://developers.google.com/drive/api/guides/api-specific-auth) documentation, for information on
141 /// the scopes supported by the Google Drive API.
142 ///
143 /// # Note
144 ///
145 /// [`has_scopes`](AccessToken::has_scopes) does not check if the present `scopes` are equal to the specified ones, it
146 /// only checks that all specified `scopes` are present in the [`AccessToken`]'s scopes.
147 ///
148 /// # Examples
149 ///
150 /// ```rust
151 /// # use drive_v3::{Credentials, Error};
152 /// # use drive_v3::AccessToken;
153 /// #
154 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
155 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
156 /// #
157 /// # let credentials = Credentials::from_file(&credentials_path, &scopes)?;
158 /// # let access_token = credentials.access_token;
159 /// #
160 /// let required_scopes = [
161 /// "https://www.googleapis.com/auth/drive.metadata.readonly",
162 /// "https://www.googleapis.com/auth/drive.file",
163 /// ];
164 ///
165 /// assert!( access_token.has_scopes(&required_scopes) );
166 /// # Ok::<(), Error>(())
167 /// ```
168 pub fn has_scopes<T: AsRef<str>> ( &self, scopes: &[T] ) -> bool {
169 let token_scopes: Vec<&str> = self.scope.split(' ').collect();
170
171 scopes.iter().all( |s| token_scopes.contains(&s.as_ref()) )
172 }
173
174 /// Requests an [`AccessToken`] with the specified `scopes` from the Google Drive API using
175 /// [OAuth2](https://developers.google.com/identity/protocols/oauth2/native-app#obtainingaccesstokens).
176 ///
177 /// See [Choose scopes](https://developers.google.com/drive/api/guides/api-specific-auth), for information on the scopes
178 /// supported by the Google Drive API.
179 ///
180 /// # Examples:
181 ///
182 /// ```no_run
183 /// use drive_v3::AccessToken;
184 /// use drive_v3::ClientSecrets;
185 /// # use drive_v3::Error;
186 ///
187 /// // Load your client_secrets file
188 /// let secrets_path = "my_client_secrets.json";
189 /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
190 /// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
191 ///
192 /// // Request an access token, this will prompt you to authorize via the browser
193 /// let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
194 /// let my_access_token = AccessToken::request(&my_client_secrets, &scopes)?;
195 ///
196 /// // After getting your token you can make a request (with reqwest for example) using it for authorization
197 /// let client = reqwest::blocking::Client::new();
198 /// let body = client.get("google-api-endpoint")
199 /// .bearer_auth(&my_access_token.access_token)
200 /// .send()?
201 /// .text()?;
202 ///
203 /// println!("response: {:?}", body);
204 /// # Ok::<(), Error>(())
205 /// ```
206 ///
207 /// # Errors
208 ///
209 /// - a [`HexDecoding`](crate::ErrorKind::HexDecoding) or [`UrlParsing`](crate::ErrorKind::UrlParsing) error, if the
210 /// creation of the `code verifier` failed.
211 /// - a [`Request`](crate::ErrorKind::Request) error, if unable to send the request or get a body from the response.
212 /// - a [`Response`](crate::ErrorKind::Response) error, if the token request returned an error response.
213 /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the response's body to an [`AccessToken`].
214 /// - a [`MismatchedScopes`](crate::ErrorKind::MismatchedScopes) error, if the scopes in the created [`AccessToken`] are
215 /// different to the `scopes` passed to the function.
216 #[cfg(not(tarpaulin_include))]
217 pub fn request<T: AsRef<str>> ( client_secrets: &ClientSecrets, scopes: &[T], ) -> crate::Result<Self> {
218 let (authorization_code, code_verifier) = client_secrets.get_authorization_code(scopes, true)?;
219 let redirect_uri = &LocalServer::default().uri;
220
221 let parameters = [
222 ( "client_id", &client_secrets.client_id ),
223 ( "client_secret", &client_secrets.client_secret ),
224 ( "code", &authorization_code.to_string() ),
225 ( "code_verifier", &code_verifier.to_string() ),
226 ( "grant_type", &String::from("authorization_code") ),
227 ( "redirect_uri", &redirect_uri ),
228 ];
229
230 let request = Client::new()
231 .post(&client_secrets.token_uri)
232 .form(¶meters);
233
234 let response = request.send()?;
235
236 if response.status() != 200 {
237 return Err( response.into() );
238 }
239
240 let access_token: AccessToken = serde_json::from_str( &response.text()? )?;
241
242 if !access_token.has_scopes(scopes) {
243 return Err( Error::new(
244 ErrorKind::MismatchedScopes,
245 "created access token does not contain the requested scopes",
246 ) )
247 }
248
249 Ok(access_token)
250 }
251
252 /// Refreshes an [`AccessToken`] by requesting a new token from
253 /// [OAuth2](https://developers.google.com/identity/protocols/oauth2/native-app#offline) using its
254 /// [`refresh_token`](AccessToken::refresh_token).
255 ///
256 /// # Note
257 ///
258 /// There are limits on the number of refresh tokens that your application will be issued:
259 ///
260 /// - A limit per client/user combination.
261 /// - A limit per user across all clients.
262 ///
263 /// It is your responsibility save refreshed tokens in long-term storage and continue to use them for as long as they remain
264 /// valid. If your application requests too many refresh tokens, it may run into these limits, in which case older refresh
265 /// tokens will stop working.
266 ///
267 /// # Examples:
268 ///
269 /// ```no_run
270 /// use drive_v3::ClientSecrets;
271 /// #
272 /// # use drive_v3::{Credentials, Error};
273 /// #
274 /// # let credentials_path = "../.secure-files/google_drive_credentials.json";
275 /// # let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
276 /// # let credentials = Credentials::from_file(credentials_path, &scopes)?;
277 /// #
278 /// # let mut access_token = credentials.access_token;
279 ///
280 /// let secrets_path = "my_client_secrets.json";
281 /// # let secrets_path = "../.secure-files/google_drive_secrets.json";
282 /// let my_client_secrets = ClientSecrets::from_file(secrets_path)?;
283 ///
284 /// if !access_token.is_valid() {
285 /// access_token.refresh(&my_client_secrets)?;
286 /// }
287 ///
288 /// assert!( access_token.is_valid() );
289 /// # Ok::<(), Error>(())
290 /// ```
291 ///
292 /// # Errors
293 ///
294 /// - a [`Request`](crate::ErrorKind::Request) error, if unable to send the refresh request or get a body from the response.
295 /// - a [`Json`](crate::ErrorKind::Json) error, if unable to parse the received token from JSON.
296 pub fn refresh( &mut self, client_secrets: &ClientSecrets ) -> crate::Result<()> {
297 let body: [(&str, &str); 4] = [
298 ( "client_id", &client_secrets.client_id ),
299 ( "client_secret", &client_secrets.client_secret ),
300 ( "grant_type", "refresh_token" ),
301 ( "refresh_token", &self.refresh_token ),
302 ];
303
304 let request = Client::new().post(&client_secrets.token_uri).form(&body);
305 let response = request.send()?;
306
307 if response.status() != 200 {
308 return Err( response.into() );
309 }
310
311 let refresh_token: RefreshToken = serde_json::from_str( &response.text()? )?;
312 self.update_with(&refresh_token);
313
314 Ok(())
315 }
316}
317
318/// A special token used to
319/// [update](https://cloud.google.com/docs/authentication/token-types#refresh) expired or invalid [`AccessTokens`](AccessToken)
320/// without having to prompt the user for authorization.
321#[derive(Clone, Serialize, Deserialize)]
322pub struct RefreshToken {
323 /// The value used for for authentication or authorization.
324 pub access_token: String,
325
326 /// The number of seconds until the new token expires.
327 pub expires_in: i128,
328
329 /// The [Drive API scopes](https://developers.google.com/drive/api/guides/api-specific-auth) added to this access token as a
330 /// string separated by spaces.
331 pub scope: String,
332
333 /// Type of this token.
334 pub token_type: String,
335}
336
337#[cfg(not(tarpaulin_include))]
338impl fmt::Debug for RefreshToken {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("RefreshToken")
341 .field("access_token", &format_args!("[hidden for security]"))
342 .field("expires_in", &self.expires_in)
343 .field("scope", &self.scope)
344 .field("token_type", &self.token_type)
345 .finish()
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use crate::ErrorKind;
352 use super::{AccessToken, RefreshToken};
353 use crate::utils::test::{VALID_CREDENTIALS, INVALID_CREDENTIALS};
354
355 fn get_test_access_token() -> AccessToken {
356 AccessToken {
357 access_token: String::from("test_access_token"),
358 expires_in: 1984,
359 refresh_token: String::from("test_refresh_token"),
360 scope: String::from("test_scope_one test_scope_two"),
361 token_type: String::from("Bearer"),
362 }
363 }
364
365 fn get_test_refresh_token() -> RefreshToken {
366 RefreshToken {
367 access_token: String::from("updated_test_access_token"),
368 expires_in: 9999,
369 scope: String::from("test_scope_one test_scope_two"),
370 token_type: String::from("Bearer"),
371 }
372 }
373
374 #[test]
375 fn update_with_test() {
376 let mut access_token = get_test_access_token();
377 let refresh_token_value = access_token.refresh_token.clone();
378
379 let refresh_token = get_test_refresh_token();
380 access_token.update_with(&refresh_token);
381
382 assert_eq!(access_token.access_token, refresh_token.access_token);
383 assert_eq!(access_token.expires_in, refresh_token.expires_in);
384 assert_eq!(access_token.refresh_token, refresh_token_value);
385 assert_eq!(access_token.scope, refresh_token.scope);
386 assert_eq!(access_token.token_type, refresh_token.token_type);
387 }
388
389 #[test]
390 fn is_valid_test() {
391 let invalid_access_token = get_test_access_token();
392
393 assert!( !invalid_access_token.is_valid() );
394 assert!( VALID_CREDENTIALS.access_token.is_valid() );
395 }
396
397 #[test]
398 fn has_scopes_test() {
399 let access_token = get_test_access_token();
400
401 let scopes = ["test_scope_one", "test_scope_two"];
402 assert!( access_token.has_scopes(&scopes) );
403
404 let scopes = ["test_scope_one"];
405 assert!( access_token.has_scopes(&scopes) );
406
407 let scopes= ["test_scope_two"];
408 assert!( access_token.has_scopes(&scopes) );
409 }
410
411 #[test]
412 fn has_scopes_mismatch_test() {
413 let access_token = get_test_access_token();
414
415 let scopes = ["invalid_test_scope"];
416 assert!( !access_token.has_scopes(&scopes) );
417
418 let scopes = ["test_scope_one", "test_scope_one", "test_scope_three"];
419 assert!( !access_token.has_scopes(&scopes) );
420
421 let scopes = ["test_scope_one", "invalid_test_scope_two"];
422 assert!( !access_token.has_scopes(&scopes) );
423 }
424
425 #[test]
426 fn has_scopes_empty_test() {
427 let access_token = get_test_access_token();
428
429 let scopes: [&str; 0] = [];
430 assert!( access_token.has_scopes(&scopes) );
431 }
432
433 #[test]
434 #[ignore = "requires user input (CI/CD)"]
435 fn request_test() {
436 let scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
437 let access_token = AccessToken::request(&VALID_CREDENTIALS.client_secrets, &scopes);
438
439 assert!( access_token.is_ok() )
440 }
441
442 #[test]
443 #[ignore = "requires user input (CI/CD)"]
444 fn request_invalid_secrets_uri_test() {
445 let mut client_secrets = VALID_CREDENTIALS.client_secrets.clone();
446
447 client_secrets.token_uri = String::from("invalid-token-uri");
448
449 let scopes: [&str; 1] = ["https://www.googleapis.com/auth/drive.metadata.readonly"];
450 let result = AccessToken::request(&client_secrets, &scopes);
451
452 assert!( result.is_err() );
453 assert_eq!( result.unwrap_err().kind, ErrorKind::Request );
454 }
455
456 #[test]
457 #[ignore = "requires user input (CI/CD)"]
458 fn request_mismatched_scopes_test() {
459 // MAKE SURE TO ONLY GIVE PERMISSION FOR ONE OF THE REQUESTED SCOPES
460
461 let client_secrets = VALID_CREDENTIALS.client_secrets.clone();
462
463 let scopes = [
464 "https://www.googleapis.com/auth/drive.metadata.readonly",
465 "https://www.googleapis.com/auth/drive.file",
466 ];
467
468 let access_token = AccessToken::request(&client_secrets, &scopes);
469
470 assert!( access_token.is_err() );
471 assert_eq!( access_token.unwrap_err().kind, ErrorKind::MismatchedScopes );
472 }
473
474 #[test]
475 fn refresh_test() {
476 let mut invalid_credentials = INVALID_CREDENTIALS.clone();
477
478 let result = invalid_credentials.access_token.refresh(&invalid_credentials.client_secrets);
479
480 assert!( result.is_ok() );
481 assert!( invalid_credentials.access_token.is_valid() );
482 }
483
484 #[test]
485 fn refresh_invalid_refresh_token_test() {
486 let mut credentials = VALID_CREDENTIALS.clone();
487
488 credentials.access_token.refresh_token = String::from("invalid-token");
489
490 let result = credentials.access_token.refresh(&credentials.client_secrets);
491
492 assert!( result.is_err() );
493 assert_eq!( result.unwrap_err().kind, ErrorKind::Response );
494 }
495
496 #[test]
497 fn refresh_invalid_secrets_test() {
498 let mut credentials = VALID_CREDENTIALS.clone();
499
500 credentials.client_secrets.client_id = String::from("invalid-client-id");
501 credentials.client_secrets.client_secret = String::from("invalid-client-secret");
502
503 let result = credentials.access_token.refresh(&credentials.client_secrets);
504
505 assert!( result.is_err() );
506 assert_eq!( result.unwrap_err().kind, ErrorKind::Response );
507 }
508
509 #[test]
510 fn refresh_invalid_secrets_uri_test() {
511 let mut credentials = VALID_CREDENTIALS.clone();
512
513 credentials.client_secrets.token_uri = String::from("invalid-token-uri");
514
515 let result = credentials.access_token.refresh(&credentials.client_secrets);
516
517 assert!( result.is_err() );
518 assert_eq!( result.unwrap_err().kind, ErrorKind::Request );
519 }
520}