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, &parameters) {
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(&parameters);
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}